mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-07 01:20:16 +00:00
Compare commits
1313 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23a2c42469 | ||
|
|
7993e3d10d | ||
|
|
bde9ee5a4c | ||
|
|
f9846a27b6 | ||
|
|
ab74afdd8d | ||
|
|
7cb21a24d4 | ||
|
|
5e26f6f3ae | ||
|
|
cce32f8dc5 | ||
|
|
1505055334 | ||
|
|
027542e2e5 | ||
|
|
0294fba042 | ||
|
|
07699b5871 | ||
|
|
b8849c8cbf | ||
|
|
00ab5cd6f2 | ||
|
|
858e87ab0d | ||
|
|
6c485c282d | ||
|
|
4bae6851d1 | ||
|
|
5288a7dc9a | ||
|
|
516deb22aa | ||
|
|
4e2ffe79a4 | ||
|
|
47256cdde6 | ||
|
|
48ee9ddb22 | ||
|
|
ad13f14432 | ||
|
|
4e34e8f1c2 | ||
|
|
cb76945688 | ||
|
|
87538711b6 | ||
|
|
822b15ea43 | ||
|
|
3598c11c8d | ||
|
|
d45325b6d7 | ||
|
|
64fc859dac | ||
|
|
3536fd7d60 | ||
|
|
15099fade5 | ||
|
|
6fe5899639 | ||
|
|
4961a4fab1 | ||
|
|
e549aac127 | ||
|
|
2bca242fdc | ||
|
|
4bd0ab76c6 | ||
|
|
a46e5ef621 | ||
|
|
ae8ad55cb8 | ||
|
|
84b1c9d8c2 | ||
|
|
fd40a80a68 | ||
|
|
5f19c55731 | ||
|
|
610c2ea131 | ||
|
|
8f8c484bc6 | ||
|
|
f1c7f8e813 | ||
|
|
e377d33652 | ||
|
|
db9ce032a4 | ||
|
|
dfdda2c871 | ||
|
|
32090aee16 | ||
|
|
20326054da | ||
|
|
dc60eee50e | ||
|
|
cf66532ac1 | ||
|
|
217950b9ad | ||
|
|
f3ee8d6322 | ||
|
|
b2aeaa2dcc | ||
|
|
dcb99e4972 | ||
|
|
25fd4d9f2c | ||
|
|
bf7940d7ff | ||
|
|
19977b4659 | ||
|
|
1a9551db82 | ||
|
|
5b46ce579b | ||
|
|
493055731e | ||
|
|
415ddf59bb | ||
|
|
03dc63f6c8 | ||
|
|
4eada9a908 | ||
|
|
512993b57f | ||
|
|
ca91bb2f7f | ||
|
|
8993affdc0 | ||
|
|
ff23e5ba37 | ||
|
|
0d1221155e | ||
|
|
c5eabe3143 | ||
|
|
97c7c34f6f | ||
|
|
3e54d70ae2 | ||
|
|
a7f470d1d9 | ||
|
|
428581dd05 | ||
|
|
572a1ca42a | ||
|
|
3bfc3dd45b | ||
|
|
db7e8b5619 | ||
|
|
54c438d8d3 | ||
|
|
1731af3f29 | ||
|
|
11fd81e398 | ||
|
|
88dfa7baa6 | ||
|
|
75e95c45a2 | ||
|
|
c6ea29d916 | ||
|
|
e9f587ecba | ||
|
|
3553101eb3 | ||
|
|
b01dd76be1 | ||
|
|
95614e5220 | ||
|
|
ae9c2ab165 | ||
|
|
33d328d967 | ||
|
|
759db7d7d5 | ||
|
|
4c18e08036 | ||
|
|
a5b88c489e | ||
|
|
17f977a9de | ||
|
|
c571dd4f0e | ||
|
|
94ed41f236 | ||
|
|
26fc878944 | ||
|
|
b57e9f58fd | ||
|
|
d18fc97717 | ||
|
|
b80d1925ff | ||
|
|
31a049eb69 | ||
|
|
cf45e57d9c | ||
|
|
1b91c26409 | ||
|
|
5d273a0c76 | ||
|
|
da6df07a9d | ||
|
|
7799e14121 | ||
|
|
2eaf689f71 | ||
|
|
8c45c8b8b9 | ||
|
|
1d3ef8734c | ||
|
|
547adda446 | ||
|
|
fbf8003237 | ||
|
|
4d922a0f9b | ||
|
|
8413c38295 | ||
|
|
adf582dba7 | ||
|
|
921d95357d | ||
|
|
1f70929e53 | ||
|
|
a7ddcc9c0f | ||
|
|
cb4b6c844a | ||
|
|
8c2b5ea7c4 | ||
|
|
de1ec90133 | ||
|
|
44a24605ad | ||
|
|
570db98548 | ||
|
|
d22d9b22b1 | ||
|
|
b93804529d | ||
|
|
78bf5648e7 | ||
|
|
c3278a8262 | ||
|
|
d4f6d65e1d | ||
|
|
5ebd004a10 | ||
|
|
459863bcff | ||
|
|
fe3401e037 | ||
|
|
933ce76057 | ||
|
|
d5a42e9d9c | ||
|
|
b8eca1ffbf | ||
|
|
49a1b4262d | ||
|
|
e903c941cb | ||
|
|
974206ebe1 | ||
|
|
687662c990 | ||
|
|
d1df3cd4d5 | ||
|
|
656bf2c60c | ||
|
|
633137d501 | ||
|
|
3916e23bbd | ||
|
|
afd2e214bc | ||
|
|
8d8a133c89 | ||
|
|
d085807070 | ||
|
|
58ddff0881 | ||
|
|
bfe20c11c3 | ||
|
|
e7c6d2c9d9 | ||
|
|
cdb8d746ef | ||
|
|
cadcc6cabe | ||
|
|
11da8d0dff | ||
|
|
f842bca471 | ||
|
|
0a699df5e8 | ||
|
|
5180285456 | ||
|
|
8ce69e802d | ||
|
|
0046df4b51 | ||
|
|
c2609b239f | ||
|
|
28408a9f64 | ||
|
|
9950ce2334 | ||
|
|
2b64c573c3 | ||
|
|
f4a3b194da | ||
|
|
f04b3d5042 | ||
|
|
59cf6f5ec9 | ||
|
|
3d3f692fd8 | ||
|
|
b2596c660b | ||
|
|
e715741abc | ||
|
|
813125e122 | ||
|
|
92ea45070c | ||
|
|
9412110c82 | ||
|
|
960b28c90a | ||
|
|
ca386a4b25 | ||
|
|
99c445a6d6 | ||
|
|
96cd467cfa | ||
|
|
e24d5cb97d | ||
|
|
58c0ef90c9 | ||
|
|
e632fcd933 | ||
|
|
78ff63a9c7 | ||
|
|
e7ccd26c70 | ||
|
|
3db0efa69f | ||
|
|
6fea478d2e | ||
|
|
2c400363e8 | ||
|
|
9d0efedaee | ||
|
|
33e9e0fb2d | ||
|
|
ef1eb4c888 | ||
|
|
0ac2dc388e | ||
|
|
a0bc0fdf21 | ||
|
|
192fce51d7 | ||
|
|
774cff3c72 | ||
|
|
0c59bc5e35 | ||
|
|
64bc36304f | ||
|
|
7e1779d48c | ||
|
|
b6c48a694b | ||
|
|
216d5f6b52 | ||
|
|
bebca337c4 | ||
|
|
61ecb13bf0 | ||
|
|
37900a92db | ||
|
|
997ed151db | ||
|
|
3db2c0d43e | ||
|
|
a8ceeec0fd | ||
|
|
83a1cce1ea | ||
|
|
548ace0115 | ||
|
|
092979b8cc | ||
|
|
02ebb9f0c3 | ||
|
|
5ff0bfb81d | ||
|
|
ed8b7d400c | ||
|
|
2cdff00788 | ||
|
|
339c11dd86 | ||
|
|
0292d991af | ||
|
|
bf944d9219 | ||
|
|
7df8c8c287 | ||
|
|
217c082ac1 | ||
|
|
588dcf492b | ||
|
|
2fdf939ca9 | ||
|
|
5f38625f21 | ||
|
|
d669eb6d05 | ||
|
|
e9d5a91def | ||
|
|
b765dc005b | ||
|
|
303b455965 | ||
|
|
f45a6a7004 | ||
|
|
f987393b32 | ||
|
|
c23afed39a | ||
|
|
1fd8139138 | ||
|
|
269f80bf8e | ||
|
|
0b51d970b4 | ||
|
|
a8e565eca8 | ||
|
|
50c8e3fcda | ||
|
|
ec824927c1 | ||
|
|
4ebdb19682 | ||
|
|
3cd9c02f71 | ||
|
|
e2cebe26e8 | ||
|
|
c174d19d1e | ||
|
|
cdc1b5d629 | ||
|
|
b01159f234 | ||
|
|
5d439b127b | ||
|
|
c46088405a | ||
|
|
003668cfaa | ||
|
|
6447db063a | ||
|
|
65f846ade0 | ||
|
|
407d8a5019 | ||
|
|
6cb6cb9e69 | ||
|
|
1c06806f90 | ||
|
|
7d15452c30 | ||
|
|
07286a73b1 | ||
|
|
02c3b1c9e2 | ||
|
|
d2fb2b8095 | ||
|
|
328dab2463 | ||
|
|
97a096b507 | ||
|
|
3b4dec442d | ||
|
|
16a0815fac | ||
|
|
3cb678f84c | ||
|
|
49948d72f3 | ||
|
|
8b0e96474b | ||
|
|
bf6b72eb55 | ||
|
|
8421cabb9d | ||
|
|
46de65cab9 | ||
|
|
351c64e99e | ||
|
|
1a62f1299d | ||
|
|
4b256cab31 | ||
|
|
233969bb58 | ||
|
|
c6766d45b5 | ||
|
|
4317c8e583 | ||
|
|
e3c3f5a6d0 | ||
|
|
d4c20c472b | ||
|
|
b77cce4ec5 | ||
|
|
8bcd36377a | ||
|
|
c9c2e39531 | ||
|
|
dd8af5565b | ||
|
|
a92092340b | ||
|
|
c5eec32c58 | ||
|
|
7465250141 | ||
|
|
69c396825b | ||
|
|
6aba43f6cc | ||
|
|
988a8526b5 | ||
|
|
3791b75000 | ||
|
|
2fcce3b3c5 | ||
|
|
da80ebcc6b | ||
|
|
cc44ecc62f | ||
|
|
0881a8ae6f | ||
|
|
d3a02ec038 | ||
|
|
42081b1937 | ||
|
|
9f6d1b10ad | ||
|
|
1616df2f61 | ||
|
|
c670ce416b | ||
|
|
f48fce8bd3 | ||
|
|
24e2da4557 | ||
|
|
416ab4ebf0 | ||
|
|
a2aafeb959 | ||
|
|
34c4614682 | ||
|
|
9e429239ab | ||
|
|
96c001e668 | ||
|
|
4facbe02fb | ||
|
|
a70765ed90 | ||
|
|
4a5e95511e | ||
|
|
dfb3d21a6d | ||
|
|
b0554682ed | ||
|
|
da4a09f977 | ||
|
|
3068210a93 | ||
|
|
7f4c7fe4e8 | ||
|
|
dd3711bdbd | ||
|
|
b15e8d5bbc | ||
|
|
dca3ba2f77 | ||
|
|
24305ba5bf | ||
|
|
4e52f9699b | ||
|
|
89ba802b23 | ||
|
|
020fc15d98 | ||
|
|
1273023ac3 | ||
|
|
4a73c366fa | ||
|
|
a5a4ef3fd7 | ||
|
|
2a49f177fe | ||
|
|
8918422156 | ||
|
|
fc7b2b11a2 | ||
|
|
402d080990 | ||
|
|
ae48e75ad7 | ||
|
|
440cbd5235 | ||
|
|
d7412c4df1 | ||
|
|
aa76bf39ab | ||
|
|
29b54d6638 | ||
|
|
f7cf978f68 | ||
|
|
1ac1cd6c14 | ||
|
|
5949571fe7 | ||
|
|
1c86ec5b8d | ||
|
|
43e7ad1b1c | ||
|
|
2438b8b66b | ||
|
|
68698e0ac8 | ||
|
|
efb0f6e23b | ||
|
|
4b3f743885 | ||
|
|
bab2846513 | ||
|
|
af83bf6712 | ||
|
|
fe6832fae8 | ||
|
|
2221a13a4d | ||
|
|
f3dbcdc7b3 | ||
|
|
af7ae048f8 | ||
|
|
1071d063ab | ||
|
|
7614d8f87a | ||
|
|
f4e50079de | ||
|
|
92e2ff4985 | ||
|
|
9b1ca64a75 | ||
|
|
ad6eacb3e9 | ||
|
|
fd535183ee | ||
|
|
6bc1dc4020 | ||
|
|
d59aa6af25 | ||
|
|
f139c02e95 | ||
|
|
7249785bcb | ||
|
|
0a8b026ccf | ||
|
|
82a6b83524 | ||
|
|
9024a19658 | ||
|
|
53da1099d1 | ||
|
|
395bb64b26 | ||
|
|
7a07263281 | ||
|
|
1c6825cc7a | ||
|
|
5ab9929cbb | ||
|
|
36d730229a | ||
|
|
b63691f6e2 | ||
|
|
13fad06239 | ||
|
|
f21960ec9d | ||
|
|
ecabff7eb4 | ||
|
|
80b2710e6f | ||
|
|
b0f0b7b75e | ||
|
|
fb3a01fa3a | ||
|
|
d30d79b5be | ||
|
|
ea80b9208d | ||
|
|
394f77c3ff | ||
|
|
2f39dc19a2 | ||
|
|
2aa79f4fbe | ||
|
|
bfa36a72b9 | ||
|
|
71ef8f0636 | ||
|
|
20cf0b7aeb | ||
|
|
946d02536b | ||
|
|
ac2a177070 | ||
|
|
21fe249d62 | ||
|
|
d84f5b30b8 | ||
|
|
188de756be | ||
|
|
baf472f83f | ||
|
|
841df4da71 | ||
|
|
f2de2d644a | ||
|
|
d9a9e9eb30 | ||
|
|
4a1597f295 | ||
|
|
86d3180666 | ||
|
|
864de6a7a4 | ||
|
|
ea6bec96d3 | ||
|
|
f618f99ece | ||
|
|
12ce441e67 | ||
|
|
0985bfb775 | ||
|
|
9de9661baa | ||
|
|
6f3f631fd1 | ||
|
|
da511334d2 | ||
|
|
40342af459 | ||
|
|
8e8bbb00f5 | ||
|
|
ef9c4476a0 | ||
|
|
d5aa965522 | ||
|
|
7a756e5d9d | ||
|
|
7c06399512 | ||
|
|
7d709542ca | ||
|
|
fa955cc2a4 | ||
|
|
aa80900a8e | ||
|
|
b4b492824e | ||
|
|
b29517bd01 | ||
|
|
0f192579ac | ||
|
|
beae9acfcc | ||
|
|
53216a500d | ||
|
|
e7858b6d7e | ||
|
|
0d278f5da8 | ||
|
|
b1ee6fd7ed | ||
|
|
d6bcffa929 | ||
|
|
c5a25f610a | ||
|
|
194e1e9151 | ||
|
|
c2f2e26ec5 | ||
|
|
6d4617960d | ||
|
|
70137409ed | ||
|
|
ed241ba032 | ||
|
|
2a44558fbd | ||
|
|
a10c2ec88d | ||
|
|
2d1dfb3b34 | ||
|
|
da1dda3e1d | ||
|
|
967ce43b59 | ||
|
|
8e358ef35a | ||
|
|
51b81b472d | ||
|
|
4f6acf114c | ||
|
|
7c7d9d6326 | ||
|
|
4841b6d4ba | ||
|
|
fc121f9785 | ||
|
|
332b2869ef | ||
|
|
c372929ab6 | ||
|
|
f4e64ac253 | ||
|
|
da87990bd6 | ||
|
|
cf1feee21d | ||
|
|
ad9226eeec | ||
|
|
6603e39e6a | ||
|
|
5e2236f9ff | ||
|
|
acb2d171e8 | ||
|
|
f3bb3943c9 | ||
|
|
7bd604e3be | ||
|
|
d56e389a95 | ||
|
|
bb4a20174c | ||
|
|
15be181642 | ||
|
|
db2e350e29 | ||
|
|
1342bcedaf | ||
|
|
be6d41ffe5 | ||
|
|
53f69bf089 | ||
|
|
51edfeb3d0 | ||
|
|
9e57ed2b1f | ||
|
|
4ae0844ee3 | ||
|
|
06a5a40e90 | ||
|
|
f0382357ca | ||
|
|
4be99c2989 | ||
|
|
9c0826592c | ||
|
|
8f0997d17d | ||
|
|
58b1a891ce | ||
|
|
e9abbe89f3 | ||
|
|
b3e6cd59a1 | ||
|
|
f22d023c4b | ||
|
|
4c8111ef98 | ||
|
|
f05dce54a7 | ||
|
|
514e0fd4b6 | ||
|
|
cb939ed450 | ||
|
|
7ea38a0c9d | ||
|
|
f1ddbfaae4 | ||
|
|
449739e6a3 | ||
|
|
ac9345b47a | ||
|
|
cd198dfea8 | ||
|
|
3187b5ba2d | ||
|
|
eea3a29699 | ||
|
|
5356044b77 | ||
|
|
71e6a94af7 | ||
|
|
5662be894e | ||
|
|
a065becea5 | ||
|
|
bf8cdda2f5 | ||
|
|
8afbece683 | ||
|
|
b3b1961496 | ||
|
|
5ffe5ab43f | ||
|
|
dc3c2823ac | ||
|
|
82c5820767 | ||
|
|
f5cf7ac25b | ||
|
|
456017e0ae | ||
|
|
c5cec1cc77 | ||
|
|
4d1a7624f4 | ||
|
|
f71627567b | ||
|
|
c8f996e29f | ||
|
|
be2a9a8d1a | ||
|
|
bb04447c44 | ||
|
|
1116f5330e | ||
|
|
66104da10c | ||
|
|
1c445f88f6 | ||
|
|
e7bc1291a0 | ||
|
|
79bd6e77b8 | ||
|
|
da19fd0d1a | ||
|
|
07890b43ca | ||
|
|
27d0c1ecc2 | ||
|
|
80472ac198 | ||
|
|
f4667f86af | ||
|
|
5fefc12d1e | ||
|
|
13b560971e | ||
|
|
9aed791fc3 | ||
|
|
3dac27a8a9 | ||
|
|
f74e850b5c | ||
|
|
4fe5dfa74c | ||
|
|
636a0dbde7 | ||
|
|
c18a6433d4 | ||
|
|
34034af1c9 | ||
|
|
07639c79d9 | ||
|
|
25d80f35f1 | ||
|
|
75e517a2da | ||
|
|
6684855767 | ||
|
|
10ef8e6e4b | ||
|
|
cecda27d73 | ||
|
|
984e207b59 | ||
|
|
693d0b8f45 | ||
|
|
66df7f1aaf | ||
|
|
259b5e8451 | ||
|
|
e1170d4edb | ||
|
|
81b956c70d | ||
|
|
868eb478d8 | ||
|
|
3db09c4d15 | ||
|
|
83c53113af | ||
|
|
d224358e21 | ||
|
|
72aef114ab | ||
|
|
6045bd89fb | ||
|
|
5b096cc3db | ||
|
|
917af4705b | ||
|
|
9ac53ef8cf | ||
|
|
2fc00508fb | ||
|
|
c72074b48e | ||
|
|
3ef2c946d5 | ||
|
|
aaf1d499bf | ||
|
|
94982392be | ||
|
|
51276c60bf | ||
|
|
78a3f43d9d | ||
|
|
02a44664b9 | ||
|
|
1fa0454288 | ||
|
|
ca0e8dedfb | ||
|
|
ba11afafb9 | ||
|
|
7e1437c6b1 | ||
|
|
1aa5cc9178 | ||
|
|
bc1d685a8c | ||
|
|
f6b9853ad0 | ||
|
|
de38f54f22 | ||
|
|
96213f69a2 | ||
|
|
036333412d | ||
|
|
82e278029c | ||
|
|
b9cdc443d7 | ||
|
|
1561ef56ed | ||
|
|
f368ad946e | ||
|
|
918e71adb7 | ||
|
|
cf3188352b | ||
|
|
6860a18c12 | ||
|
|
ff553cc9dd | ||
|
|
574377636e | ||
|
|
b2d41b1cd9 | ||
|
|
9435830351 | ||
|
|
d694619a95 | ||
|
|
4f11518934 | ||
|
|
2d55d43d40 | ||
|
|
45f7677bdc | ||
|
|
099083ea6b | ||
|
|
7a322b6326 | ||
|
|
d1adb19b8a | ||
|
|
a0b1b34c71 | ||
|
|
bf8b9b90cd | ||
|
|
c5757a0266 | ||
|
|
ee447abcad | ||
|
|
a940a87ddc | ||
|
|
5813e81dc6 | ||
|
|
a6d3be4dbf | ||
|
|
166bec0c08 | ||
|
|
c8d67beb9c | ||
|
|
392dc8af59 | ||
|
|
9605593d11 | ||
|
|
b95a178584 | ||
|
|
fbf6320614 | ||
|
|
e06adc6d7e | ||
|
|
1f76377a7c | ||
|
|
dca75a08ba | ||
|
|
2d61dbc774 | ||
|
|
3ee9a67aa4 | ||
|
|
ae953b0884 | ||
|
|
d5bf210998 | ||
|
|
389285585d | ||
|
|
3656eb4740 | ||
|
|
f1bdf40dda | ||
|
|
d96cb61f26 | ||
|
|
7151615260 | ||
|
|
1550ab9e2f | ||
|
|
1132663cc7 | ||
|
|
3ccb17ce59 | ||
|
|
472ef19100 | ||
|
|
c65306f877 | ||
|
|
f7d80930f2 | ||
|
|
0fdf308874 | ||
|
|
7a8307fe7c | ||
|
|
697f6714a4 | ||
|
|
ec5fb77a66 | ||
|
|
f1c9ab4e4f | ||
|
|
3b0fb6aae8 | ||
|
|
6e72ee62ae | ||
|
|
37bfe44046 | ||
|
|
48ea055781 | ||
|
|
dcadfbbd4a | ||
|
|
9bcedf224e | ||
|
|
69ddec6589 | ||
|
|
72e80dbe0e | ||
|
|
c818aa13eb | ||
|
|
ba87eb6753 | ||
|
|
d170fbdb9f | ||
|
|
c58eb0d5a3 | ||
|
|
59f2bef187 | ||
|
|
1ca51c8586 | ||
|
|
c0936b103c | ||
|
|
9d3246ed12 | ||
|
|
ef99a5d972 | ||
|
|
a31bf77776 | ||
|
|
24e4c48468 | ||
|
|
2721f5ccc9 | ||
|
|
6806caffc7 | ||
|
|
52ca867670 | ||
|
|
72eb360f2d | ||
|
|
2b4736afcd | ||
|
|
7dc7c53029 | ||
|
|
327dcc98e3 | ||
|
|
87deaf1658 | ||
|
|
7679ee7321 | ||
|
|
4553651138 | ||
|
|
5383ba5587 | ||
|
|
432e8ef2bc | ||
|
|
70899d3ab2 | ||
|
|
b42b0d3fe5 | ||
|
|
7d9a84a445 | ||
|
|
1e6c5b205c | ||
|
|
c7620cca6f | ||
|
|
b02bb18a70 | ||
|
|
4e79b09dd9 | ||
|
|
6f5970a2e1 | ||
|
|
3d2cca6762 | ||
|
|
4354590a69 | ||
|
|
ef5b39c410 | ||
|
|
7b8e24a588 | ||
|
|
53841642a8 | ||
|
|
b08112f936 | ||
|
|
53ae5bce13 | ||
|
|
e8e80fe6b5 | ||
|
|
0e848d73f9 | ||
|
|
cbea225d97 | ||
|
|
a7d53227de | ||
|
|
437969eac9 | ||
|
|
bf4b224fcf | ||
|
|
e3117a2a23 | ||
|
|
c6a8e7d9b9 | ||
|
|
c96ab4fcbb | ||
|
|
efea61dc50 | ||
|
|
bc250a6afa | ||
|
|
284fac379c | ||
|
|
5aa13b9084 | ||
|
|
14ed6799d7 | ||
|
|
a7420ff2b5 | ||
|
|
e4e8ad6780 | ||
|
|
c0673c50e6 | ||
|
|
7d94913efb | ||
|
|
c9f73bd325 | ||
|
|
c03176af59 | ||
|
|
2771efb51c | ||
|
|
932b376b4e | ||
|
|
0c4ae63ad5 | ||
|
|
b99f6eb904 | ||
|
|
78af6bbb98 | ||
|
|
537c7e1137 | ||
|
|
5f16439752 | ||
|
|
3a8a94448a | ||
|
|
e9c88ae4f4 | ||
|
|
4847045259 | ||
|
|
997a016122 | ||
|
|
512f2cc9c4 | ||
|
|
6876b1a25b | ||
|
|
107e7d5d91 | ||
|
|
09d79b0a9b | ||
|
|
b5c9d99424 | ||
|
|
176e3fd141 | ||
|
|
95acf63ea3 | ||
|
|
90f5eb1270 | ||
|
|
7dfcba1649 | ||
|
|
e3152188ef | ||
|
|
231afe464a | ||
|
|
e68dc04900 | ||
|
|
4696622b0a | ||
|
|
83ea3c96ec | ||
|
|
333e63156e | ||
|
|
a0c3da17b4 | ||
|
|
4c7a1abd39 | ||
|
|
9fda37158a | ||
|
|
648fd2a622 | ||
|
|
99b0c9900e | ||
|
|
f6258221c1 | ||
|
|
68e534777c | ||
|
|
29686f63ac | ||
|
|
dcc1965bfe | ||
|
|
03ac0c91ae | ||
|
|
709b8ac2b7 | ||
|
|
e9670fd144 | ||
|
|
f9688d7519 | ||
|
|
da8b5a5367 | ||
|
|
28bcd01e8d | ||
|
|
fba67ef951 | ||
|
|
3fa01be9e4 | ||
|
|
270825ab2a | ||
|
|
008515c844 | ||
|
|
301ef1bdc6 | ||
|
|
cf1e167034 | ||
|
|
beed1ba089 | ||
|
|
2ab7e23790 | ||
|
|
fceb5f7b22 | ||
|
|
0dac2f7a8d | ||
|
|
6a6a718898 | ||
|
|
faec6f7f31 | ||
|
|
26dda48e50 | ||
|
|
3108accdee | ||
|
|
e0f060d89b | ||
|
|
380852b58e | ||
|
|
3dea0d2806 | ||
|
|
0505014152 | ||
|
|
3bd8cbc62f | ||
|
|
d583aaa0c3 | ||
|
|
3a7375f15e | ||
|
|
79a5fb469b | ||
|
|
9fd0c74e90 | ||
|
|
335e5d131c | ||
|
|
b7d42c1e93 | ||
|
|
0db0528e8e | ||
|
|
704e7e9f44 | ||
|
|
c58f7f293d | ||
|
|
19095552aa | ||
|
|
a64ff63a41 | ||
|
|
17db2b27bf | ||
|
|
ac8d73b258 | ||
|
|
a6f5c88b47 | ||
|
|
1c0408de08 | ||
|
|
9cebfd9d90 | ||
|
|
e932e5237e | ||
|
|
fbf221ae6d | ||
|
|
32808e4111 | ||
|
|
9f94f9de48 | ||
|
|
4571cf7baa | ||
|
|
bfae582fa3 | ||
|
|
575852e6b5 | ||
|
|
10b4291b54 | ||
|
|
bcf5121937 | ||
|
|
aeaeceb92c | ||
|
|
16f55d4275 | ||
|
|
b588ce920d | ||
|
|
1fb2c831e8 | ||
|
|
246f5d2e20 | ||
|
|
c707b7d128 | ||
|
|
ba41ca45fa | ||
|
|
7aacd6834a | ||
|
|
de14853237 | ||
|
|
9973298e2a | ||
|
|
65c37cc852 | ||
|
|
b6818fd4d2 | ||
|
|
fe7af80198 | ||
|
|
9aed6a06cf | ||
|
|
b3a0961c6c | ||
|
|
d9a9a47075 | ||
|
|
f9bb000ccf | ||
|
|
d6c0cff3bd | ||
|
|
95e171e19a | ||
|
|
d7b206cc93 | ||
|
|
06dfbdf7c8 | ||
|
|
3395a3305f | ||
|
|
5aaa3c09c1 | ||
|
|
b36a0c71d1 | ||
|
|
a402e0c5e6 | ||
|
|
660364d6a7 | ||
|
|
b170fe921e | ||
|
|
84372cef4a | ||
|
|
890178cf25 | ||
|
|
a284de73e6 | ||
|
|
45592ccdfd | ||
|
|
f4094c5eb3 | ||
|
|
dd2b933a0d | ||
|
|
c099b36af3 | ||
|
|
cc83b06cd1 | ||
|
|
5f30a69a9e | ||
|
|
faee41c303 | ||
|
|
e32cfed1d8 | ||
|
|
1e4b971f95 | ||
|
|
b0483cd47d | ||
|
|
40d2f38abe | ||
|
|
59516a8bb1 | ||
|
|
8aa4b7bf7f | ||
|
|
e639a3516d | ||
|
|
0897a09f49 | ||
|
|
6ac0b4ade8 | ||
|
|
34d7896b06 | ||
|
|
688c37ebf4 | ||
|
|
2c00e1ecd9 | ||
|
|
42f5b0a6b8 | ||
|
|
14bc4ed59f | ||
|
|
0b8a3bc3b9 | ||
|
|
c04caff55c | ||
|
|
7f23425e59 | ||
|
|
1aaa429081 | ||
|
|
04fbda46dd | ||
|
|
d821755b49 | ||
|
|
ae7dfeb5b6 | ||
|
|
b0406b9ead | ||
|
|
5bd9369a62 | ||
|
|
285ecaacd0 | ||
|
|
34878bc26a | ||
|
|
76217890c0 | ||
|
|
bf6fa6dd3d | ||
|
|
a9da2ec895 | ||
|
|
f3d3441d02 | ||
|
|
3292f70071 | ||
|
|
49b5dd56b5 | ||
|
|
32acb7e903 | ||
|
|
276b9f1839 | ||
|
|
7a77aabb4b | ||
|
|
aeb69c0f8c | ||
|
|
d9f3f322c5 | ||
|
|
33c4dd4c2d | ||
|
|
ca8349a897 | ||
|
|
cd62ee3f29 | ||
|
|
958b52596c | ||
|
|
c7bcd87f37 | ||
|
|
80852d1135 | ||
|
|
84326e2491 | ||
|
|
e3aec9bc81 | ||
|
|
21b45d2a5b | ||
|
|
842898df15 | ||
|
|
afb7f173cf | ||
|
|
14975ce5bc | ||
|
|
667e747ed1 | ||
|
|
1c51c8ab7d | ||
|
|
39e3fc69e5 | ||
|
|
b42fe05c51 | ||
|
|
ca1ae7cf9b | ||
|
|
2026942b05 | ||
|
|
aa525e4a63 | ||
|
|
3ed39ad20e | ||
|
|
cc2cee4af6 | ||
|
|
6c81752e46 | ||
|
|
a87eac4308 | ||
|
|
a2cd942a95 | ||
|
|
a840ff8f3f | ||
|
|
1c20249884 | ||
|
|
e53d77b501 | ||
|
|
09a59ce2d3 | ||
|
|
8b28f7d14e | ||
|
|
a81ec21762 | ||
|
|
9819b3619e | ||
|
|
311dc61803 | ||
|
|
d934328904 | ||
|
|
6ea20f3503 | ||
|
|
8b3ce85183 | ||
|
|
a059ca6915 | ||
|
|
1e05e30472 | ||
|
|
249e8f2277 | ||
|
|
aaf9ab68c6 | ||
|
|
3d6aee079e | ||
|
|
81d061e74e | ||
|
|
fb93a4a9e3 | ||
|
|
ceec607e7f | ||
|
|
fb082cf50f | ||
|
|
493b1e6d3c | ||
|
|
806c49a690 | ||
|
|
aa347b52ba | ||
|
|
4385eadc28 | ||
|
|
6b20fef52a | ||
|
|
c92740e8a9 | ||
|
|
d13d0bba51 | ||
|
|
d83202b938 | ||
|
|
cc049851d0 | ||
|
|
14a9652324 | ||
|
|
af44e9556d | ||
|
|
7e7eb0efc1 | ||
|
|
8dcb6f24b5 | ||
|
|
79fe6083eb | ||
|
|
dd1a9100c5 | ||
|
|
44998ca450 | ||
|
|
7a153b5c94 | ||
|
|
5a06f5c5fc | ||
|
|
dc7f39677f | ||
|
|
08f5c48fc8 | ||
|
|
9774949cc9 | ||
|
|
53d0f69dc3 | ||
|
|
6081f4947e | ||
|
|
6d18b52931 | ||
|
|
81ecaf945d | ||
|
|
55397f6347 | ||
|
|
2faffc52ee | ||
|
|
6c1f0055dc | ||
|
|
811716592c | ||
|
|
e2d2d63bcd | ||
|
|
dde7ec8e64 | ||
|
|
ce55a8cc4b | ||
|
|
30bfa911fc | ||
|
|
da3f842b8c | ||
|
|
130cbdd7af | ||
|
|
b099634ba1 | ||
|
|
c2afc6cd0a | ||
|
|
80b5470663 | ||
|
|
7411794fa1 | ||
|
|
55fe0d8adc | ||
|
|
b63dd9506e | ||
|
|
6f256e6380 | ||
|
|
2bd4346075 | ||
|
|
f23e5b17b6 | ||
|
|
56a358481e | ||
|
|
d5704cf2a3 | ||
|
|
550e8f32ac | ||
|
|
f90ce04a83 | ||
|
|
ccfb42e4ff | ||
|
|
25e96f82db | ||
|
|
253c327252 | ||
|
|
a75f8686ba | ||
|
|
1ef51e7939 | ||
|
|
746ed57c0e | ||
|
|
5132fcdb8b | ||
|
|
332986ba43 | ||
|
|
472b4fe48c | ||
|
|
fd2d3fcfd7 | ||
|
|
967ac65586 | ||
|
|
16b40cbede | ||
|
|
75890d7bdd | ||
|
|
e8f19b4c0d | ||
|
|
6bdb23449a | ||
|
|
f64cc237fc | ||
|
|
ef2111099a | ||
|
|
df50a6823f | ||
|
|
324020d5fe | ||
|
|
544691ab05 | ||
|
|
5236de5b03 | ||
|
|
91b370650a | ||
|
|
e062f2dfa8 | ||
|
|
c1a25756c2 | ||
|
|
d692994ea4 | ||
|
|
a3590dfa26 | ||
|
|
da9b7b0368 | ||
|
|
054fad5360 | ||
|
|
e0954f3b36 | ||
|
|
76fe7d4eba | ||
|
|
942d8412c4 | ||
|
|
2eaa199e6a | ||
|
|
83ce57302d | ||
|
|
de727f854a | ||
|
|
0627366b2f | ||
|
|
586e0df62d | ||
|
|
c0577ea87a | ||
|
|
d81e7dc00e | ||
|
|
9a5f224931 | ||
|
|
21d6ce2380 | ||
|
|
972f664b6b | ||
|
|
1dc4ad1efa | ||
|
|
a0a609e8af | ||
|
|
dc1f202eca | ||
|
|
ce5cd2202f | ||
|
|
2df5cb114d | ||
|
|
ef0304beff | ||
|
|
dd2ae64120 | ||
|
|
cde6bdfa77 | ||
|
|
f397b2264c | ||
|
|
768ff1a850 | ||
|
|
7735aad9d6 | ||
|
|
7bff9b6269 | ||
|
|
24f0bb4af5 | ||
|
|
c3f9d8e41b | ||
|
|
64b6f09b0d | ||
|
|
a73104b566 | ||
|
|
41907209bb | ||
|
|
d12feed623 | ||
|
|
9e0c3e7838 | ||
|
|
a9afb7cba3 | ||
|
|
44bd5e04dd | ||
|
|
9be1b2cb23 | ||
|
|
92800afd95 | ||
|
|
929cb12e7e | ||
|
|
de55ba218f | ||
|
|
71fb748d70 | ||
|
|
6e341aebab | ||
|
|
a1bf28b7f0 | ||
|
|
aa90e53312 | ||
|
|
ea5b5b1f64 | ||
|
|
2205aba3ed | ||
|
|
027f51763e | ||
|
|
1a298aad9c | ||
|
|
a342867d3f | ||
|
|
b5749c75d9 | ||
|
|
3ea6f01b4e | ||
|
|
37e53513b6 | ||
|
|
1829b55bb0 | ||
|
|
6d19fe1481 | ||
|
|
781ff713ba | ||
|
|
0b9e1e7b56 | ||
|
|
c80f739461 | ||
|
|
684001ac62 | ||
|
|
f47f42090d | ||
|
|
c03c255304 | ||
|
|
fc65b68f30 | ||
|
|
130458385e | ||
|
|
480438eee6 | ||
|
|
9dd4570b68 | ||
|
|
0280176ccd | ||
|
|
b4e1c1f51e | ||
|
|
1c7bb34ffd | ||
|
|
e0fa4cf874 | ||
|
|
b3be06667d | ||
|
|
9243f0c5e3 | ||
|
|
982604fbf2 | ||
|
|
250ee2ea7d | ||
|
|
95037d8d9d | ||
|
|
8a7f7f5004 | ||
|
|
12a23f01b4 | ||
|
|
3a88808983 | ||
|
|
3be6156774 | ||
|
|
cf4c17deaf | ||
|
|
3501478828 | ||
|
|
dcf0a6fbfd | ||
|
|
4b7a5b7bfa | ||
|
|
ec1cc29ecb | ||
|
|
f286a4fcd4 | ||
|
|
e2ae8af072 | ||
|
|
585e98fe2b | ||
|
|
c407ed070c | ||
|
|
6baaa18224 | ||
|
|
584591c3e3 | ||
|
|
43369cbe06 | ||
|
|
3bfffab201 | ||
|
|
0d1d9f3e9c | ||
|
|
3bc7bba262 | ||
|
|
9c82276760 | ||
|
|
3578046101 | ||
|
|
26efd6f151 | ||
|
|
1bf6c3faad | ||
|
|
9faf780740 | ||
|
|
3ab8cfbc14 | ||
|
|
3983bae160 | ||
|
|
7346ea85c0 | ||
|
|
eb7d7ce354 | ||
|
|
b1b57a3f28 | ||
|
|
82cf76a8f9 | ||
|
|
d76e548ec1 | ||
|
|
9f633bc125 | ||
|
|
3b38d2f507 | ||
|
|
a751a80a05 | ||
|
|
77e628e840 | ||
|
|
822d0e5520 | ||
|
|
0d5c7718c0 | ||
|
|
0538a4098d | ||
|
|
300816ffa1 | ||
|
|
804199d9b6 | ||
|
|
4c3512a45c | ||
|
|
bcaea74352 | ||
|
|
c9d1ee24ca | ||
|
|
9b18151104 | ||
|
|
34a7f0ca93 | ||
|
|
5b645f9d34 | ||
|
|
284d6b279b | ||
|
|
dce6395395 | ||
|
|
6322aa154b | ||
|
|
7f01d1d8c8 | ||
|
|
069a9745b0 | ||
|
|
78087617d1 | ||
|
|
d72ce4da64 | ||
|
|
a25d1530ef | ||
|
|
d6ecbbdf0a | ||
|
|
66a5bc4fad | ||
|
|
d703e712f7 | ||
|
|
f196d77f66 | ||
|
|
0d75b9fa96 | ||
|
|
5391ccdfe6 | ||
|
|
f68dbbd3da | ||
|
|
1a32b1f002 | ||
|
|
79bf9d25db | ||
|
|
1b491e50c9 | ||
|
|
7c4ce957c7 | ||
|
|
4081413876 | ||
|
|
5dd1a738f8 | ||
|
|
8a7c1d6a00 | ||
|
|
f93aba1d66 | ||
|
|
e3b261b0b7 | ||
|
|
ee2bcdec65 | ||
|
|
beaf50f5c6 | ||
|
|
581c54bebe | ||
|
|
30bcbc433a | ||
|
|
5f7cdbe0b8 | ||
|
|
ede161d296 | ||
|
|
b5f9d47c89 | ||
|
|
e4c40158c5 | ||
|
|
cda31fb755 | ||
|
|
dada11dc5f | ||
|
|
277fd2250a | ||
|
|
073a42cc95 | ||
|
|
7fc84c7019 | ||
|
|
c06d07a276 | ||
|
|
4c7da89219 | ||
|
|
932f35a7f0 | ||
|
|
756e171ad0 | ||
|
|
4777c1cd5b | ||
|
|
b1195c125f | ||
|
|
da31b96b55 | ||
|
|
86d6232236 | ||
|
|
061e814195 | ||
|
|
56bc57cf50 | ||
|
|
27cdbf7b94 | ||
|
|
4b85c5f52c | ||
|
|
cd0afb85c4 | ||
|
|
dfea1730dc | ||
|
|
b50ea730b1 | ||
|
|
bc21350298 | ||
|
|
10afd895c4 | ||
|
|
c54d8df504 | ||
|
|
acfabfff9c | ||
|
|
65693e9e15 | ||
|
|
bf10cf5f1a | ||
|
|
2385d396c3 | ||
|
|
3a3fadcece | ||
|
|
ce5c88006e | ||
|
|
d29d41322a | ||
|
|
46ac4a2f85 | ||
|
|
da3e04df8b | ||
|
|
967b45bc1a | ||
|
|
ddf3ca7ab3 | ||
|
|
4ba5b4b55d | ||
|
|
8ad056b207 | ||
|
|
e4eb5cb443 | ||
|
|
56427b8057 | ||
|
|
65c7f78e9f | ||
|
|
8166ebd91a | ||
|
|
ddc16d8642 | ||
|
|
c77add6d21 | ||
|
|
c6eafdfbaf | ||
|
|
112c7ea315 | ||
|
|
30ad0c5674 | ||
|
|
cdd8602e74 | ||
|
|
8c793e0704 | ||
|
|
683596f91e | ||
|
|
84430a4a8a | ||
|
|
9fae76107f | ||
|
|
bd7d47fcea | ||
|
|
2b9afa775e | ||
|
|
70aa4b9231 | ||
|
|
0aacab43ca | ||
|
|
dcbdfcc9d2 | ||
|
|
7819a1010c | ||
|
|
3bffd14b02 | ||
|
|
ab6e1abe9c | ||
|
|
707cd32b13 | ||
|
|
2f5182b2d2 | ||
|
|
780548b577 | ||
|
|
0a1260b03a | ||
|
|
3167d47882 | ||
|
|
c7a7cdf734 | ||
|
|
9f94b11d4c | ||
|
|
b175179e47 | ||
|
|
d8a921f6a6 | ||
|
|
6e2ce83d57 | ||
|
|
47c7dd590d | ||
|
|
9a7f7cb74f | ||
|
|
18f0247491 | ||
|
|
731c33dd97 | ||
|
|
1952a1c68d | ||
|
|
c2c3ee8e4a | ||
|
|
211a8b288a | ||
|
|
e166e29e87 | ||
|
|
235f686da9 | ||
|
|
9613d65756 | ||
|
|
044daf4fe2 | ||
|
|
d3c7567369 | ||
|
|
bcf30b29ad | ||
|
|
b4984d5e15 | ||
|
|
464e1fcfa5 | ||
|
|
dd2cd9312a | ||
|
|
e565a4bfc4 | ||
|
|
d5da6b0cef | ||
|
|
aa337f588c | ||
|
|
4b8244fbf8 | ||
|
|
4ac80b8570 | ||
|
|
5539251d82 | ||
|
|
5c778f2f15 | ||
|
|
fdcb876495 | ||
|
|
d9d6fbb085 | ||
|
|
436b3c7d0c | ||
|
|
7b56a7a3cb | ||
|
|
10e7821461 | ||
|
|
cf890e9d43 | ||
|
|
a808c06a10 | ||
|
|
db02021aba | ||
|
|
ed25abe05f | ||
|
|
08d2f902dd | ||
|
|
0393e87519 | ||
|
|
45570e4695 | ||
|
|
64b341cc10 | ||
|
|
828101dd51 | ||
|
|
7e22afbc7c | ||
|
|
30572e28c2 | ||
|
|
d45f89c95b | ||
|
|
ab0637c2c3 | ||
|
|
1bc05aef20 | ||
|
|
772c117e68 | ||
|
|
040d985908 | ||
|
|
15a7312273 | ||
|
|
3122ff2433 | ||
|
|
027857b261 | ||
|
|
94bb4031f3 | ||
|
|
07d609cbc2 | ||
|
|
68a04b9282 | ||
|
|
399e004884 | ||
|
|
79650f795f | ||
|
|
270d302834 | ||
|
|
32fdf8efd6 | ||
|
|
61e28cdb6f | ||
|
|
118b250473 | ||
|
|
6fd730c96b | ||
|
|
48142a01dd | ||
|
|
8b69468e5f | ||
|
|
bcfaaf7da6 | ||
|
|
a85612baf8 | ||
|
|
6d28560626 | ||
|
|
86fa1138be | ||
|
|
f452899fe2 | ||
|
|
3f5ebccbff | ||
|
|
ff79437d9b | ||
|
|
5452a8ee29 | ||
|
|
00b042a3eb | ||
|
|
a53946a8a1 | ||
|
|
b8ab9f1c0a | ||
|
|
3faa2ae78c | ||
|
|
0271e8e692 | ||
|
|
74cffcf51a | ||
|
|
6d07a28a29 | ||
|
|
6200630904 | ||
|
|
7d99cee3ef | ||
|
|
99ce820cc8 | ||
|
|
ab8de33c76 | ||
|
|
32bfd567ac | ||
|
|
57f047a05a | ||
|
|
5a11a8ef69 | ||
|
|
9b61076d42 | ||
|
|
46dcb0d890 | ||
|
|
ef6a8e4f32 | ||
|
|
b9172b982f | ||
|
|
1c6ab2d759 | ||
|
|
59d3955db1 | ||
|
|
db7109c43b | ||
|
|
fd696f1243 | ||
|
|
401c16559d | ||
|
|
fa6b3490e2 | ||
|
|
4e14e38bd5 | ||
|
|
f5755bcadf | ||
|
|
9ea1de432d | ||
|
|
468d94c920 | ||
|
|
26a95988da | ||
|
|
c9ee9b45c7 | ||
|
|
02f4e3b3ff | ||
|
|
f500dd627a | ||
|
|
865469f233 | ||
|
|
67ffc00d48 | ||
|
|
389ee3624c | ||
|
|
51b0b5c5ab | ||
|
|
10efca1a74 | ||
|
|
a9512d0994 | ||
|
|
fad58dbd08 | ||
|
|
0b01c8560d | ||
|
|
3bb93abb34 | ||
|
|
f81002df60 | ||
|
|
df752a15ce | ||
|
|
d27e1ab148 | ||
|
|
3eb45eba0e | ||
|
|
d9ebe531ed | ||
|
|
7ca6d4e8f7 | ||
|
|
2b7918bd6f | ||
|
|
8fe912d95c | ||
|
|
820ef6e9d8 | ||
|
|
0a65a2384c | ||
|
|
1bc036a12d | ||
|
|
b040bd6157 | ||
|
|
3ef312fb95 | ||
|
|
91753655b7 | ||
|
|
17a4bc10bc | ||
|
|
885e0c8b76 | ||
|
|
2a0e79bbfa | ||
|
|
f64ce52305 | ||
|
|
c715660cb8 | ||
|
|
93407cf7cf | ||
|
|
a8e8d1d06c | ||
|
|
eec67a675f | ||
|
|
56424eca5c | ||
|
|
6797c7f1b1 | ||
|
|
4bfdec1eb2 | ||
|
|
7b79c0f08f | ||
|
|
8c36179d35 | ||
|
|
490f142d73 | ||
|
|
26766c22eb | ||
|
|
e006f101c3 | ||
|
|
74cc722b96 | ||
|
|
6dd50da54e | ||
|
|
f85a3757cf | ||
|
|
95cbd026cc | ||
|
|
e1f249ce20 | ||
|
|
67f42b2f26 | ||
|
|
b86d2a2d4f | ||
|
|
d4145abd33 | ||
|
|
20d0db6cfb | ||
|
|
ca025c2b1d | ||
|
|
c3a774e414 | ||
|
|
0ef54caa28 | ||
|
|
1118f02689 | ||
|
|
1cdc29e260 | ||
|
|
339dd3dc6c | ||
|
|
27047d8f51 | ||
|
|
7c4b47652e | ||
|
|
8e2d4c6da5 | ||
|
|
e76cd252fe | ||
|
|
fbdacce3fe | ||
|
|
898dde8812 | ||
|
|
d7ae9b90a0 | ||
|
|
d2bc5d6f29 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
.*.swp
|
.*.swp
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
_trial_temp/
|
_trial_temp/
|
||||||
logs/
|
logs/
|
||||||
dbs/
|
dbs/
|
||||||
@@ -11,6 +12,14 @@ docs/build/
|
|||||||
|
|
||||||
cmdclient_config.json
|
cmdclient_config.json
|
||||||
homeserver*.db
|
homeserver*.db
|
||||||
|
homeserver*.log
|
||||||
|
homeserver*.pid
|
||||||
|
homeserver*.yaml
|
||||||
|
|
||||||
|
*.signing.key
|
||||||
|
*.tls.crt
|
||||||
|
*.tls.dh
|
||||||
|
*.tls.key
|
||||||
|
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov
|
htmlcov
|
||||||
@@ -18,9 +27,14 @@ htmlcov
|
|||||||
demo/*.db
|
demo/*.db
|
||||||
demo/*.log
|
demo/*.log
|
||||||
demo/*.pid
|
demo/*.pid
|
||||||
|
demo/etc
|
||||||
|
|
||||||
graph/*.svg
|
graph/*.svg
|
||||||
graph/*.png
|
graph/*.png
|
||||||
graph/*.dot
|
graph/*.dot
|
||||||
|
|
||||||
|
**/webclient/config.js
|
||||||
|
**/webclient/test/coverage/
|
||||||
|
**/webclient/test/environment-protractor.js
|
||||||
|
|
||||||
uploads
|
uploads
|
||||||
|
|||||||
274
CHANGES.rst
274
CHANGES.rst
@@ -1,3 +1,277 @@
|
|||||||
|
Changes in synapse 0.5.3c (2014-12-02)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
* Change the default value for the `content_addr` option to use the HTTP
|
||||||
|
listener, as by default the HTTPS listener will be using a self-signed
|
||||||
|
certificate.
|
||||||
|
|
||||||
|
Changes in synapse 0.5.3 (2014-11-27)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
* Fix bug that caused joining a remote room to fail if a single event was not
|
||||||
|
signed correctly.
|
||||||
|
* Fix bug which caused servers to continuously try and fetch events from other
|
||||||
|
servers.
|
||||||
|
|
||||||
|
Changes in synapse 0.5.2 (2014-11-26)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Fix major bug that caused rooms to disappear from peoples initial sync.
|
||||||
|
|
||||||
|
Changes in synapse 0.5.1 (2014-11-26)
|
||||||
|
=====================================
|
||||||
|
See UPGRADES.rst for specific instructions on how to upgrade.
|
||||||
|
|
||||||
|
* Fix bug where we served up an Event that did not match its signatures.
|
||||||
|
* Fix regression where we no longer correctly handled the case where a
|
||||||
|
homeserver receives an event for a room it doesn't recognise (but is in.)
|
||||||
|
|
||||||
|
Changes in synapse 0.5.0 (2014-11-19)
|
||||||
|
=====================================
|
||||||
|
This release includes changes to the federation protocol and client-server API
|
||||||
|
that is not backwards compatible.
|
||||||
|
|
||||||
|
This release also changes the internal database schemas and so requires servers to
|
||||||
|
drop their current history. See UPGRADES.rst for details.
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Add authentication and authorization to the federation protocol. Events are
|
||||||
|
now signed by their originating homeservers.
|
||||||
|
* Implement the new authorization model for rooms.
|
||||||
|
* Split out web client into a seperate repository: matrix-angular-sdk.
|
||||||
|
* Change the structure of PDUs.
|
||||||
|
* Fix bug where user could not join rooms via an alias containing 4-byte
|
||||||
|
UTF-8 characters.
|
||||||
|
* Merge concept of PDUs and Events internally.
|
||||||
|
* Improve logging by adding request ids to log lines.
|
||||||
|
* Implement a very basic room initial sync API.
|
||||||
|
* Implement the new invite/join federation APIs.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* The webclient has been moved to a seperate repository.
|
||||||
|
|
||||||
|
Changes in synapse 0.4.2 (2014-10-31)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Fix bugs where we did not notify users of correct presence updates.
|
||||||
|
* Fix bug where we did not handle sub second event stream timeouts.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add ability to click on messages to see JSON.
|
||||||
|
* Add ability to redact messages.
|
||||||
|
* Add ability to view and edit all room state JSON.
|
||||||
|
* Handle incoming redactions.
|
||||||
|
* Improve feedback on errors.
|
||||||
|
* Fix bugs in mobile CSS.
|
||||||
|
* Fix bugs with desktop notifications.
|
||||||
|
|
||||||
|
Changes in synapse 0.4.1 (2014-10-17)
|
||||||
|
=====================================
|
||||||
|
Webclient:
|
||||||
|
* Fix bug with display of timestamps.
|
||||||
|
|
||||||
|
Changes in synpase 0.4.0 (2014-10-17)
|
||||||
|
=====================================
|
||||||
|
This release includes changes to the federation protocol and client-server API
|
||||||
|
that is not backwards compatible.
|
||||||
|
|
||||||
|
The Matrix specification has been moved to a separate git repository:
|
||||||
|
http://github.com/matrix-org/matrix-doc
|
||||||
|
|
||||||
|
You will also need an updated syutil and config. See UPGRADES.rst.
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Sign federation transactions to assert strong identity over federation.
|
||||||
|
* Rename timestamp keys in PDUs and events from 'ts' and 'hsob_ts' to 'origin_server_ts'.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse 0.3.4 (2014-09-25)
|
||||||
|
=====================================
|
||||||
|
This version adds support for using a TURN server. See docs/turn-howto.rst on
|
||||||
|
how to set one up.
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Add support for redaction of messages.
|
||||||
|
* Fix bug where inviting a user on a remote home server could take up to
|
||||||
|
20-30s.
|
||||||
|
* Implement a get current room state API.
|
||||||
|
* Add support specifying and retrieving turn server configuration.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add button to send messages to users from the home page.
|
||||||
|
* Add support for using TURN for VoIP calls.
|
||||||
|
* Show display name change messages.
|
||||||
|
* Fix bug where the client didn't get the state of a newly joined room
|
||||||
|
until after it has been refreshed.
|
||||||
|
* Fix bugs with tab complete.
|
||||||
|
* Fix bug where holding down the down arrow caused chrome to chew 100% CPU.
|
||||||
|
* Fix bug where desktop notifications occasionally used "Undefined" as the
|
||||||
|
display name.
|
||||||
|
* Fix more places where we sometimes saw room IDs incorrectly.
|
||||||
|
* Fix bug which caused lag when entering text in the text box.
|
||||||
|
|
||||||
|
Changes in synapse 0.3.3 (2014-09-22)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Fix bug where you continued to get events for rooms you had left.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add support for video calls with basic UI.
|
||||||
|
* Fix bug where one to one chats were named after your display name rather
|
||||||
|
than the other person's.
|
||||||
|
* Fix bug which caused lag when typing in the textarea.
|
||||||
|
* Refuse to run on browsers we know won't work.
|
||||||
|
* Trigger pagination when joining new rooms.
|
||||||
|
* Fix bug where we sometimes didn't display invitations in recents.
|
||||||
|
* Automatically join room when accepting a VoIP call.
|
||||||
|
* Disable outgoing and reject incoming calls on browsers we don't support
|
||||||
|
VoIP in.
|
||||||
|
* Don't display desktop notifications for messages in the room you are
|
||||||
|
non-idle and speaking in.
|
||||||
|
|
||||||
|
Changes in synapse 0.3.2 (2014-09-18)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Fix bug where an empty "bing words" list in old accounts didn't send
|
||||||
|
notifications when it should have done.
|
||||||
|
|
||||||
|
Changes in synapse 0.3.1 (2014-09-18)
|
||||||
|
=====================================
|
||||||
|
This is a release to hotfix v0.3.0 to fix two regressions.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Fix a regression where we sometimes displayed duplicate events.
|
||||||
|
* Fix a regression where we didn't immediately remove rooms you were
|
||||||
|
banned in from the recents list.
|
||||||
|
|
||||||
|
Changes in synapse 0.3.0 (2014-09-18)
|
||||||
|
=====================================
|
||||||
|
See UPGRADE for information about changes to the client server API, including
|
||||||
|
breaking backwards compatibility with VoIP calls and registration API.
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* When a user changes their displayname or avatar the server will now update
|
||||||
|
all their join states to reflect this.
|
||||||
|
* The server now adds "age" key to events to indicate how old they are. This
|
||||||
|
is clock independent, so at no point does any server or webclient have to
|
||||||
|
assume their clock is in sync with everyone else.
|
||||||
|
* Fix bug where we didn't correctly pull in missing PDUs.
|
||||||
|
* Fix bug where prev_content key wasn't always returned.
|
||||||
|
* Add support for password resets.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Improve page content loading.
|
||||||
|
* Join/parts now trigger desktop notifications.
|
||||||
|
* Always show room aliases in the UI if one is present.
|
||||||
|
* No longer show user-count in the recents side panel.
|
||||||
|
* Add up & down arrow support to the text box for message sending to step
|
||||||
|
through your sent history.
|
||||||
|
* Don't display notifications for our own messages.
|
||||||
|
* Emotes are now formatted correctly in desktop notifications.
|
||||||
|
* The recents list now differentiates between public & private rooms.
|
||||||
|
* Fix bug where when switching between rooms the pagination flickered before
|
||||||
|
the view jumped to the bottom of the screen.
|
||||||
|
* Add bing word support.
|
||||||
|
|
||||||
|
Registration API:
|
||||||
|
* The registration API has been overhauled to function like the login API. In
|
||||||
|
practice, this means registration requests must now include the following:
|
||||||
|
'type':'m.login.password'. See UPGRADE for more information on this.
|
||||||
|
* The 'user_id' key has been renamed to 'user' to better match the login API.
|
||||||
|
* There is an additional login type: 'm.login.email.identity'.
|
||||||
|
* The command client and web client have been updated to reflect these changes.
|
||||||
|
|
||||||
|
Changes in synapse 0.2.3 (2014-09-12)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Fix bug where we stopped sending events to remote home servers if a
|
||||||
|
user from that home server left, even if there were some still in the
|
||||||
|
room.
|
||||||
|
* Fix bugs in the state conflict resolution where it was incorrectly
|
||||||
|
rejecting events.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Display room names and topics.
|
||||||
|
* Allow setting/editing of room names and topics.
|
||||||
|
* Display information about rooms on the main page.
|
||||||
|
* Handle ban and kick events in real time.
|
||||||
|
* VoIP UI and reliability improvements.
|
||||||
|
* Add glare support for VoIP.
|
||||||
|
* Improvements to initial startup speed.
|
||||||
|
* Don't display duplicate join events.
|
||||||
|
* Local echo of messages.
|
||||||
|
* Differentiate sending and sent of local echo.
|
||||||
|
* Various minor bug fixes.
|
||||||
|
|
||||||
|
Changes in synapse 0.2.2 (2014-09-06)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* When the server returns state events it now also includes the previous
|
||||||
|
content.
|
||||||
|
* Add support for inviting people when creating a new room.
|
||||||
|
* Make the homeserver inform the room via `m.room.aliases` when a new alias
|
||||||
|
is added for a room.
|
||||||
|
* Validate `m.room.power_level` events.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add support for captchas on registration.
|
||||||
|
* Handle `m.room.aliases` events.
|
||||||
|
* Asynchronously send messages and show a local echo.
|
||||||
|
* Inform the UI when a message failed to send.
|
||||||
|
* Only autoscroll on receiving a new message if the user was already at the
|
||||||
|
bottom of the screen.
|
||||||
|
* Add support for ban/kick reasons.
|
||||||
|
|
||||||
|
Changes in synapse 0.2.1 (2014-09-03)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Added support for signing up with a third party id.
|
||||||
|
* Add synctl scripts.
|
||||||
|
* Added rate limiting.
|
||||||
|
* Add option to change the external address the content repo uses.
|
||||||
|
* Presence bug fixes.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Added support for signing up with a third party id.
|
||||||
|
* Added support for banning and kicking users.
|
||||||
|
* Added support for displaying and setting ops.
|
||||||
|
* Added support for room names.
|
||||||
|
* Fix bugs with room membership event display.
|
||||||
|
|
||||||
|
Changes in synapse 0.2.0 (2014-09-02)
|
||||||
|
=====================================
|
||||||
|
This update changes many configuration options, updates the
|
||||||
|
database schema and mandates SSL for server-server connections.
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Require SSL for server-server connections.
|
||||||
|
* Add SSL listener for client-server connections.
|
||||||
|
* Add ability to use config files.
|
||||||
|
* Add support for kicking/banning and power levels.
|
||||||
|
* Allow setting of room names and topics on creation.
|
||||||
|
* Change presence to include last seen time of the user.
|
||||||
|
* Change url path prefix to /_matrix/...
|
||||||
|
* Bug fixes to presence.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Reskin the CSS for registration and login.
|
||||||
|
* Various improvements to rooms CSS.
|
||||||
|
* Support changes in client-server API.
|
||||||
|
* Bug fixes to VOIP UI.
|
||||||
|
* Various bug fixes to handling of changes to room member list.
|
||||||
|
|
||||||
|
Changes in synapse 0.1.2 (2014-08-29)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add basic call state UI for VoIP calls.
|
||||||
|
|
||||||
Changes in synapse 0.1.1 (2014-08-29)
|
Changes in synapse 0.1.1 (2014-08-29)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
recursive-include docs *
|
recursive-include docs *
|
||||||
recursive-include tests *.py
|
recursive-include tests *.py
|
||||||
recursive-include synapse/persistence/schema *.sql
|
recursive-include synapse/storage/schema *.sql
|
||||||
|
recursive-include syweb/webclient *
|
||||||
|
|||||||
271
README.rst
271
README.rst
@@ -1,65 +1,50 @@
|
|||||||
Quick Start
|
Introduction
|
||||||
===========
|
============
|
||||||
|
|
||||||
Matrix is an ambitious new ecosystem for open federated Instant Messaging and
|
Matrix is an ambitious new ecosystem for open federated Instant Messaging and
|
||||||
VoIP[1]. The basics you need to know to get up and running are:
|
VoIP. The basics you need to know to get up and running are:
|
||||||
|
|
||||||
- Chatrooms are distributed and do not exist on any single server. Rooms
|
- Everything in Matrix happens in a room. Rooms are distributed and do not
|
||||||
can be found using names like ``#matrix:matrix.org`` or
|
exist on any single server. Rooms can be located using convenience aliases
|
||||||
``#test:localhost:8080`` or they can be ephemeral.
|
like ``#matrix:matrix.org`` or ``#test:localhost:8008``.
|
||||||
|
|
||||||
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
||||||
you will normally refer to yourself and others using a 3PID: email
|
you will normally refer to yourself and others using a 3PID: email
|
||||||
address, phone number, etc rather than manipulating Matrix user IDs)
|
address, phone number, etc rather than manipulating Matrix user IDs)
|
||||||
|
|
||||||
The overall architecture is::
|
The overall architecture is::
|
||||||
|
|
||||||
client <----> homeserver <=================> homeserver <-----> client
|
client <----> homeserver <=====================> homeserver <----> client
|
||||||
e.g. matrix.org:8080 e.g. mydomain.net:8080
|
https://somewhere.org/_matrix https://elsewhere.net/_matrix
|
||||||
|
|
||||||
To get up and running:
|
``#matrix:matrix.org`` is the official support room for Matrix, and can be
|
||||||
|
accessed by the web client at http://matrix.org/alpha or via an IRC bridge at
|
||||||
- To simply play with an **existing** homeserver you can
|
irc://irc.freenode.net/matrix.
|
||||||
just go straight to http://matrix.org/alpha.
|
|
||||||
|
|
||||||
- To run your own **private** homeserver on localhost:8080, install synapse
|
|
||||||
with ``python setup.py develop --user`` and then run one with
|
|
||||||
``python synapse/app/homeserver.py`` - you will find a webclient running
|
|
||||||
at http://localhost:8080 (use a recent Chrome, Safari or Firefox for now,
|
|
||||||
please...)
|
|
||||||
|
|
||||||
- To make the homeserver **public** and let it exchange messages with
|
|
||||||
other homeservers and participate in the overall Matrix federation, open
|
|
||||||
up port 8080 and run ``python synapse/app/homeserver.py --host
|
|
||||||
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
|
|
||||||
say hi! :)
|
|
||||||
|
|
||||||
For more detailed setup instructions, please see further down this document.
|
Synapse is currently in rapid development, but as of version 0.5 we believe it
|
||||||
|
is sufficiently stable to be run as an internet-facing service for real usage!
|
||||||
|
|
||||||
[1] VoIP currently in development
|
|
||||||
|
|
||||||
|
|
||||||
About Matrix
|
About Matrix
|
||||||
============
|
============
|
||||||
|
|
||||||
Matrix specifies a set of pragmatic RESTful HTTP JSON APIs as an open standard,
|
Matrix specifies a set of pragmatic RESTful HTTP JSON APIs as an open standard,
|
||||||
which handle:
|
which handle:
|
||||||
|
|
||||||
- Creating and managing fully distributed chat rooms with no
|
- Creating and managing fully distributed chat rooms with no
|
||||||
single points of control or failure
|
single points of control or failure
|
||||||
- Eventually-consistent cryptographically secure[2] synchronisation of room
|
- Eventually-consistent cryptographically secure synchronisation of room
|
||||||
state across a global open network of federated servers and services
|
state across a global open network of federated servers and services
|
||||||
- Sending and receiving extensible messages in a room with (optional)
|
- Sending and receiving extensible messages in a room with (optional)
|
||||||
end-to-end encryption[3]
|
end-to-end encryption[1]
|
||||||
- Inviting, joining, leaving, kicking, banning room members
|
- Inviting, joining, leaving, kicking, banning room members
|
||||||
- Managing user accounts (registration, login, logout)
|
- Managing user accounts (registration, login, logout)
|
||||||
- Using 3rd Party IDs (3PIDs) such as email addresses, phone numbers,
|
- Using 3rd Party IDs (3PIDs) such as email addresses, phone numbers,
|
||||||
Facebook accounts to authenticate, identify and discover users on Matrix.
|
Facebook accounts to authenticate, identify and discover users on Matrix.
|
||||||
- Placing 1:1 VoIP and Video calls (in development)
|
- Placing 1:1 VoIP and Video calls
|
||||||
|
|
||||||
These APIs are intended to be implemented on a wide range of servers, services
|
These APIs are intended to be implemented on a wide range of servers, services
|
||||||
and clients, letting developers build messaging and VoIP functionality on top of
|
and clients, letting developers build messaging and VoIP functionality on top
|
||||||
the entirely open Matrix ecosystem rather than using closed or proprietary
|
of the entirely open Matrix ecosystem rather than using closed or proprietary
|
||||||
solutions. The hope is for Matrix to act as the building blocks for a new
|
solutions. The hope is for Matrix to act as the building blocks for a new
|
||||||
generation of fully open and interoperable messaging and VoIP apps for the
|
generation of fully open and interoperable messaging and VoIP apps for the
|
||||||
internet.
|
internet.
|
||||||
@@ -74,61 +59,129 @@ In Matrix, every user runs one or more Matrix clients, which connect through to
|
|||||||
a Matrix homeserver which stores all their personal chat history and user
|
a Matrix homeserver which stores all their personal chat history and user
|
||||||
account information - much as a mail client connects through to an IMAP/SMTP
|
account information - much as a mail client connects through to an IMAP/SMTP
|
||||||
server. Just like email, you can either run your own Matrix homeserver and
|
server. Just like email, you can either run your own Matrix homeserver and
|
||||||
control and own your own communications and history or use one hosted by someone
|
control and own your own communications and history or use one hosted by
|
||||||
else (e.g. matrix.org) - there is no single point of control or mandatory
|
someone else (e.g. matrix.org) - there is no single point of control or
|
||||||
service provider in Matrix, unlike WhatsApp, Facebook, Hangouts, etc.
|
mandatory service provider in Matrix, unlike WhatsApp, Facebook, Hangouts, etc.
|
||||||
|
|
||||||
Synapse ships with two basic demo Matrix clients: webclient (a basic group chat
|
Synapse ships with two basic demo Matrix clients: webclient (a basic group chat
|
||||||
web client demo implemented in AngularJS) and cmdclient (a basic Python
|
web client demo implemented in AngularJS) and cmdclient (a basic Python
|
||||||
commandline utility which lets you easily see what the JSON APIs are up to).
|
command line utility which lets you easily see what the JSON APIs are up to).
|
||||||
|
|
||||||
We'd like to invite you to take a look at the Matrix spec, try to run a
|
Meanwhile, iOS and Android SDKs and clients are currently in development and available from:
|
||||||
homeserver, and join the existing Matrix chatrooms already out there, experiment
|
|
||||||
with the APIs and the demo clients, and let us know your thoughts at
|
|
||||||
https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
|
|
||||||
|
|
||||||
Thanks for trying Matrix!
|
- https://github.com/matrix-org/matrix-ios-sdk
|
||||||
|
- https://github.com/matrix-org/matrix-android-sdk
|
||||||
|
|
||||||
[2] Cryptographic signing of messages isn't turned on yet
|
We'd like to invite you to join #matrix:matrix.org (via http://matrix.org/alpha), run a homeserver, take a look at the Matrix spec at
|
||||||
|
http://matrix.org/docs/spec, experiment with the APIs and the demo
|
||||||
|
clients, and report any bugs via http://matrix.org/jira.
|
||||||
|
|
||||||
[3] End-to-end encryption is currently in development
|
Thanks for using Matrix!
|
||||||
|
|
||||||
|
[1] End-to-end encryption is currently in development
|
||||||
|
|
||||||
Homeserver Installation
|
Homeserver Installation
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
First, the dependencies need to be installed. Start by installing
|
System requirements:
|
||||||
'python2.7-dev' and the various tools of the compiler toolchain.
|
- POSIX-compliant system (tested on Linux & OSX)
|
||||||
N.B. synapse requires python 2.x where x >= 7
|
- Python 2.7
|
||||||
|
|
||||||
Installing prerequisites on ubuntu::
|
Synapse is written in python but some of the libraries is uses are written in
|
||||||
|
C. So before we can install synapse itself we need a working C compiler and the
|
||||||
|
header files for python C extensions.
|
||||||
|
|
||||||
$ sudo apt-get install build-essential python2.7-dev libffi-dev
|
Installing prerequisites on Ubuntu or Debian::
|
||||||
|
|
||||||
Installing prerequisites on Mac OS X::
|
$ sudo apt-get install build-essential python2.7-dev libffi-dev \
|
||||||
|
python-pip python-setuptools sqlite3 \
|
||||||
|
libssl-dev
|
||||||
|
|
||||||
|
Installing prerequisites on Mac OS X::
|
||||||
|
|
||||||
$ xcode-select --install
|
$ xcode-select --install
|
||||||
|
|
||||||
|
To install the synapse homeserver run::
|
||||||
|
|
||||||
|
$ pip install --user --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
||||||
|
|
||||||
|
This installs synapse, along with the libraries it uses, into
|
||||||
|
``$HOME/.local/lib/`` on Linux or ``$HOME/Library/Python/2.7/lib/`` on OSX.
|
||||||
|
|
||||||
|
Troubleshooting Installation
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Synapse requires pip 1.7 or later, so if your OS provides too old a version and
|
||||||
|
you get errors about ``error: no such option: --process-dependency-links`` you
|
||||||
|
may need to manually upgrade it::
|
||||||
|
|
||||||
|
$ sudo pip install --upgrade pip
|
||||||
|
|
||||||
|
If pip crashes mid-installation for reason (e.g. lost terminal), pip may
|
||||||
|
refuse to run until you remove the temporary installation directory it
|
||||||
|
created. To reset the installation::
|
||||||
|
|
||||||
|
$ rm -rf /tmp/pip_install_matrix
|
||||||
|
|
||||||
|
pip seems to leak *lots* of memory during installation. For instance, a Linux
|
||||||
|
host with 512MB of RAM may run out of memory whilst installing Twisted. If this
|
||||||
|
happens, you will have to individually install the dependencies which are
|
||||||
|
failing, e.g.::
|
||||||
|
|
||||||
|
$ pip install --user twisted
|
||||||
|
|
||||||
|
Running Your Homeserver
|
||||||
|
=======================
|
||||||
|
|
||||||
|
To actually run your new homeserver, pick a working directory for Synapse to run
|
||||||
|
(e.g. ``~/.synapse``), and::
|
||||||
|
|
||||||
|
$ mkdir ~/.synapse
|
||||||
|
$ cd ~/.synapse
|
||||||
|
|
||||||
|
$ # on Linux
|
||||||
|
$ ~/.local/bin/synctl start
|
||||||
|
|
||||||
|
$ # on OSX
|
||||||
|
$ ~/Library/Python/2.7/bin/synctl start
|
||||||
|
|
||||||
|
Troubleshooting Running
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
If ``synctl`` fails with ``pkg_resources.DistributionNotFound`` errors you may
|
||||||
|
need a newer version of setuptools than that provided by your OS.::
|
||||||
|
|
||||||
|
$ sudo pip install setuptools --upgrade
|
||||||
|
|
||||||
|
If synapse fails with ``missing "sodium.h"`` crypto errors, you may need
|
||||||
|
to manually upgrade PyNaCL, as synapse uses NaCl (http://nacl.cr.yp.to/) for
|
||||||
|
encryption and digital signatures.
|
||||||
|
Unfortunately PyNACL currently has a few issues
|
||||||
|
(https://github.com/pyca/pynacl/issues/53) and
|
||||||
|
(https://github.com/pyca/pynacl/issues/79) that mean it may not install
|
||||||
|
correctly, causing all tests to fail with errors about missing "sodium.h". To
|
||||||
|
fix try re-installing from PyPI or directly from
|
||||||
|
(https://github.com/pyca/pynacl)::
|
||||||
|
|
||||||
|
$ # Install from PyPI
|
||||||
|
$ pip install --user --upgrade --force pynacl
|
||||||
|
$ # Install from github
|
||||||
|
$ pip install --user https://github.com/pyca/pynacl/tarball/master
|
||||||
|
|
||||||
|
|
||||||
|
Homeserver Development
|
||||||
|
======================
|
||||||
|
|
||||||
|
To check out a homeserver for development, clone the git repo into a working
|
||||||
|
directory of your choice::
|
||||||
|
|
||||||
|
$ git clone https://github.com/matrix-org/synapse.git
|
||||||
|
$ cd synapse
|
||||||
|
|
||||||
The homeserver has a number of external dependencies, that are easiest
|
The homeserver has a number of external dependencies, that are easiest
|
||||||
to install by making setup.py do so, in --user mode::
|
to install by making setup.py do so, in --user mode::
|
||||||
|
|
||||||
$ python setup.py develop --user
|
$ python setup.py develop --user
|
||||||
|
|
||||||
You'll need a version of setuptools new enough to know about git, so you
|
|
||||||
may need to also run:
|
|
||||||
|
|
||||||
$ sudo apt-get install python-pip
|
|
||||||
$ sudo pip install --upgrade setuptools
|
|
||||||
|
|
||||||
If you don't have access to github, then you may need to install ``syutil``
|
|
||||||
manually by checking it out and running ``python setup.py develop --user`` on it
|
|
||||||
too.
|
|
||||||
|
|
||||||
If you get errors about ``sodium.h`` being missing, you may also need to
|
|
||||||
manually install a newer PyNaCl via pip as setuptools installs an old one. Or
|
|
||||||
you can check PyNaCl out of git directly (https://github.com/pyca/pynacl) and
|
|
||||||
installing it. Installing PyNaCl using pip may also work (remember to remove any
|
|
||||||
other versions installed by setuputils in, for example, ~/.local/lib).
|
|
||||||
|
|
||||||
This will run a process of downloading and installing into your
|
This will run a process of downloading and installing into your
|
||||||
user's .local/lib directory all of the required dependencies that are
|
user's .local/lib directory all of the required dependencies that are
|
||||||
@@ -149,9 +202,9 @@ This should end with a 'PASSED' result::
|
|||||||
Upgrading an existing homeserver
|
Upgrading an existing homeserver
|
||||||
================================
|
================================
|
||||||
|
|
||||||
Before upgrading an existing homeserver to a new version, please refer to
|
IMPORTANT: Before upgrading an existing homeserver to a new version, please
|
||||||
UPGRADE.rst for any additional instructions.
|
refer to UPGRADE.rst for any additional instructions.
|
||||||
|
|
||||||
|
|
||||||
Setting up Federation
|
Setting up Federation
|
||||||
=====================
|
=====================
|
||||||
@@ -161,19 +214,25 @@ be publicly visible on the internet, and they will need to know its host name.
|
|||||||
You have two choices here, which will influence the form of your Matrix user
|
You have two choices here, which will influence the form of your Matrix user
|
||||||
IDs:
|
IDs:
|
||||||
|
|
||||||
1) Use the machine's own hostname as available on public DNS in the form of its
|
1) Use the machine's own hostname as available on public DNS in the form of
|
||||||
A or AAAA records. This is easier to set up initially, perhaps for testing,
|
its A or AAAA records. This is easier to set up initially, perhaps for
|
||||||
but lacks the flexibility of SRV.
|
testing, but lacks the flexibility of SRV.
|
||||||
|
|
||||||
2) Set up a SRV record for your domain name. This requires you create a SRV
|
2) Set up a SRV record for your domain name. This requires you create a SRV
|
||||||
record in DNS, but gives the flexibility to run the server on your own
|
record in DNS, but gives the flexibility to run the server on your own
|
||||||
choice of TCP port, on a machine that might not be the same name as the
|
choice of TCP port, on a machine that might not be the same name as the
|
||||||
domain name.
|
domain name.
|
||||||
|
|
||||||
For the first form, simply pass the required hostname (of the machine) as the
|
For the first form, simply pass the required hostname (of the machine) as the
|
||||||
--host parameter::
|
--server-name parameter::
|
||||||
|
|
||||||
$ python synapse/app/homeserver.py --host machine.my.domain.name
|
$ python -m synapse.app.homeserver \
|
||||||
|
--server-name machine.my.domain.name \
|
||||||
|
--config-path homeserver.config \
|
||||||
|
--generate-config
|
||||||
|
$ python -m synapse.app.homeserver --config-path homeserver.config
|
||||||
|
|
||||||
|
Alternatively, you can run ``synctl start`` to guide you through the process.
|
||||||
|
|
||||||
For the second form, first create your SRV record and publish it in DNS. This
|
For the second form, first create your SRV record and publish it in DNS. This
|
||||||
needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname
|
needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname
|
||||||
@@ -181,12 +240,20 @@ and port where the server is running. (At the current time synapse does not
|
|||||||
support clustering multiple servers into a single logical homeserver). The DNS
|
support clustering multiple servers into a single logical homeserver). The DNS
|
||||||
record would then look something like::
|
record would then look something like::
|
||||||
|
|
||||||
|
$ dig -t srv _matrix._tcp.machine.my.domaine.name
|
||||||
_matrix._tcp IN SRV 10 0 8448 machine.my.domain.name.
|
_matrix._tcp IN SRV 10 0 8448 machine.my.domain.name.
|
||||||
|
|
||||||
|
|
||||||
At this point, you should then run the homeserver with the hostname of this
|
At this point, you should then run the homeserver with the hostname of this
|
||||||
SRV record, as that is the name other machines will expect it to have::
|
SRV record, as that is the name other machines will expect it to have::
|
||||||
|
|
||||||
$ python synapse/app/homeserver.py --host my.domain.name --port 8448
|
$ python -m synapse.app.homeserver \
|
||||||
|
--server-name YOURDOMAIN \
|
||||||
|
--bind-port 8448 \
|
||||||
|
--config-path homeserver.config \
|
||||||
|
--generate-config
|
||||||
|
$ python -m synapse.app.homeserver --config-path homeserver.config
|
||||||
|
|
||||||
|
|
||||||
You may additionally want to pass one or more "-v" options, in order to
|
You may additionally want to pass one or more "-v" options, in order to
|
||||||
increase the verbosity of logging output; at least for initial testing.
|
increase the verbosity of logging output; at least for initial testing.
|
||||||
@@ -204,11 +271,13 @@ private federation (``localhost:8080``, ``localhost:8081`` and
|
|||||||
http://localhost:8080. Simply run::
|
http://localhost:8080. Simply run::
|
||||||
|
|
||||||
$ demo/start.sh
|
$ demo/start.sh
|
||||||
|
|
||||||
|
This is mainly useful just for development purposes.
|
||||||
|
|
||||||
Running The Demo Web Client
|
Running The Demo Web Client
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
The homeserver runs a web client by default at http://localhost:8080.
|
The homeserver runs a web client by default at https://localhost:8448/.
|
||||||
|
|
||||||
If this is the first time you have used the client from that browser (it uses
|
If this is the first time you have used the client from that browser (it uses
|
||||||
HTML5 local storage to remember its config), you will need to log in to your
|
HTML5 local storage to remember its config), you will need to log in to your
|
||||||
@@ -228,8 +297,8 @@ account. Your name will take the form of::
|
|||||||
|
|
||||||
Specify your desired localpart in the topmost box of the "Register for an
|
Specify your desired localpart in the topmost box of the "Register for an
|
||||||
account" form, and click the "Register" button. Hostnames can contain ports if
|
account" form, and click the "Register" button. Hostnames can contain ports if
|
||||||
required due to lack of SRV records (e.g. @matthew:localhost:8080 on an internal
|
required due to lack of SRV records (e.g. @matthew:localhost:8448 on an
|
||||||
synapse sandbox running on localhost)
|
internal synapse sandbox running on localhost)
|
||||||
|
|
||||||
|
|
||||||
Logging In To An Existing Account
|
Logging In To An Existing Account
|
||||||
@@ -244,9 +313,9 @@ Identity Servers
|
|||||||
|
|
||||||
The job of authenticating 3PIDs and tracking which 3PIDs are associated with a
|
The job of authenticating 3PIDs and tracking which 3PIDs are associated with a
|
||||||
given Matrix user is very security-sensitive, as there is obvious risk of spam
|
given Matrix user is very security-sensitive, as there is obvious risk of spam
|
||||||
if it is too easy to sign up for Matrix accounts or harvest 3PID data. Meanwhile
|
if it is too easy to sign up for Matrix accounts or harvest 3PID data.
|
||||||
the job of publishing the end-to-end encryption public keys for Matrix users is
|
Meanwhile the job of publishing the end-to-end encryption public keys for
|
||||||
also very security-sensitive for similar reasons.
|
Matrix users is also very security-sensitive for similar reasons.
|
||||||
|
|
||||||
Therefore the role of managing trusted identity in the Matrix ecosystem is
|
Therefore the role of managing trusted identity in the Matrix ecosystem is
|
||||||
farmed out to a cluster of known trusted ecosystem partners, who run 'Matrix
|
farmed out to a cluster of known trusted ecosystem partners, who run 'Matrix
|
||||||
@@ -255,19 +324,21 @@ track 3PID logins and publish end-user public keys.
|
|||||||
|
|
||||||
It's currently early days for identity servers as Matrix is not yet using 3PIDs
|
It's currently early days for identity servers as Matrix is not yet using 3PIDs
|
||||||
as the primary means of identity and E2E encryption is not complete. As such,
|
as the primary means of identity and E2E encryption is not complete. As such,
|
||||||
we're not yet running an identity server in public.
|
we are running a single identity server (http://matrix.org:8090) at the current
|
||||||
|
time.
|
||||||
|
|
||||||
|
|
||||||
Where's the spec?!
|
Where's the spec?!
|
||||||
==================
|
==================
|
||||||
|
|
||||||
For now, please go spelunking in the ``docs/`` directory to find out.
|
The source of the matrix spec lives at https://github.com/matrix-org/matrix-doc.
|
||||||
|
A recent HTML snapshot of this lives at http://matrix.org/docs/spec
|
||||||
|
|
||||||
|
|
||||||
Building Internal API Documentation
|
Building Internal API Documentation
|
||||||
===================================
|
===================================
|
||||||
|
|
||||||
Before building internal API documentation install spinx and
|
Before building internal API documentation install sphinx and
|
||||||
sphinxcontrib-napoleon::
|
sphinxcontrib-napoleon::
|
||||||
|
|
||||||
$ pip install sphinx
|
$ pip install sphinx
|
||||||
|
|||||||
115
UPGRADE.rst
115
UPGRADE.rst
@@ -1,3 +1,118 @@
|
|||||||
|
Upgrading to v0.5.1
|
||||||
|
===================
|
||||||
|
|
||||||
|
Depending on precisely when you installed v0.5.0 you may have ended up with
|
||||||
|
a stale release of the reference matrix webclient installed as a python module.
|
||||||
|
To uninstall it and ensure you are depending on the latest module, please run::
|
||||||
|
|
||||||
|
$ pip uninstall syweb
|
||||||
|
|
||||||
|
Upgrading to v0.5.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
The webclient has been split out into a seperate repository/pacakage in this
|
||||||
|
release. Before you restart your homeserver you will need to pull in the
|
||||||
|
webclient package by running::
|
||||||
|
|
||||||
|
python setup.py develop --user
|
||||||
|
|
||||||
|
This release completely changes the database schema and so requires upgrading
|
||||||
|
it before starting the new version of the homeserver.
|
||||||
|
|
||||||
|
The script "database-prepare-for-0.5.0.sh" should be used to upgrade the
|
||||||
|
database. This will save all user information, such as logins and profiles,
|
||||||
|
but will otherwise purge the database. This includes messages, which
|
||||||
|
rooms the home server was a member of and room alias mappings.
|
||||||
|
|
||||||
|
If you would like to keep your history, please take a copy of your database
|
||||||
|
file and ask for help in #matrix:matrix.org. The upgrade process is,
|
||||||
|
unfortunately, non trivial and requires human intervention to resolve any
|
||||||
|
resulting conflicts during the upgrade process.
|
||||||
|
|
||||||
|
Before running the command the homeserver should be first completely
|
||||||
|
shutdown. To run it, simply specify the location of the database, e.g.:
|
||||||
|
|
||||||
|
./database-prepare-for-0.5.0.sh "homeserver.db"
|
||||||
|
|
||||||
|
Once this has successfully completed it will be safe to restart the
|
||||||
|
homeserver. You may notice that the homeserver takes a few seconds longer to
|
||||||
|
restart than usual as it reinitializes the database.
|
||||||
|
|
||||||
|
On startup of the new version, users can either rejoin remote rooms using room
|
||||||
|
aliases or by being reinvited. Alternatively, if any other homeserver sends a
|
||||||
|
message to a room that the homeserver was previously in the local HS will
|
||||||
|
automatically rejoin the room.
|
||||||
|
|
||||||
|
Upgrading to v0.4.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
This release needs an updated syutil version. Run::
|
||||||
|
|
||||||
|
python setup.py develop
|
||||||
|
|
||||||
|
You will also need to upgrade your configuration as the signing key format has
|
||||||
|
changed. Run::
|
||||||
|
|
||||||
|
python -m synapse.app.homeserver --config-path <CONFIG> --generate-config
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading to v0.3.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
This registration API now closely matches the login API. This introduces a bit
|
||||||
|
more backwards and forwards between the HS and the client, but this improves
|
||||||
|
the overall flexibility of the API. You can now GET on /register to retrieve a list
|
||||||
|
of valid registration flows. Upon choosing one, they are submitted in the same
|
||||||
|
way as login, e.g::
|
||||||
|
|
||||||
|
{
|
||||||
|
type: m.login.password,
|
||||||
|
user: foo,
|
||||||
|
password: bar
|
||||||
|
}
|
||||||
|
|
||||||
|
The default HS supports 2 flows, with and without Identity Server email
|
||||||
|
authentication. Enabling captcha on the HS will add in an extra step to all
|
||||||
|
flows: ``m.login.recaptcha`` which must be completed before you can transition
|
||||||
|
to the next stage. There is a new login type: ``m.login.email.identity`` which
|
||||||
|
contains the ``threepidCreds`` key which were previously sent in the original
|
||||||
|
register request. For more information on this, see the specification.
|
||||||
|
|
||||||
|
Web Client
|
||||||
|
----------
|
||||||
|
|
||||||
|
The VoIP specification has changed between v0.2.0 and v0.3.0. Users should
|
||||||
|
refresh any browser tabs to get the latest web client code. Users on
|
||||||
|
v0.2.0 of the web client will not be able to call those on v0.3.0 and
|
||||||
|
vice versa.
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading to v0.2.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
The home server now requires setting up of SSL config before it can run. To
|
||||||
|
automatically generate default config use::
|
||||||
|
|
||||||
|
$ python synapse/app/homeserver.py \
|
||||||
|
--server-name machine.my.domain.name \
|
||||||
|
--bind-port 8448 \
|
||||||
|
--config-path homeserver.config \
|
||||||
|
--generate-config
|
||||||
|
|
||||||
|
This config can be edited if desired, for example to specify a different SSL
|
||||||
|
certificate to use. Once done you can run the home server using::
|
||||||
|
|
||||||
|
$ python synapse/app/homeserver.py --config-path homeserver.config
|
||||||
|
|
||||||
|
See the README.rst for more information.
|
||||||
|
|
||||||
|
Also note that some config options have been renamed, including:
|
||||||
|
|
||||||
|
- "host" to "server-name"
|
||||||
|
- "database" to "database-path"
|
||||||
|
- "port" to "bind-port" and "unsecure-port"
|
||||||
|
|
||||||
|
|
||||||
Upgrading to v0.0.1
|
Upgrading to v0.0.1
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -60,7 +60,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
"complete_usernames": "on",
|
"complete_usernames": "on",
|
||||||
"send_delivery_receipts": "on"
|
"send_delivery_receipts": "on"
|
||||||
}
|
}
|
||||||
self.path_prefix = "/matrix/client/api/v1"
|
self.path_prefix = "/_matrix/client/api/v1"
|
||||||
self.event_stream_token = "END"
|
self.event_stream_token = "END"
|
||||||
self.prompt = ">>> "
|
self.prompt = ">>> "
|
||||||
|
|
||||||
@@ -88,6 +88,8 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _domain(self):
|
def _domain(self):
|
||||||
|
if "user" not in self.config or not self.config["user"]:
|
||||||
|
return None
|
||||||
return self.config["user"].split(":")[1]
|
return self.config["user"].split(":")[1]
|
||||||
|
|
||||||
def do_config(self, line):
|
def do_config(self, line):
|
||||||
@@ -143,35 +145,50 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
<noupdate> : Do not automatically clobber config values.
|
<noupdate> : Do not automatically clobber config values.
|
||||||
"""
|
"""
|
||||||
args = self._parse(line, ["userid", "noupdate"])
|
args = self._parse(line, ["userid", "noupdate"])
|
||||||
path = "/register"
|
|
||||||
|
|
||||||
password = None
|
password = None
|
||||||
pwd = None
|
pwd = None
|
||||||
pwd2 = "_"
|
pwd2 = "_"
|
||||||
while pwd != pwd2:
|
while pwd != pwd2:
|
||||||
pwd = getpass.getpass("(Optional) Type a password for this user: ")
|
pwd = getpass.getpass("Type a password for this user: ")
|
||||||
if len(pwd) == 0:
|
|
||||||
print "Not using a password for this user."
|
|
||||||
break
|
|
||||||
pwd2 = getpass.getpass("Retype the password: ")
|
pwd2 = getpass.getpass("Retype the password: ")
|
||||||
if pwd != pwd2:
|
if pwd != pwd2 or len(pwd) == 0:
|
||||||
print "Password mismatch."
|
print "Password mismatch."
|
||||||
|
pwd = None
|
||||||
else:
|
else:
|
||||||
password = pwd
|
password = pwd
|
||||||
|
|
||||||
body = {}
|
body = {
|
||||||
|
"type": "m.login.password"
|
||||||
|
}
|
||||||
if "userid" in args:
|
if "userid" in args:
|
||||||
body["user_id"] = args["userid"]
|
body["user"] = args["userid"]
|
||||||
if password:
|
if password:
|
||||||
body["password"] = password
|
body["password"] = password
|
||||||
|
|
||||||
reactor.callFromThread(self._do_register, "POST", path, body,
|
reactor.callFromThread(self._do_register, body,
|
||||||
"noupdate" not in args)
|
"noupdate" not in args)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_register(self, method, path, data, update_config):
|
def _do_register(self, data, update_config):
|
||||||
url = self._url() + path
|
# check the registration flows
|
||||||
json_res = yield self.http_client.do_request(method, url, data=data)
|
url = self._url() + "/register"
|
||||||
|
json_res = yield self.http_client.do_request("GET", url)
|
||||||
|
print json.dumps(json_res, indent=4)
|
||||||
|
|
||||||
|
passwordFlow = None
|
||||||
|
for flow in json_res["flows"]:
|
||||||
|
if flow["type"] == "m.login.recaptcha" or ("stages" in flow and "m.login.recaptcha" in flow["stages"]):
|
||||||
|
print "Unable to register: Home server requires captcha."
|
||||||
|
return
|
||||||
|
if flow["type"] == "m.login.password" and "stages" not in flow:
|
||||||
|
passwordFlow = flow
|
||||||
|
break
|
||||||
|
|
||||||
|
if not passwordFlow:
|
||||||
|
return
|
||||||
|
|
||||||
|
json_res = yield self.http_client.do_request("POST", url, data=data)
|
||||||
print json.dumps(json_res, indent=4)
|
print json.dumps(json_res, indent=4)
|
||||||
if update_config and "user_id" in json_res:
|
if update_config and "user_id" in json_res:
|
||||||
self.config["user"] = json_res["user_id"]
|
self.config["user"] = json_res["user_id"]
|
||||||
@@ -191,10 +208,12 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
p = getpass.getpass("Enter your password: ")
|
p = getpass.getpass("Enter your password: ")
|
||||||
user = args["user_id"]
|
user = args["user_id"]
|
||||||
if self._is_on("complete_usernames") and not user.startswith("@"):
|
if self._is_on("complete_usernames") and not user.startswith("@"):
|
||||||
user = "@" + user + ":" + self._domain()
|
domain = self._domain()
|
||||||
|
if domain:
|
||||||
|
user = "@" + user + ":" + domain
|
||||||
|
|
||||||
reactor.callFromThread(self._do_login, user, p)
|
reactor.callFromThread(self._do_login, user, p)
|
||||||
print " got %s " % p
|
#print " got %s " % p
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print e
|
print e
|
||||||
|
|
||||||
@@ -252,7 +271,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_emailrequest(self, args):
|
def _do_emailrequest(self, args):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/requestToken"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
@@ -274,7 +293,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_emailvalidate(self, args):
|
def _do_emailvalidate(self, args):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/submitToken"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
@@ -294,7 +313,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_3pidbind(self, args):
|
def _do_3pidbind(self, args):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/3pid/bind"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/3pid/bind"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
@@ -312,7 +331,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
try:
|
try:
|
||||||
args = self._parse(line, ["roomname"], force_keys=True)
|
args = self._parse(line, ["roomname"], force_keys=True)
|
||||||
path = "/join/%s" % urllib.quote(args["roomname"])
|
path = "/join/%s" % urllib.quote(args["roomname"])
|
||||||
reactor.callFromThread(self._run_and_pprint, "PUT", path, {})
|
reactor.callFromThread(self._run_and_pprint, "POST", path, {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print e
|
print e
|
||||||
|
|
||||||
@@ -360,14 +379,14 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
def _do_invite(self, roomid, userstring):
|
def _do_invite(self, roomid, userstring):
|
||||||
if (not userstring.startswith('@') and
|
if (not userstring.startswith('@') and
|
||||||
self._is_on("complete_usernames")):
|
self._is_on("complete_usernames")):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/lookup"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/lookup"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring})
|
json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring})
|
||||||
|
|
||||||
mxid = None
|
mxid = None
|
||||||
|
|
||||||
if 'mxid' in json_res and 'signatures' in json_res:
|
if 'mxid' in json_res and 'signatures' in json_res:
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/pubkey/ed25519"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/pubkey/ed25519"
|
||||||
|
|
||||||
pubKey = None
|
pubKey = None
|
||||||
pubKeyObj = yield self.http_client.do_request("GET", url)
|
pubKeyObj = yield self.http_client.do_request("GET", url)
|
||||||
@@ -700,7 +719,7 @@ def main(server_url, identity_server_url, username, token, config_path):
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser("Starts a synapse client.")
|
parser = argparse.ArgumentParser("Starts a synapse client.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-s", "--server", dest="server", default="http://localhost:8080",
|
"-s", "--server", dest="server", default="http://localhost:8008",
|
||||||
help="The URL of the home server to talk to.")
|
help="The URL of the home server to talk to.")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-i", "--identity-server", dest="identityserver", default="http://localhost:8090",
|
"-i", "--identity-server", dest="identityserver", default="http://localhost:8090",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
21
database-prepare-for-0.5.0.sh
Executable file
21
database-prepare-for-0.5.0.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This is will prepare a synapse database for running with v0.5.0 of synapse.
|
||||||
|
# It will store all the user information, but will *delete* all messages and
|
||||||
|
# room data.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cp "$1" "$1.bak"
|
||||||
|
|
||||||
|
DUMP=$(sqlite3 "$1" << 'EOF'
|
||||||
|
.dump users
|
||||||
|
.dump access_tokens
|
||||||
|
.dump presence
|
||||||
|
.dump profiles
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
rm "$1"
|
||||||
|
|
||||||
|
sqlite3 "$1" <<< "$DUMP"
|
||||||
@@ -14,3 +14,4 @@ fi
|
|||||||
find "$DIR" -name "*.log" -delete
|
find "$DIR" -name "*.log" -delete
|
||||||
find "$DIR" -name "*.db" -delete
|
find "$DIR" -name "*.db" -delete
|
||||||
|
|
||||||
|
rm -rf $DIR/etc
|
||||||
|
|||||||
9
demo/demo.tls.dh
Normal file
9
demo/demo.tls.dh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
2048-bit DH parameters taken from rfc3526
|
||||||
|
-----BEGIN DH PARAMETERS-----
|
||||||
|
MIIBCAKCAQEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb
|
||||||
|
IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft
|
||||||
|
awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT
|
||||||
|
mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh
|
||||||
|
fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq
|
||||||
|
5RXSJhiY+gUQFXKOWoqsqmj//////////wIBAg==
|
||||||
|
-----END DH PARAMETERS-----
|
||||||
@@ -6,20 +6,38 @@ CWD=$(pwd)
|
|||||||
|
|
||||||
cd "$DIR/.."
|
cd "$DIR/.."
|
||||||
|
|
||||||
|
mkdir -p demo/etc
|
||||||
|
|
||||||
|
# Check the --no-rate-limit param
|
||||||
|
PARAMS=""
|
||||||
|
if [ $# -eq 1 ]; then
|
||||||
|
if [ $1 = "--no-rate-limit" ]; then
|
||||||
|
PARAMS="--rc-messages-per-second 1000 --rc-message-burst-count 1000"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
for port in 8080 8081 8082; do
|
for port in 8080 8081 8082; do
|
||||||
echo "Starting server on port $port... "
|
echo "Starting server on port $port... "
|
||||||
|
|
||||||
|
https_port=$((port + 400))
|
||||||
|
|
||||||
python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
-p "$port" \
|
--generate-config \
|
||||||
-H "localhost:$port" \
|
--config-path "demo/etc/$port.config" \
|
||||||
|
-p "$https_port" \
|
||||||
|
--unsecure-port "$port" \
|
||||||
|
-H "localhost:$https_port" \
|
||||||
-f "$DIR/$port.log" \
|
-f "$DIR/$port.log" \
|
||||||
-d "$DIR/$port.db" \
|
-d "$DIR/$port.db" \
|
||||||
-vv \
|
|
||||||
-D --pid-file "$DIR/$port.pid" \
|
-D --pid-file "$DIR/$port.pid" \
|
||||||
--manhole $((port + 1000))
|
--manhole $((port + 1000)) \
|
||||||
|
--tls-dh-params-path "demo/demo.tls.dh" \
|
||||||
|
$PARAMS $SYNAPSE_PARAMS
|
||||||
|
|
||||||
|
python -m synapse.app.homeserver \
|
||||||
|
--config-path "demo/etc/$port.config" \
|
||||||
|
-vv \
|
||||||
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Starting webclient on port 8000..."
|
|
||||||
python "demo/webserver.py" -p 8000 -P "$DIR/webserver.pid" "webclient"
|
|
||||||
|
|
||||||
cd "$CWD"
|
cd "$CWD"
|
||||||
|
|||||||
6
docs/README.rst
Normal file
6
docs/README.rst
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
All matrix-generic documentation now lives in its own project at
|
||||||
|
|
||||||
|
github.com/matrix-org/matrix-doc.git
|
||||||
|
|
||||||
|
Only Synapse implementation-specific documentation lives here now
|
||||||
|
(together with some older stuff will be shortly migrated over to matrix-doc)
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
.. WARNING::
|
||||||
|
These architecture notes are spectacularly old, and date back to when Synapse
|
||||||
|
was just federation code in isolation. This should be merged into the main
|
||||||
|
spec.
|
||||||
|
|
||||||
|
|
||||||
= Server to Server =
|
= Server to Server =
|
||||||
|
|
||||||
== Server to Server Stack ==
|
== Server to Server Stack ==
|
||||||
68
docs/architecture.rst
Normal file
68
docs/architecture.rst
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
Synapse Architecture
|
||||||
|
====================
|
||||||
|
|
||||||
|
As of the end of Oct 2014, Synapse's overall architecture looks like::
|
||||||
|
|
||||||
|
synapse
|
||||||
|
.-----------------------------------------------------.
|
||||||
|
| Notifier |
|
||||||
|
| ^ | |
|
||||||
|
| | | |
|
||||||
|
| .------------|------. |
|
||||||
|
| | handlers/ | | |
|
||||||
|
| | v | |
|
||||||
|
| | Event*Handler <--------> rest/* <=> Client
|
||||||
|
| | Rooms*Handler | |
|
||||||
|
HSes <=> federation/* <==> FederationHandler | |
|
||||||
|
| | | PresenceHandler | |
|
||||||
|
| | | TypingHandler | |
|
||||||
|
| | '-------------------' |
|
||||||
|
| | | | |
|
||||||
|
| | state/* | |
|
||||||
|
| | | | |
|
||||||
|
| | v v |
|
||||||
|
| `--------------> storage/* |
|
||||||
|
| | |
|
||||||
|
'--------------------------|--------------------------'
|
||||||
|
v
|
||||||
|
.----.
|
||||||
|
| DB |
|
||||||
|
'----'
|
||||||
|
|
||||||
|
* Handlers: business logic of synapse itself. Follows a set contract of BaseHandler:
|
||||||
|
|
||||||
|
- BaseHandler gives us onNewRoomEvent which: (TODO: flesh this out and make it less cryptic):
|
||||||
|
|
||||||
|
+ handle_state(event)
|
||||||
|
+ auth(event)
|
||||||
|
+ persist_event(event)
|
||||||
|
+ notify notifier or federation(event)
|
||||||
|
|
||||||
|
- PresenceHandler: use distributor to get EDUs out of Federation. Very
|
||||||
|
lightweight logic built on the distributor
|
||||||
|
- TypingHandler: use distributor to get EDUs out of Federation. Very
|
||||||
|
lightweight logic built on the distributor
|
||||||
|
- EventsHandler: handles the events stream...
|
||||||
|
- FederationHandler: - gets PDU from Federation Layer; turns into an event;
|
||||||
|
follows basehandler functionality.
|
||||||
|
- RoomsHandler: does all the room logic, including members - lots of classes in
|
||||||
|
RoomsHandler.
|
||||||
|
- ProfileHandler: talks to the storage to store/retrieve profile info.
|
||||||
|
|
||||||
|
* EventFactory: generates events of particular event types.
|
||||||
|
* Notifier: Backs the events handler
|
||||||
|
* REST: Interfaces handlers and events to the outside world via HTTP/JSON.
|
||||||
|
Converts events back and forth from JSON.
|
||||||
|
* Federation: holds the HTTP client & server to talk to other servers. Does
|
||||||
|
replication to make sure there's nothing missing in the graph. Handles
|
||||||
|
reliability. Handles txns.
|
||||||
|
* Distributor: generic event bus. used for presence & typing only currently.
|
||||||
|
Notifier could be implemented using Distributor - so far we are only using for
|
||||||
|
things which actually /require/ dynamic pluggability however as it can
|
||||||
|
obfuscate the actual flow of control.
|
||||||
|
* Auth: helper singleton to say whether a given event is allowed to do a given
|
||||||
|
thing (TODO: put this on the diagram)
|
||||||
|
* State: helper singleton: does state conflict resolution. You give it an event
|
||||||
|
and it tells you if it actually updates the state or not, and annotates the
|
||||||
|
event up properly and handles merge conflict resolution.
|
||||||
|
* Storage: abstracts the storage engine.
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
TODO(kegan): Tweak joinalias API keys/path? Event stream historical > live needs
|
|
||||||
a token (currently doesn't). im/sync responses include outdated event formats
|
|
||||||
(room membership change messages). Room config (specifically: message history,
|
|
||||||
public rooms). /register seems super simplistic compared to /login, maybe it
|
|
||||||
would be better if /register used the same technique as /login? /register should
|
|
||||||
be "user" not "user_id".
|
|
||||||
|
|
||||||
|
|
||||||
How to use the client-server API
|
|
||||||
================================
|
|
||||||
|
|
||||||
This guide focuses on how the client-server APIs *provided by the reference
|
|
||||||
home server* can be used. Since this is specific to a home server
|
|
||||||
implementation, there may be variations in relation to registering/logging in
|
|
||||||
which are not covered in extensive detail in this guide.
|
|
||||||
|
|
||||||
If you haven't already, get a home server up and running on
|
|
||||||
``http://localhost:8080``.
|
|
||||||
|
|
||||||
|
|
||||||
Accounts
|
|
||||||
========
|
|
||||||
Before you can send and receive messages, you must **register** for an account.
|
|
||||||
If you already have an account, you must **login** into it.
|
|
||||||
|
|
||||||
**Try out the fiddle: http://jsfiddle.net/jrf1h02d/**
|
|
||||||
|
|
||||||
Registration
|
|
||||||
------------
|
|
||||||
The aim of registration is to get a user ID and access token which you will need
|
|
||||||
when accessing other APIs::
|
|
||||||
|
|
||||||
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/register"
|
|
||||||
|
|
||||||
{
|
|
||||||
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",
|
|
||||||
"home_server": "localhost",
|
|
||||||
"user_id": "@example:localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
NB: If a ``user_id`` is not specified, one will be randomly generated for you.
|
|
||||||
If you do not specify a ``password``, you will be unable to login to the account
|
|
||||||
if you forget the ``access_token``.
|
|
||||||
|
|
||||||
Implementation note: The matrix specification does not enforce how users
|
|
||||||
register with a server. It just specifies the URL path and absolute minimum
|
|
||||||
keys. The reference home server uses a username/password to authenticate user,
|
|
||||||
but other home servers may use different methods.
|
|
||||||
|
|
||||||
Login
|
|
||||||
-----
|
|
||||||
The aim when logging in is to get an access token for your existing user ID::
|
|
||||||
|
|
||||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/login"
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.password"
|
|
||||||
}
|
|
||||||
|
|
||||||
curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/login"
|
|
||||||
|
|
||||||
{
|
|
||||||
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",
|
|
||||||
"home_server": "localhost",
|
|
||||||
"user_id": "@example:localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
Implementation note: Different home servers may implement different methods for
|
|
||||||
logging in to an existing account. In order to check that you know how to login
|
|
||||||
to this home server, you must perform a ``GET`` first and make sure you
|
|
||||||
recognise the login type. If you do not know how to login, you can
|
|
||||||
``GET /login/fallback`` which will return a basic webpage which you can use to
|
|
||||||
login. The reference home server implementation support username/password login,
|
|
||||||
but other home servers may support different login methods (e.g. OAuth2).
|
|
||||||
|
|
||||||
|
|
||||||
Communicating
|
|
||||||
=============
|
|
||||||
|
|
||||||
In order to communicate with another user, you must **create a room** with that
|
|
||||||
user and **send a message** to that room.
|
|
||||||
|
|
||||||
**Try out the fiddle: http://jsfiddle.net/jnwqcshc/**
|
|
||||||
|
|
||||||
Creating a room
|
|
||||||
---------------
|
|
||||||
If you want to send a message to someone, you have to be in a room with them. To
|
|
||||||
create a room::
|
|
||||||
|
|
||||||
curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
|
||||||
|
|
||||||
{
|
|
||||||
"room_alias": "#tutorial:localhost",
|
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
The "room alias" is a human-readable string which can be shared with other users
|
|
||||||
so they can join a room, rather than the room ID which is a randomly generated
|
|
||||||
string. You can have multiple room aliases per room.
|
|
||||||
|
|
||||||
TODO(kegan): How to add/remove aliases from an existing room.
|
|
||||||
|
|
||||||
|
|
||||||
Sending messages
|
|
||||||
----------------
|
|
||||||
You can now send messages to this room::
|
|
||||||
|
|
||||||
curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
|
||||||
|
|
||||||
NB: There are no limitations to the types of messages which can be exchanged.
|
|
||||||
The only requirement is that ``"msgtype"`` is specified.
|
|
||||||
|
|
||||||
NB: Depending on the room config, users who join the room may be able to see
|
|
||||||
message history from before they joined.
|
|
||||||
|
|
||||||
Users and rooms
|
|
||||||
===============
|
|
||||||
|
|
||||||
Each room can be configured to allow or disallow certain rules. In particular,
|
|
||||||
these rules may specify if you require an **invitation** from someone already in
|
|
||||||
the room in order to **join the room**. In addition, you may also be able to
|
|
||||||
join a room **via a room alias** if one was set up.
|
|
||||||
|
|
||||||
**Try out the fiddle: http://jsfiddle.net/og1xokcr/**
|
|
||||||
|
|
||||||
Inviting a user to a room
|
|
||||||
-------------------------
|
|
||||||
You can directly invite a user to a room like so::
|
|
||||||
|
|
||||||
curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
|
||||||
|
|
||||||
This informs ``@myfriend:localhost`` of the room ID
|
|
||||||
``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room.
|
|
||||||
|
|
||||||
Joining a room via an invite
|
|
||||||
----------------------------
|
|
||||||
If you receive an invite, you can join the room by changing the membership to
|
|
||||||
join::
|
|
||||||
|
|
||||||
curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
|
||||||
|
|
||||||
NB: Only the person invited (``@myfriend:localhost``) can change the membership
|
|
||||||
state to ``"join"``.
|
|
||||||
|
|
||||||
Joining a room via an alias
|
|
||||||
---------------------------
|
|
||||||
Alternatively, if you know the room alias for this room and the room config
|
|
||||||
allows it, you can directly join a room via the alias::
|
|
||||||
|
|
||||||
curl -XPUT -d '{}' "http://localhost:8080/matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
|
||||||
|
|
||||||
{
|
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
You will need to use the room ID when sending messages, not the room alias.
|
|
||||||
|
|
||||||
NB: If the room is configured to be an invite-only room, you will still require
|
|
||||||
an invite in order to join the room even though you know the room alias. As a
|
|
||||||
result, it is more common to see a room alias in relation to a public room,
|
|
||||||
which do not require invitations.
|
|
||||||
|
|
||||||
Getting events
|
|
||||||
==============
|
|
||||||
An event is some interesting piece of data that a client may be interested in.
|
|
||||||
It can be a message in a room, a room invite, etc. There are many different ways
|
|
||||||
of getting events, depending on what the client already knows.
|
|
||||||
|
|
||||||
**Try out the fiddle: http://jsfiddle.net/5uk4dqe2/**
|
|
||||||
|
|
||||||
Getting all state
|
|
||||||
-----------------
|
|
||||||
If the client doesn't know any information on the rooms the user is
|
|
||||||
invited/joined on, they can get all the user's state for all rooms::
|
|
||||||
|
|
||||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"membership": "join",
|
|
||||||
"messages": {
|
|
||||||
"chunk": [
|
|
||||||
{
|
|
||||||
"content": {
|
|
||||||
"body": "@example:localhost joined the room.",
|
|
||||||
"hsob_ts": 1408444664249,
|
|
||||||
"membership": "join",
|
|
||||||
"membership_source": "@example:localhost",
|
|
||||||
"membership_target": "@example:localhost",
|
|
||||||
"msgtype": "m.text"
|
|
||||||
},
|
|
||||||
"event_id": "lZjmmlrEvo",
|
|
||||||
"msg_id": "m1408444664249",
|
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
|
|
||||||
"type": "m.room.message",
|
|
||||||
"user_id": "_homeserver_"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": {
|
|
||||||
"body": "hello",
|
|
||||||
"hsob_ts": 1408445405672,
|
|
||||||
"msgtype": "m.text"
|
|
||||||
},
|
|
||||||
"event_id": "BiBJqamISg",
|
|
||||||
"msg_id": "msgid1",
|
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
|
|
||||||
"type": "m.room.message",
|
|
||||||
"user_id": "@example:localhost"
|
|
||||||
},
|
|
||||||
[...]
|
|
||||||
{
|
|
||||||
"content": {
|
|
||||||
"body": "@myfriend:localhost joined the room.",
|
|
||||||
"hsob_ts": 1408446501661,
|
|
||||||
"membership": "join",
|
|
||||||
"membership_source": "@myfriend:localhost",
|
|
||||||
"membership_target": "@myfriend:localhost",
|
|
||||||
"msgtype": "m.text"
|
|
||||||
},
|
|
||||||
"event_id": "IMmXbOzFAa",
|
|
||||||
"msg_id": "m1408446501661",
|
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
|
|
||||||
"type": "m.room.message",
|
|
||||||
"user_id": "_homeserver_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "20",
|
|
||||||
"start": "0"
|
|
||||||
},
|
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
This returns all the room IDs of rooms the user is invited/joined on, as well as
|
|
||||||
all of the messages and feedback for these rooms. This can be a LOT of data. You
|
|
||||||
may just want the most recent message for each room. This can be achieved by
|
|
||||||
applying pagination stream parameters to this request::
|
|
||||||
|
|
||||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"membership": "join",
|
|
||||||
"messages": {
|
|
||||||
"chunk": [
|
|
||||||
{
|
|
||||||
"content": {
|
|
||||||
"body": "@myfriend:localhost joined the room.",
|
|
||||||
"hsob_ts": 1408446501661,
|
|
||||||
"membership": "join",
|
|
||||||
"membership_source": "@myfriend:localhost",
|
|
||||||
"membership_target": "@myfriend:localhost",
|
|
||||||
"msgtype": "m.text"
|
|
||||||
},
|
|
||||||
"event_id": "IMmXbOzFAa",
|
|
||||||
"msg_id": "m1408446501661",
|
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost",
|
|
||||||
"type": "m.room.message",
|
|
||||||
"user_id": "_homeserver_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"end": "20",
|
|
||||||
"start": "21"
|
|
||||||
},
|
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Getting live state
|
|
||||||
------------------
|
|
||||||
Once you know which rooms the client has previously interacted with, you need to
|
|
||||||
listen for incoming events. This can be done like so::
|
|
||||||
|
|
||||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
|
|
||||||
|
|
||||||
{
|
|
||||||
"chunk": [],
|
|
||||||
"end": "215",
|
|
||||||
"start": "215"
|
|
||||||
}
|
|
||||||
|
|
||||||
This will block waiting for an incoming event, timing out after several seconds.
|
|
||||||
Even if there are no new events (as in the example above), there will be some
|
|
||||||
pagination stream response keys. The client should make subsequent requests
|
|
||||||
using the value of the ``"end"`` key (in this case ``215``) as the ``from``
|
|
||||||
query parameter. This value should be stored so when the client reopens your app
|
|
||||||
after a period of inactivity, you can resume from where you got up to in the
|
|
||||||
event stream. If it has been a long period of inactivity, there may be LOTS of
|
|
||||||
events waiting for the user. In this case, you may wish to get all state instead
|
|
||||||
and then resume getting live state from a newer end token.
|
|
||||||
|
|
||||||
NB: The timeout can be changed by adding a ``timeout`` query parameter, which is
|
|
||||||
in milliseconds. A timeout of 0 will not block.
|
|
||||||
|
|
||||||
|
|
||||||
Example application
|
|
||||||
-------------------
|
|
||||||
The following example demonstrates registration and login, live event streaming,
|
|
||||||
creating and joining rooms, sending messages, getting member lists and getting
|
|
||||||
historical messages for a room. This covers most functionality of a messaging
|
|
||||||
application.
|
|
||||||
|
|
||||||
**Try out the fiddle: http://jsfiddle.net/L8r3o1wr/**
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"apiVersion": "1.0.0",
|
|
||||||
"swaggerVersion": "1.2",
|
|
||||||
"apis": [
|
|
||||||
{
|
|
||||||
"path": "/login",
|
|
||||||
"description": "Login operations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/registration",
|
|
||||||
"description": "Registration operations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms",
|
|
||||||
"description": "Room operations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/profile",
|
|
||||||
"description": "Profile operations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/presence",
|
|
||||||
"description": "Presence operations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/events",
|
|
||||||
"description": "Event operations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/directory",
|
|
||||||
"description": "Directory operations"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"authorizations": {
|
|
||||||
"token": {
|
|
||||||
"scopes": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"info": {
|
|
||||||
"title": "Matrix Client-Server API Reference",
|
|
||||||
"description": "This contains the client-server API for the reference implementation of the home server",
|
|
||||||
"termsOfServiceUrl": "http://matrix.org",
|
|
||||||
"license": "Apache 2.0",
|
|
||||||
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
{
|
|
||||||
"apiVersion": "1.0.0",
|
|
||||||
"swaggerVersion": "1.2",
|
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
|
||||||
"resourcePath": "/directory",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
{
|
|
||||||
"path": "/directory/room/{roomAlias}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get the room ID corresponding to this room alias.",
|
|
||||||
"type": "DirectoryResponse",
|
|
||||||
"nickname": "get_room_id_for_alias",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomAlias",
|
|
||||||
"description": "The room alias.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Create a new mapping from room alias to room ID.",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "add_room_alias",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomAlias",
|
|
||||||
"description": "The room alias to set.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The room ID to set.",
|
|
||||||
"required": true,
|
|
||||||
"type": "RoomAliasRequest",
|
|
||||||
"paramType": "body"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"models": {
|
|
||||||
"DirectoryResponse": {
|
|
||||||
"id": "DirectoryResponse",
|
|
||||||
"properties": {
|
|
||||||
"room_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The fully-qualified room ID.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"servers": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "string"
|
|
||||||
},
|
|
||||||
"description": "A list of servers that know about this room.",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"RoomAliasRequest": {
|
|
||||||
"id": "RoomAliasRequest",
|
|
||||||
"properties": {
|
|
||||||
"room_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The room ID to map the alias to.",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
{
|
|
||||||
"apiVersion": "1.0.0",
|
|
||||||
"swaggerVersion": "1.2",
|
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
|
||||||
"resourcePath": "/events",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
{
|
|
||||||
"path": "/events",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Listen on the event stream",
|
|
||||||
"notes": "This can only be done by the logged in user. This will block until an event is received, or until the timeout is reached.",
|
|
||||||
"type": "PaginationChunk",
|
|
||||||
"nickname": "get_event_stream"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"description": "The token to stream from.",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "timeout",
|
|
||||||
"description": "The maximum time in milliseconds to wait for an event.",
|
|
||||||
"required": false,
|
|
||||||
"type": "integer",
|
|
||||||
"paramType": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "Bad pagination token."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/events/{eventId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get information about a single event.",
|
|
||||||
"notes": "Get information about a single event.",
|
|
||||||
"type": "Event",
|
|
||||||
"nickname": "get_event",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "eventId",
|
|
||||||
"description": "The event ID to get.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"message": "Event not found."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/initialSync",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get this user's current state.",
|
|
||||||
"notes": "Get this user's current state.",
|
|
||||||
"type": "InitialSyncResponse",
|
|
||||||
"nickname": "initial_sync",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "limit",
|
|
||||||
"description": "The maximum number of messages to return for each room.",
|
|
||||||
"type": "integer",
|
|
||||||
"paramType": "query",
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/publicRooms",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get a list of publicly visible rooms.",
|
|
||||||
"type": "PublicRoomsPaginationChunk",
|
|
||||||
"nickname": "get_public_room_list"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"models": {
|
|
||||||
"PaginationChunk": {
|
|
||||||
"id": "PaginationChunk",
|
|
||||||
"properties": {
|
|
||||||
"start": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"chunk": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "An array of events.",
|
|
||||||
"required": true,
|
|
||||||
"items": {
|
|
||||||
"$ref": "Event"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Event": {
|
|
||||||
"id": "Event",
|
|
||||||
"properties": {
|
|
||||||
"event_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "An ID which uniquely identifies this event.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"room_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The room in which this event occurred.",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"PublicRoomInfo": {
|
|
||||||
"id": "PublicRoomInfo",
|
|
||||||
"properties": {
|
|
||||||
"aliases": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of room aliases for this room.",
|
|
||||||
"items": {
|
|
||||||
"$ref": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The name of the room, as given by the m.room.name state event."
|
|
||||||
},
|
|
||||||
"room_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The room ID for this public room.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"topic": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The topic of this room, as given by the m.room.topic state event."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"PublicRoomsPaginationChunk": {
|
|
||||||
"id": "PublicRoomsPaginationChunk",
|
|
||||||
"properties": {
|
|
||||||
"start": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"chunk": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of public room data.",
|
|
||||||
"required": true,
|
|
||||||
"items": {
|
|
||||||
"$ref": "PublicRoomInfo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"InitialSyncResponse": {
|
|
||||||
"id": "InitialSyncResponse",
|
|
||||||
"properties": {
|
|
||||||
"end": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A streaming token which can be used with /events to continue from this snapshot of data.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"presence": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of presence events.",
|
|
||||||
"items": {
|
|
||||||
"$ref": "Event"
|
|
||||||
},
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of initial sync room data.",
|
|
||||||
"required": false,
|
|
||||||
"items": {
|
|
||||||
"$ref": "InitialSyncRoomData"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"InitialSyncRoomData": {
|
|
||||||
"id": "InitialSyncRoomData",
|
|
||||||
"properties": {
|
|
||||||
"membership": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "This user's membership state in this room.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"room_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The ID of this room.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"type": "PaginationChunk",
|
|
||||||
"description": "The most recent messages for this room, governed by the limit parameter.",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of state events representing the current state of the room.",
|
|
||||||
"required": false,
|
|
||||||
"items": {
|
|
||||||
"$ref": "Event"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
{
|
|
||||||
"apiVersion": "1.0.0",
|
|
||||||
"apis": [
|
|
||||||
{
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"nickname": "get_login_info",
|
|
||||||
"notes": "All login stages MUST be mentioned if there is >1 login type.",
|
|
||||||
"summary": "Get the login mechanism to use when logging in.",
|
|
||||||
"type": "LoginInfo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"nickname": "submit_login",
|
|
||||||
"notes": "If this is part of a multi-stage login, there MUST be a 'session' key.",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "A login submission",
|
|
||||||
"name": "body",
|
|
||||||
"paramType": "body",
|
|
||||||
"required": true,
|
|
||||||
"type": "LoginSubmission"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "Bad login type"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "Missing JSON keys"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "Submit a login action.",
|
|
||||||
"type": "LoginResult"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"path": "/login"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"models": {
|
|
||||||
"LoginInfo": {
|
|
||||||
"id": "LoginInfo",
|
|
||||||
"properties": {
|
|
||||||
"stages": {
|
|
||||||
"description": "Multi-stage login only: An array of all the login types required to login.",
|
|
||||||
"format": "string",
|
|
||||||
"type": "array"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"description": "The login type that must be used when logging in.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"LoginResult": {
|
|
||||||
"id": "LoginResult",
|
|
||||||
"properties": {
|
|
||||||
"access_token": {
|
|
||||||
"description": "The access token for this user's login if this is the final stage of the login process.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"next": {
|
|
||||||
"description": "Multi-stage login only: The next login type to submit.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"session": {
|
|
||||||
"description": "Multi-stage login only: The session token to send when submitting the next login type.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"LoginSubmission": {
|
|
||||||
"id": "LoginSubmission",
|
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"description": "The type of login being submitted.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"session": {
|
|
||||||
"description": "Multi-stage login only: The session token from an earlier login stage.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"_login_type_defined_keys_": {
|
|
||||||
"description": "Keys as defined by the specified login type, e.g. \"user\", \"password\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"resourcePath": "/login",
|
|
||||||
"swaggerVersion": "1.2"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
{
|
|
||||||
"apiVersion": "1.0.0",
|
|
||||||
"swaggerVersion": "1.2",
|
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
|
||||||
"resourcePath": "/presence",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
{
|
|
||||||
"path": "/presence/{userId}/status",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Update this user's presence state.",
|
|
||||||
"notes": "This can only be done by the logged in user.",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "update_presence",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The new presence state",
|
|
||||||
"required": true,
|
|
||||||
"type": "PresenceUpdate",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose presence to set.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get this user's presence state.",
|
|
||||||
"notes": "Get this user's presence state.",
|
|
||||||
"type": "PresenceUpdate",
|
|
||||||
"nickname": "get_presence",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose presence to get.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/presence/list/{userId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Retrieve a list of presences for all of this user's friends.",
|
|
||||||
"notes": "",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "Presence"
|
|
||||||
},
|
|
||||||
"nickname": "get_presence_list",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose presence list to get.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"summary": "Add or remove users from this presence list.",
|
|
||||||
"notes": "Add or remove users from this presence list.",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "modify_presence_list",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose presence list is being modified.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The modifications to make to this presence list.",
|
|
||||||
"required": true,
|
|
||||||
"type": "PresenceListModifications",
|
|
||||||
"paramType": "body"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"models": {
|
|
||||||
"PresenceUpdate": {
|
|
||||||
"id": "PresenceUpdate",
|
|
||||||
"properties": {
|
|
||||||
"state": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Enum: The presence state.",
|
|
||||||
"enum": [
|
|
||||||
"offline",
|
|
||||||
"unavailable",
|
|
||||||
"online",
|
|
||||||
"free_for_chat"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"status_msg": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The user-defined message associated with this presence state."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"subTypes": [
|
|
||||||
"Presence"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Presence": {
|
|
||||||
"id": "Presence",
|
|
||||||
"properties": {
|
|
||||||
"mtime_age": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"description": "The last time this user's presence state changed, in milliseconds."
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The fully qualified user ID"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"PresenceListModifications": {
|
|
||||||
"id": "PresenceListModifications",
|
|
||||||
"properties": {
|
|
||||||
"invite": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of user IDs to add to the list.",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A fully qualified user ID."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"drop": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of user IDs to remove from the list.",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A fully qualified user ID."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
{
|
|
||||||
"apiVersion": "1.0.0",
|
|
||||||
"swaggerVersion": "1.2",
|
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
|
||||||
"resourcePath": "/profile",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"apis": [
|
|
||||||
{
|
|
||||||
"path": "/profile/{userId}/displayname",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Set a display name.",
|
|
||||||
"notes": "This can only be done by the logged in user.",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "set_display_name",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The new display name for this user.",
|
|
||||||
"required": true,
|
|
||||||
"type": "DisplayName",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose display name to set.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get a display name.",
|
|
||||||
"notes": "This can be done by anyone.",
|
|
||||||
"type": "DisplayName",
|
|
||||||
"nickname": "get_display_name",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose display name to get.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/profile/{userId}/avatar_url",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Set an avatar URL.",
|
|
||||||
"notes": "This can only be done by the logged in user.",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "set_avatar_url",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The new avatar url for this user.",
|
|
||||||
"required": true,
|
|
||||||
"type": "AvatarUrl",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose avatar url to set.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get an avatar url.",
|
|
||||||
"notes": "This can be done by anyone.",
|
|
||||||
"type": "AvatarUrl",
|
|
||||||
"nickname": "get_avatar_url",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose avatar url to get.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"models": {
|
|
||||||
"DisplayName": {
|
|
||||||
"id": "DisplayName",
|
|
||||||
"properties": {
|
|
||||||
"displayname": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The textual display name"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AvatarUrl": {
|
|
||||||
"id": "AvatarUrl",
|
|
||||||
"properties": {
|
|
||||||
"avatar_url": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A url to an image representing an avatar."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
"apiVersion": "1.0.0",
|
|
||||||
"apis": [
|
|
||||||
{
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"nickname": "register",
|
|
||||||
"notes": "Volatile: This API is likely to change.",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "A registration request",
|
|
||||||
"name": "body",
|
|
||||||
"paramType": "body",
|
|
||||||
"required": true,
|
|
||||||
"type": "RegistrationRequest"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "No JSON object."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "User ID must only contain characters which do not require url encoding."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "User ID already taken."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "Register with the home server.",
|
|
||||||
"type": "RegistrationResponse"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"path": "/register"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"models": {
|
|
||||||
"RegistrationResponse": {
|
|
||||||
"id": "RegistrationResponse",
|
|
||||||
"properties": {
|
|
||||||
"access_token": {
|
|
||||||
"description": "The access token for this user.",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"description": "The fully-qualified user ID.",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"RegistrationRequest": {
|
|
||||||
"id": "RegistrationRequest",
|
|
||||||
"properties": {
|
|
||||||
"user_id": {
|
|
||||||
"description": "The desired user ID. If not specified, a random user ID will be allocated.",
|
|
||||||
"type": "string",
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"resourcePath": "/register",
|
|
||||||
"swaggerVersion": "1.2"
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,863 +0,0 @@
|
|||||||
{
|
|
||||||
"apiVersion": "1.0.0",
|
|
||||||
"swaggerVersion": "1.2",
|
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
|
||||||
"resourcePath": "/rooms",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"authorizations": {
|
|
||||||
"token": []
|
|
||||||
},
|
|
||||||
"apis": [
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/send/{eventType}/{txnId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Send a generic non-state event to this room.",
|
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/{eventType}",
|
|
||||||
"type": "EventId",
|
|
||||||
"nickname": "send_non_state_event",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The event contents",
|
|
||||||
"required": true,
|
|
||||||
"type": "EventContent",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to send the message in.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "eventType",
|
|
||||||
"description": "The type of event to send.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "txnId",
|
|
||||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/state/{eventType}/{stateKey}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Send a generic state event to this room.",
|
|
||||||
"notes": "The state key can be omitted, such that you can PUT to /rooms/{roomId}/state/{eventType}. The state key defaults to a 0 length string in this case.",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "send_state_event",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The event contents",
|
|
||||||
"required": true,
|
|
||||||
"type": "EventContent",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to send the message in.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "eventType",
|
|
||||||
"description": "The type of event to send.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "stateKey",
|
|
||||||
"description": "An identifier used to specify clobbering semantics. State events with the same (roomId, eventType, stateKey) will be replaced.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/send/m.room.message/{txnId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Send a message in this room.",
|
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message",
|
|
||||||
"type": "EventId",
|
|
||||||
"nickname": "send_message",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The message contents",
|
|
||||||
"required": true,
|
|
||||||
"type": "Message",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to send the message in.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "txnId",
|
|
||||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/state/m.room.topic",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Set the topic for this room.",
|
|
||||||
"notes": "Set the topic for this room.",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "set_topic",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The topic contents",
|
|
||||||
"required": true,
|
|
||||||
"type": "Topic",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to set the topic in.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get the topic for this room.",
|
|
||||||
"notes": "Get the topic for this room.",
|
|
||||||
"type": "Topic",
|
|
||||||
"nickname": "get_topic",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to get topic in.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"message": "Topic not found."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Send feedback to a message.",
|
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback",
|
|
||||||
"type": "EventId",
|
|
||||||
"nickname": "send_feedback",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The feedback contents",
|
|
||||||
"required": true,
|
|
||||||
"type": "Feedback",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to send the feedback in.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "txnId",
|
|
||||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "Bad feedback type."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/invite/{txnId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Invite a user to this room.",
|
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/invite",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "invite",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room which has this user.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "txnId",
|
|
||||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The user to invite.",
|
|
||||||
"required": true,
|
|
||||||
"type": "InviteRequest",
|
|
||||||
"paramType": "body"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/join/{txnId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Join this room.",
|
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/join",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "join_room",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to join.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "txnId",
|
|
||||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/leave/{txnId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Leave this room.",
|
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/leave",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "leave",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to leave.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "txnId",
|
|
||||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/state/m.room.member/{userId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Change the membership state for a user in a room.",
|
|
||||||
"notes": "Change the membership state for a user in a room.",
|
|
||||||
"type": "void",
|
|
||||||
"nickname": "set_membership",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The new membership state",
|
|
||||||
"required": true,
|
|
||||||
"type": "Member",
|
|
||||||
"paramType": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose membership is being changed.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room which has this user.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "No membership key."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "Bad membership value."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 403,
|
|
||||||
"message": "When inviting: You are not in the room."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 403,
|
|
||||||
"message": "When inviting: <target> is already in the room."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 403,
|
|
||||||
"message": "When joining: Cannot force another user to join."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 403,
|
|
||||||
"message": "When joining: You are not invited to this room."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get the membership state of a user in a room.",
|
|
||||||
"notes": "Get the membership state of a user in a room.",
|
|
||||||
"type": "Member",
|
|
||||||
"nickname": "get_membership",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"description": "The user whose membership state you want to get.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room which has this user.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 404,
|
|
||||||
"message": "Member not found."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/join/{roomAliasOrId}",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "PUT",
|
|
||||||
"summary": "Join a room via a room alias or room ID.",
|
|
||||||
"notes": "Join a room via a room alias or room ID.",
|
|
||||||
"type": "RoomInfo",
|
|
||||||
"nickname": "join",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomAliasOrId",
|
|
||||||
"description": "The room alias or room ID to join.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "Bad room alias."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/createRoom",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "POST",
|
|
||||||
"summary": "Create a room.",
|
|
||||||
"notes": "Create a room.",
|
|
||||||
"type": "RoomInfo",
|
|
||||||
"nickname": "create_room",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "body",
|
|
||||||
"description": "The desired configuration for the room.",
|
|
||||||
"required": true,
|
|
||||||
"type": "RoomConfig",
|
|
||||||
"paramType": "body"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responseMessages": [
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "Body must be JSON."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "Room alias already taken."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/messages",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get a list of messages for this room.",
|
|
||||||
"notes": "Get a list of messages for this room.",
|
|
||||||
"type": "MessagePaginationChunk",
|
|
||||||
"nickname": "get_messages",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to get messages in.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"description": "The token to start getting results from.",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"description": "The token to stop getting results at.",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "limit",
|
|
||||||
"description": "The maximum number of messages to return.",
|
|
||||||
"required": false,
|
|
||||||
"type": "integer",
|
|
||||||
"paramType": "query"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/members",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get a list of members for this room.",
|
|
||||||
"notes": "Get a list of members for this room.",
|
|
||||||
"type": "MemberPaginationChunk",
|
|
||||||
"nickname": "get_members",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to get a list of members from.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "from",
|
|
||||||
"description": "The token to start getting results from.",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "to",
|
|
||||||
"description": "The token to stop getting results at.",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "limit",
|
|
||||||
"description": "The maximum number of members to return.",
|
|
||||||
"required": false,
|
|
||||||
"type": "integer",
|
|
||||||
"paramType": "query"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/state",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get a list of all the current state events for this room.",
|
|
||||||
"notes": "Get a list of all the current state events for this room.",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "Event"
|
|
||||||
},
|
|
||||||
"nickname": "get_state_events",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to get a list of current state events from.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "/rooms/{roomId}/initialSync",
|
|
||||||
"operations": [
|
|
||||||
{
|
|
||||||
"method": "GET",
|
|
||||||
"summary": "Get all the current information for this room, including messages and state events.",
|
|
||||||
"notes": "Get all the current information for this room, including messages and state events.",
|
|
||||||
"type": "InitialSyncRoomData",
|
|
||||||
"nickname": "get_room_sync_data",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "roomId",
|
|
||||||
"description": "The room to get information for.",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"paramType": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"models": {
|
|
||||||
"Topic": {
|
|
||||||
"id": "Topic",
|
|
||||||
"properties": {
|
|
||||||
"topic": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The topic text"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Message": {
|
|
||||||
"id": "Message",
|
|
||||||
"properties": {
|
|
||||||
"msgtype": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The type of message being sent, e.g. \"m.text\"",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"_msgtype_defined_keys_": {
|
|
||||||
"description": "Additional keys as defined by the msgtype, e.g. \"body\""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Feedback": {
|
|
||||||
"id": "Feedback",
|
|
||||||
"properties": {
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Member": {
|
|
||||||
"id": "Member",
|
|
||||||
"properties": {
|
|
||||||
"membership": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Enum: The membership state of this member.",
|
|
||||||
"enum": [
|
|
||||||
"invite",
|
|
||||||
"join",
|
|
||||||
"leave",
|
|
||||||
"knock"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"RoomInfo": {
|
|
||||||
"id": "RoomInfo",
|
|
||||||
"properties": {
|
|
||||||
"room_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The allocated room ID.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"room_alias": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The alias for the room.",
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"RoomConfig": {
|
|
||||||
"id": "RoomConfig",
|
|
||||||
"properties": {
|
|
||||||
"visibility": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Enum: The room visibility.",
|
|
||||||
"required": false,
|
|
||||||
"enum": [
|
|
||||||
"public",
|
|
||||||
"private"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"room_alias_name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The alias to give the new room.",
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"PaginationRequest": {
|
|
||||||
"id": "PaginationRequest",
|
|
||||||
"properties": {
|
|
||||||
"from": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The token to start getting results from."
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The token to stop getting results at."
|
|
||||||
},
|
|
||||||
"limit": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "The maximum number of entries to return."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"PaginationChunk": {
|
|
||||||
"id": "PaginationChunk",
|
|
||||||
"properties": {
|
|
||||||
"start": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"subTypes": [
|
|
||||||
"MessagePaginationChunk"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"MessagePaginationChunk": {
|
|
||||||
"id": "MessagePaginationChunk",
|
|
||||||
"properties": {
|
|
||||||
"chunk": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of message events.",
|
|
||||||
"items": {
|
|
||||||
"$ref": "MessageEvent"
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MemberPaginationChunk": {
|
|
||||||
"id": "MemberPaginationChunk",
|
|
||||||
"properties": {
|
|
||||||
"chunk": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of member events.",
|
|
||||||
"items": {
|
|
||||||
"$ref": "MemberEvent"
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Event": {
|
|
||||||
"id": "Event",
|
|
||||||
"properties": {
|
|
||||||
"event_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "An ID which uniquely identifies this event. This is automatically set by the server.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"room_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The room in which this event occurred. This is automatically set by the server.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The event type.",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"subTypes": [
|
|
||||||
"MessageEvent"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"EventId": {
|
|
||||||
"id": "EventId",
|
|
||||||
"properties": {
|
|
||||||
"event_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The allocated event ID for this event.",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"EventContent": {
|
|
||||||
"id": "EventContent",
|
|
||||||
"properties": {
|
|
||||||
"__event_content_keys__": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Event-specific content keys and values.",
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MessageEvent": {
|
|
||||||
"id": "MessageEvent",
|
|
||||||
"properties": {
|
|
||||||
"content": {
|
|
||||||
"type": "Message"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"MemberEvent": {
|
|
||||||
"id": "MemberEvent",
|
|
||||||
"properties": {
|
|
||||||
"content": {
|
|
||||||
"type": "Member"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"InviteRequest": {
|
|
||||||
"id": "InviteRequest",
|
|
||||||
"properties": {
|
|
||||||
"user_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The fully-qualified user ID."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"InitialSyncRoomData": {
|
|
||||||
"id": "InitialSyncRoomData",
|
|
||||||
"properties": {
|
|
||||||
"membership": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "This user's membership state in this room.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"room_id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The ID of this room.",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"type": "MessagePaginationChunk",
|
|
||||||
"description": "The most recent messages for this room, governed by the limit parameter.",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "A list of state events representing the current state of the room.",
|
|
||||||
"required": false,
|
|
||||||
"items": {
|
|
||||||
"$ref": "Event"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
===================
|
|
||||||
Documentation Style
|
|
||||||
===================
|
|
||||||
|
|
||||||
A brief single sentence to describe what this file contains; in this case a
|
|
||||||
description of the style to write documentation in.
|
|
||||||
|
|
||||||
|
|
||||||
Sections
|
|
||||||
========
|
|
||||||
|
|
||||||
Each section should be separated from the others by two blank lines. Headings
|
|
||||||
should be underlined using a row of equals signs (===). Paragraphs should be
|
|
||||||
separated by a single blank line, and wrap to no further than 80 columns.
|
|
||||||
|
|
||||||
[[TODO(username): if you want to leave some unanswered questions, notes for
|
|
||||||
further consideration, or other kinds of comment, use a TODO section. Make sure
|
|
||||||
to notate it with your name so we know who to ask about it!]]
|
|
||||||
|
|
||||||
Subsections
|
|
||||||
-----------
|
|
||||||
|
|
||||||
If required, subsections can use a row of dashes to underline their header. A
|
|
||||||
single blank line between subsections of a single section.
|
|
||||||
|
|
||||||
|
|
||||||
Bullet Lists
|
|
||||||
============
|
|
||||||
|
|
||||||
* Bullet lists can use asterisks with a single space either side.
|
|
||||||
|
|
||||||
* Another blank line between list elements.
|
|
||||||
|
|
||||||
|
|
||||||
Definition Lists
|
|
||||||
================
|
|
||||||
|
|
||||||
Terms:
|
|
||||||
Start in the first column, ending with a colon
|
|
||||||
|
|
||||||
Definitions:
|
|
||||||
Take a two space indent, following immediately from the term without a blank
|
|
||||||
line before it, but having a blank line afterwards.
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
========
|
|
||||||
Presence
|
|
||||||
========
|
|
||||||
|
|
||||||
A description of presence information and visibility between users.
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
Each user has the concept of Presence information. This encodes a sense of the
|
|
||||||
"availability" of that user, suitable for display on other user's clients.
|
|
||||||
|
|
||||||
|
|
||||||
Presence Information
|
|
||||||
====================
|
|
||||||
|
|
||||||
The basic piece of presence information is an enumeration of a small set of
|
|
||||||
state; such as "free to chat", "online", "busy", or "offline". The default state
|
|
||||||
unless the user changes it is "online". Lower states suggest some amount of
|
|
||||||
decreased availability from normal, which might have some client-side effect
|
|
||||||
like muting notification sounds and suggests to other users not to bother them
|
|
||||||
unless it is urgent. Equally, the "free to chat" state exists to let the user
|
|
||||||
announce their general willingness to receive messages moreso than default.
|
|
||||||
|
|
||||||
Home servers should also allow a user to set their state as "hidden" - a state
|
|
||||||
which behaves as offline, but allows the user to see the client state anyway and
|
|
||||||
generally interact with client features such as reading message history or
|
|
||||||
accessing contacts in the address book.
|
|
||||||
|
|
||||||
This basic state field applies to the user as a whole, regardless of how many
|
|
||||||
client devices they have connected. The home server should synchronise this
|
|
||||||
status choice among multiple devices to ensure the user gets a consistent
|
|
||||||
experience.
|
|
||||||
|
|
||||||
Idle Time
|
|
||||||
---------
|
|
||||||
|
|
||||||
As well as the basic state field, the presence information can also show a sense
|
|
||||||
of an "idle timer". This should be maintained individually by the user's
|
|
||||||
clients, and the homeserver can take the highest reported time as that to
|
|
||||||
report. Likely this should be presented in fairly coarse granularity; possibly
|
|
||||||
being limited to letting the home server automatically switch from a "free to
|
|
||||||
chat" or "online" mode into "idle".
|
|
||||||
|
|
||||||
When a user is offline, the Home Server can still report when the user was last
|
|
||||||
seen online, again perhaps in a somewhat coarse manner.
|
|
||||||
|
|
||||||
Device Type
|
|
||||||
-----------
|
|
||||||
|
|
||||||
Client devices that may limit the user experience somewhat (such as "mobile"
|
|
||||||
devices with limited ability to type on a real keyboard or read large amounts of
|
|
||||||
text) should report this to the home server, as this is also useful information
|
|
||||||
to report as "presence" if the user cannot be expected to provide a good typed
|
|
||||||
response to messages.
|
|
||||||
|
|
||||||
|
|
||||||
Presence List
|
|
||||||
=============
|
|
||||||
|
|
||||||
Each user's home server stores a "presence list" for that user. This stores a
|
|
||||||
list of other user IDs the user has chosen to add to it (remembering any ACL
|
|
||||||
Pointer if appropriate).
|
|
||||||
|
|
||||||
To be added to a contact list, the user being added must grant permission. Once
|
|
||||||
granted, both user's HS(es) store this information, as it allows the user who
|
|
||||||
has added the contact some more abilities; see below. Since such subscriptions
|
|
||||||
are likely to be bidirectional, HSes may wish to automatically accept requests
|
|
||||||
when a reverse subscription already exists.
|
|
||||||
|
|
||||||
As a convenience, presence lists should support the ability to collect users
|
|
||||||
into groups, which could allow things like inviting the entire group to a new
|
|
||||||
("ad-hoc") chat room, or easy interaction with the profile information ACL
|
|
||||||
implementation of the HS.
|
|
||||||
|
|
||||||
|
|
||||||
Presence and Permissions
|
|
||||||
========================
|
|
||||||
|
|
||||||
For a viewing user to be allowed to see the presence information of a target
|
|
||||||
user, either
|
|
||||||
|
|
||||||
* The target user has allowed the viewing user to add them to their presence
|
|
||||||
list, or
|
|
||||||
|
|
||||||
* The two users share at least one room in common
|
|
||||||
|
|
||||||
In the latter case, this allows for clients to display some minimal sense of
|
|
||||||
presence information in a user list for a room.
|
|
||||||
|
|
||||||
Home servers can also use the user's choice of presence state as a signal for
|
|
||||||
how to handle new private one-to-one chat message requests. For example, it
|
|
||||||
might decide:
|
|
||||||
|
|
||||||
"free to chat": accept anything
|
|
||||||
"online": accept from anyone in my addres book list
|
|
||||||
"busy": accept from anyone in this "important people" group in my address
|
|
||||||
book list
|
|
||||||
|
|
||||||
|
|
||||||
API Efficiency
|
|
||||||
==============
|
|
||||||
|
|
||||||
A simple implementation of presence messaging has the ability to cause a large
|
|
||||||
amount of Internet traffic relating to presence updates. In order to minimise
|
|
||||||
the impact of such a feature, the following observations can be made:
|
|
||||||
|
|
||||||
* There is no point in a Home Server polling status for peers in a user's
|
|
||||||
presence list if the user has no clients connected that care about it.
|
|
||||||
|
|
||||||
* It is highly likely that most presence subscriptions will be symmetric - a
|
|
||||||
given user watching another is likely to in turn be watched by that user.
|
|
||||||
|
|
||||||
* It is likely that most subscription pairings will be between users who share
|
|
||||||
at least one Room in common, and so their Home Servers are actively
|
|
||||||
exchanging message PDUs or transactions relating to that Room.
|
|
||||||
|
|
||||||
* Presence update messages do not need realtime guarantees. It is acceptable to
|
|
||||||
delay delivery of updates for some small amount of time (10 seconds to a
|
|
||||||
minute).
|
|
||||||
|
|
||||||
The general model of presence information is that of a HS registering its
|
|
||||||
interest in receiving presence status updates from other HSes, which then
|
|
||||||
promise to send them when required. Rather than actively polling for the
|
|
||||||
currentt state all the time, HSes can rely on their relative stability to only
|
|
||||||
push updates when required.
|
|
||||||
|
|
||||||
A Home Server should not rely on the longterm validity of this presence
|
|
||||||
information, however, as this would not cover such cases as a user's server
|
|
||||||
crashing and thus failing to inform their peers that users it used to host are
|
|
||||||
no longer available online. Therefore, each promise of future updates should
|
|
||||||
carry with a timeout value (whether explicit in the message, or implicit as some
|
|
||||||
defined default in the protocol), after which the receiving HS should consider
|
|
||||||
the information potentially stale and request it again.
|
|
||||||
|
|
||||||
However, because of the likelyhood that two home servers are exchanging messages
|
|
||||||
relating to chat traffic in a room common to both of them, the ongoing receipt
|
|
||||||
of these messages can be taken by each server as an implicit notification that
|
|
||||||
the sending server is still up and running, and therefore that no status changes
|
|
||||||
have happened; because if they had the server would have sent them. A second,
|
|
||||||
larger timeout should be applied to this implicit inference however, to protect
|
|
||||||
against implementation bugs or other reasons that the presence state cache may
|
|
||||||
become invalid; eventually the HS should re-enquire the current state of users
|
|
||||||
and update them with its own.
|
|
||||||
|
|
||||||
The following workflows can therefore be used to handle presence updates:
|
|
||||||
|
|
||||||
1 When a user first appears online their HS sends a message to each other HS
|
|
||||||
containing at least one user to be watched; each message carrying both a
|
|
||||||
notification of the sender's new online status, and a request to obtain and
|
|
||||||
watch the target users' presence information. This message implicitly
|
|
||||||
promises the sending HS will now push updates to the target HSes.
|
|
||||||
|
|
||||||
2 The target HSes then respond a single message each, containing the current
|
|
||||||
status of the requested user(s). These messages too implicitly promise the
|
|
||||||
target HSes will themselves push updates to the sending HS.
|
|
||||||
|
|
||||||
As these messages arrive at the sending user's HS they can be pushed to the
|
|
||||||
user's client(s), possibly batched again to ensure not too many small
|
|
||||||
messages which add extra protocol overheads.
|
|
||||||
|
|
||||||
At this point, all the user's clients now have the current presence status
|
|
||||||
information for this moment in time, and have promised to send each other
|
|
||||||
updates in future.
|
|
||||||
|
|
||||||
3 The HS maintains two watchdog timers per peer HS it is exchanging presence
|
|
||||||
information with. The first timer should have a relatively small expiry
|
|
||||||
(perhaps 1 minute), and the second timer should have a much longer time
|
|
||||||
(perhaps 1 hour).
|
|
||||||
|
|
||||||
4 Any time any kind of message is received from a peer HS, the short-term
|
|
||||||
presence timer associated with it is reset.
|
|
||||||
|
|
||||||
5 Whenever either of these timers expires, an HS should push a status reminder
|
|
||||||
to the target HS whose timer has now expired, and request again from that
|
|
||||||
server the status of the subscribed users.
|
|
||||||
|
|
||||||
6 On receipt of one of these presence status reminders, an HS can reset both
|
|
||||||
of its presence watchdog timers.
|
|
||||||
|
|
||||||
To avoid bursts of traffic, implementations should attempt to stagger the expiry
|
|
||||||
of the longer-term watchdog timers for different peer HSes.
|
|
||||||
|
|
||||||
When individual users actively change their status (either by explicit requests
|
|
||||||
from clients, or inferred changes due to idle timers or client timeouts), the HS
|
|
||||||
should batch up any status changes for some reasonable amount of time (10
|
|
||||||
seconds to a minute). This allows for reduced protocol overheads in the case of
|
|
||||||
multiple messages needing to be sent to the same peer HS; as is the likely
|
|
||||||
scenario in many cases, such as a given human user having multiple user
|
|
||||||
accounts.
|
|
||||||
|
|
||||||
|
|
||||||
API Requirements
|
|
||||||
================
|
|
||||||
|
|
||||||
The data model presented here puts the following requirements on the APIs:
|
|
||||||
|
|
||||||
Client-Server
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Requests that a client can make to its Home Server
|
|
||||||
|
|
||||||
* get/set current presence state
|
|
||||||
Basic enumeration + ability to set a custom piece of text
|
|
||||||
|
|
||||||
* report per-device idle time
|
|
||||||
After some (configurable?) idle time the device should send a single message
|
|
||||||
to set the idle duration. The HS can then infer a "start of idle" instant and
|
|
||||||
use that to keep the device idleness up to date. At some later point the
|
|
||||||
device can cancel this idleness.
|
|
||||||
|
|
||||||
* report per-device type
|
|
||||||
Inform the server that this device is a "mobile" device, or perhaps some
|
|
||||||
other to-be-defined category of reduced capability that could be presented to
|
|
||||||
other users.
|
|
||||||
|
|
||||||
* start/stop presence polling for my presence list
|
|
||||||
It is likely that these messages could be implicitly inferred by other
|
|
||||||
messages, though having explicit control is always useful.
|
|
||||||
|
|
||||||
* get my presence list
|
|
||||||
[implicit poll start?]
|
|
||||||
It is possible that the HS doesn't yet have current presence information when
|
|
||||||
the client requests this. There should be a "don't know" type too.
|
|
||||||
|
|
||||||
* add/remove a user to my presence list
|
|
||||||
|
|
||||||
Server-Server
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Requests that Home Servers make to others
|
|
||||||
|
|
||||||
* request permission to add a user to presence list
|
|
||||||
|
|
||||||
* allow/deny a request to add to a presence list
|
|
||||||
|
|
||||||
* perform a combined presence state push and subscription request
|
|
||||||
For each sending user ID, the message contains their new status.
|
|
||||||
For each receiving user ID, the message should contain an indication on
|
|
||||||
whether the sending server is also interested in receiving status from that
|
|
||||||
user; either as an immediate update response now, or as a promise to send
|
|
||||||
future updates.
|
|
||||||
|
|
||||||
Server to Client
|
|
||||||
----------------
|
|
||||||
|
|
||||||
[[TODO(paul): There also needs to be some way for a user's HS to push status
|
|
||||||
updates of the presence list to clients, but the general server-client event
|
|
||||||
model currently lacks a space to do that.]]
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
========
|
|
||||||
Profiles
|
|
||||||
========
|
|
||||||
|
|
||||||
A description of Synapse user profile metadata support.
|
|
||||||
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
Internally within Synapse users are referred to by an opaque ID, which consists
|
|
||||||
of some opaque localpart combined with the domain name of their home server.
|
|
||||||
Obviously this does not yield a very nice user experience; users would like to
|
|
||||||
see readable names for other users that are in some way meaningful to them.
|
|
||||||
Additionally, users like to be able to publish "profile" details to inform other
|
|
||||||
users of other information about them.
|
|
||||||
|
|
||||||
It is also conceivable that since we are attempting to provide a
|
|
||||||
worldwide-applicable messaging system, that users may wish to present different
|
|
||||||
subsets of information in their profile to different other people, from a
|
|
||||||
privacy and permissions perspective.
|
|
||||||
|
|
||||||
A Profile consists of a display name, an (optional?) avatar picture, and a set
|
|
||||||
of other metadata fields that the user may wish to publish (email address, phone
|
|
||||||
numbers, website URLs, etc...). We put no requirements on the display name other
|
|
||||||
than it being a valid Unicode string. Since it is likely that users will end up
|
|
||||||
having multiple accounts (perhaps by necessity of being hosted in multiple
|
|
||||||
places, perhaps by choice of wanting multiple distinct identifies), it would be
|
|
||||||
useful that a metadata field type exists that can refer to another Synapse User
|
|
||||||
ID, so that clients and HSes can make use of this information.
|
|
||||||
|
|
||||||
Metadata Fields
|
|
||||||
---------------
|
|
||||||
|
|
||||||
[[TODO(paul): Likely this list is incomplete; more fields can be defined as we
|
|
||||||
think of them. At the very least, any sort of supported ID for the 3rd Party ID
|
|
||||||
servers should be accounted for here.]]
|
|
||||||
|
|
||||||
* Synapse Directory Server username(s)
|
|
||||||
|
|
||||||
* Email address
|
|
||||||
|
|
||||||
* Phone number - classify "home"/"work"/"mobile"/custom?
|
|
||||||
|
|
||||||
* Twitter/Facebook/Google+/... social networks
|
|
||||||
|
|
||||||
* Location - keep this deliberately vague to allow people to choose how
|
|
||||||
granular it is
|
|
||||||
|
|
||||||
* "Bio" information - date of birth, etc...
|
|
||||||
|
|
||||||
* Synapse User ID of another account
|
|
||||||
|
|
||||||
* Web URL
|
|
||||||
|
|
||||||
* Freeform description text
|
|
||||||
|
|
||||||
|
|
||||||
Visibility Permissions
|
|
||||||
======================
|
|
||||||
|
|
||||||
A home server implementation could offer the ability to set permissions on
|
|
||||||
limited visibility of those fields. When another user requests access to the
|
|
||||||
target user's profile, their own identity should form part of that request. The
|
|
||||||
HS implementation can then decide which fields to make available to the
|
|
||||||
requestor.
|
|
||||||
|
|
||||||
A particular detail of implementation could allow the user to create one or more
|
|
||||||
ACLs; where each list is granted permission to see a given set of non-public
|
|
||||||
fields (compare to Google+ Circles) and contains a set of other people allowed
|
|
||||||
to use it. By giving these ACLs strong identities within the HS, they can be
|
|
||||||
referenced in communications with it, granting other users who encounter these
|
|
||||||
the "ACL Token" to use the details in that ACL.
|
|
||||||
|
|
||||||
If we further allow an ACL Token to be present on Room join requests or stored
|
|
||||||
by 3PID servers, then users of these ACLs gain the extra convenience of not
|
|
||||||
having to manually curate people in the access list; anyone in the room or with
|
|
||||||
knowledge of the 3rd Party ID is automatically granted access. Every HS and
|
|
||||||
client implementation would have to be aware of the existence of these ACL
|
|
||||||
Token, and include them in requests if present, but not every HS implementation
|
|
||||||
needs to actually provide the full permissions model. This can be used as a
|
|
||||||
distinguishing feature among competing implementations. However, servers MUST
|
|
||||||
NOT serve profile information from a cache if there is a chance that its limited
|
|
||||||
understanding could lead to information leakage.
|
|
||||||
|
|
||||||
|
|
||||||
Client Concerns of Multiple Accounts
|
|
||||||
====================================
|
|
||||||
|
|
||||||
Because a given person may want to have multiple Synapse User accounts, client
|
|
||||||
implementations should allow the use of multiple accounts simultaneously
|
|
||||||
(especially in the field of mobile phone clients, which generally don't support
|
|
||||||
running distinct instances of the same application). Where features like address
|
|
||||||
books, presence lists or rooms are presented, the client UI should remember to
|
|
||||||
make distinct with user account is in use for each.
|
|
||||||
|
|
||||||
|
|
||||||
Directory Servers
|
|
||||||
=================
|
|
||||||
|
|
||||||
Directory Servers can provide a forward mapping from human-readable names to
|
|
||||||
User IDs. These can provide a service similar to giving domain-namespaced names
|
|
||||||
for Rooms; in this case they can provide a way for a user to reference their
|
|
||||||
User ID in some external form (e.g. that can be printed on a business card).
|
|
||||||
|
|
||||||
The format for Synapse user name will consist of a localpart specific to the
|
|
||||||
directory server, and the domain name of that directory server:
|
|
||||||
|
|
||||||
@localname:some.domain.name
|
|
||||||
|
|
||||||
The localname is separated from the domain name using a colon, so as to ensure
|
|
||||||
the localname can still contain periods, as users may want this for similarity
|
|
||||||
to email addresses or the like, which typically can contain them. The format is
|
|
||||||
also visually quite distinct from email addresses, phone numbers, etc... so
|
|
||||||
hopefully reasonably "self-describing" when written on e.g. a business card
|
|
||||||
without surrounding context.
|
|
||||||
|
|
||||||
[[TODO(paul): we might have to think about this one - too close to email?
|
|
||||||
Twitter? Also it suggests a format scheme for room names of
|
|
||||||
#localname:domain.name, which I quite like]]
|
|
||||||
|
|
||||||
Directory server administrators should be able to make some kind of policy
|
|
||||||
decision on how these are allocated. Servers within some "closed" domain (such
|
|
||||||
as company-specific ones) may wish to verify the validity of a mapping using
|
|
||||||
their own internal mechanisms; "public" naming servers can operate on a FCFS
|
|
||||||
basis. There are overlapping concerns here with the idea of the 3rd party
|
|
||||||
identity servers as well, though in this specific case we are creating a new
|
|
||||||
namespace to allocate names into.
|
|
||||||
|
|
||||||
It would also be nice from a user experience perspective if the profile that a
|
|
||||||
given name links to can also declare that name as part of its metadata.
|
|
||||||
Furthermore as a security and consistency perspective it would be nice if each
|
|
||||||
end (the directory server and the user's home server) check the validity of the
|
|
||||||
mapping in some way. This needs investigation from a security perspective to
|
|
||||||
ensure against spoofing.
|
|
||||||
|
|
||||||
One such model may be that the user starts by declaring their intent to use a
|
|
||||||
given user name link to their home server, which then contacts the directory
|
|
||||||
service. At some point later (maybe immediately for "public open FCFS servers",
|
|
||||||
maybe after some kind of human intervention for verification) the DS decides to
|
|
||||||
honour this link, and includes it in its served output. It should also tell the
|
|
||||||
HS of this fact, so that the HS can present this as fact when requested for the
|
|
||||||
profile information. For efficiency, it may further wish to provide the HS with
|
|
||||||
a cryptographically-signed certificate as proof, so the HS serving the profile
|
|
||||||
can provide that too when asked, avoiding requesting HSes from constantly having
|
|
||||||
to contact the DS to verify this mapping. (Note: This is similar to the security
|
|
||||||
model often applied in DNS to verify PTR <-> A bidirectional mappings).
|
|
||||||
|
|
||||||
|
|
||||||
Identity Servers
|
|
||||||
================
|
|
||||||
|
|
||||||
The identity servers should support the concept of pointing a 3PID being able to
|
|
||||||
store an ACL Token as well as the main User ID. It is however, beyond scope to
|
|
||||||
do any kind of verification that any third-party IDs that the profile is
|
|
||||||
claiming match up to the 3PID mappings.
|
|
||||||
|
|
||||||
|
|
||||||
User Interface and Expectations Concerns
|
|
||||||
========================================
|
|
||||||
|
|
||||||
Given the weak "security" of some parts of this model as compared to what users
|
|
||||||
might expect, some care should be taken on how it is presented to users,
|
|
||||||
specifically in the naming or other wording of user interface components.
|
|
||||||
|
|
||||||
Most notably mere knowledge of an ACL Pointer is enough to read the information
|
|
||||||
stored in it. It is possible that Home or Identity Servers could leak this
|
|
||||||
information, allowing others to see it. This is a security-vs-convenience
|
|
||||||
balancing choice on behalf of the user who would choose, or not, to make use of
|
|
||||||
such a feature to publish their information.
|
|
||||||
|
|
||||||
Additionally, unless some form of strong end-to-end user-based encryption is
|
|
||||||
used, a user of ACLs for information privacy has to trust other home servers not
|
|
||||||
to lie about the identify of the user requesting access to the Profile.
|
|
||||||
|
|
||||||
|
|
||||||
API Requirements
|
|
||||||
================
|
|
||||||
|
|
||||||
The data model presented here puts the following requirements on the APIs:
|
|
||||||
|
|
||||||
Client-Server
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Requests that a client can make to its Home Server
|
|
||||||
|
|
||||||
* get/set my Display Name
|
|
||||||
This should return/take a simple "text/plain" field
|
|
||||||
|
|
||||||
* get/set my Avatar URL
|
|
||||||
The avatar image data itself is not stored by this API; we'll just store a
|
|
||||||
URL to let the clients fetch it. Optionally HSes could integrate this with
|
|
||||||
their generic content attacmhent storage service, allowing a user to set
|
|
||||||
upload their profile Avatar and update the URL to point to it.
|
|
||||||
|
|
||||||
* get/add/remove my metadata fields
|
|
||||||
Also we need to actually define types of metadata
|
|
||||||
|
|
||||||
* get another user's Display Name / Avatar / metadata fields
|
|
||||||
|
|
||||||
[[TODO(paul): At some later stage we should consider the API for:
|
|
||||||
|
|
||||||
* get/set ACL permissions on my metadata fields
|
|
||||||
|
|
||||||
* manage my ACL tokens
|
|
||||||
]]
|
|
||||||
|
|
||||||
Server-Server
|
|
||||||
-------------
|
|
||||||
|
|
||||||
Requests that Home Servers make to others
|
|
||||||
|
|
||||||
* get a user's Display Name / Avatar
|
|
||||||
|
|
||||||
* get a user's full profile - name/avatar + MD fields
|
|
||||||
This request must allow for specifying the User ID of the requesting user,
|
|
||||||
for permissions purposes. It also needs to take into account any ACL Tokens
|
|
||||||
the requestor has.
|
|
||||||
|
|
||||||
* push a change of Display Name to observers (overlaps with the presence API)
|
|
||||||
|
|
||||||
Room Event PDU Types
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
Events that are pushed from Home Servers to other Home Servers or clients.
|
|
||||||
|
|
||||||
* user Display Name change
|
|
||||||
|
|
||||||
* user Avatar change
|
|
||||||
[[TODO(paul): should the avatar image itself be stored in all the room
|
|
||||||
histories? maybe this event should just be a hint to clients that they should
|
|
||||||
re-fetch the avatar image]]
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
PUT /send/abc/ HTTP/1.1
|
|
||||||
Host: ...
|
|
||||||
Content-Length: ...
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"origin": "localhost:5000",
|
|
||||||
"pdus": [
|
|
||||||
{
|
|
||||||
"content": {},
|
|
||||||
"context": "tng",
|
|
||||||
"depth": 12,
|
|
||||||
"is_state": false,
|
|
||||||
"origin": "localhost:5000",
|
|
||||||
"pdu_id": 1404381396854,
|
|
||||||
"pdu_type": "feedback",
|
|
||||||
"prev_pdus": [
|
|
||||||
[
|
|
||||||
"1404381395883",
|
|
||||||
"localhost:6000"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"ts": 1404381427581
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"prev_ids": [
|
|
||||||
"1404381396852"
|
|
||||||
],
|
|
||||||
"ts": 1404381427823
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
...
|
|
||||||
|
|
||||||
======================================
|
|
||||||
|
|
||||||
GET /pull/-1/ HTTP/1.1
|
|
||||||
Host: ...
|
|
||||||
Content-Length: 0
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Length: ...
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
origin: ...,
|
|
||||||
prev_ids: ...,
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
data_id: ...,
|
|
||||||
prev_pdus: [...],
|
|
||||||
depth: ...,
|
|
||||||
ts: ...,
|
|
||||||
context: ...,
|
|
||||||
origin: ...,
|
|
||||||
content: {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
==================
|
|
||||||
Room Join Workflow
|
|
||||||
==================
|
|
||||||
|
|
||||||
An outline of the workflows required when a user joins a room.
|
|
||||||
|
|
||||||
Discovery
|
|
||||||
=========
|
|
||||||
|
|
||||||
To join a room, a user has to discover the room by some mechanism in order to
|
|
||||||
obtain the (opaque) Room ID and a candidate list of likely home servers that
|
|
||||||
contain it.
|
|
||||||
|
|
||||||
Sending an Invitation
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
The most direct way a user discovers the existence of a room is from a
|
|
||||||
invitation from some other user who is a member of that room.
|
|
||||||
|
|
||||||
The inviter's HS sets the membership status of the invitee to "invited" in the
|
|
||||||
"m.members" state key by sending a state update PDU. The HS then broadcasts this
|
|
||||||
PDU among the existing members in the usual way. An invitation message is also
|
|
||||||
sent to the invited user, containing the Room ID and the PDU ID of this
|
|
||||||
invitation state change and potentially a list of some other home servers to use
|
|
||||||
to accept the invite. The user's client can then choose to display it in some
|
|
||||||
way to alert the user.
|
|
||||||
|
|
||||||
[[TODO(paul): At present, no API has been designed or described to actually send
|
|
||||||
that invite to the invited user. Likely it will be some facet of the larger
|
|
||||||
user-user API required for presence, profile management, etc...]]
|
|
||||||
|
|
||||||
Directory Service
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
Alternatively, the user may discover the channel via a directory service; either
|
|
||||||
by performing a name lookup, or some kind of browse or search acitivty. However
|
|
||||||
this is performed, the end result is that the user's home server requests the
|
|
||||||
Room ID and candidate list from the directory service.
|
|
||||||
|
|
||||||
[[TODO(paul): At present, no API has been designed or described for this
|
|
||||||
directory service]]
|
|
||||||
|
|
||||||
|
|
||||||
Joining
|
|
||||||
=======
|
|
||||||
|
|
||||||
Once the ID and home servers are obtained, the user can then actually join the
|
|
||||||
room.
|
|
||||||
|
|
||||||
Accepting an Invite
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
If a user has received and accepted an invitation to join a room, the invitee's
|
|
||||||
home server can now send an invite acceptance message to a chosen candidate
|
|
||||||
server from the list given in the invitation, citing also the PDU ID of the
|
|
||||||
invitation as "proof" of their invite. (This is required as due to late message
|
|
||||||
propagation it could be the case that the acceptance is received before the
|
|
||||||
invite by some servers). If this message is allowed by the candidate server, it
|
|
||||||
generates a new PDU that updates the invitee's membership status to "joined",
|
|
||||||
referring back to the acceptance PDU, and broadcasts that as a state change in
|
|
||||||
the usual way. The newly-invited user is now a full member of the room, and
|
|
||||||
state propagation proceeds as usual.
|
|
||||||
|
|
||||||
Joining a Public Room
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
If a user has discovered the existence of a room they wish to join but does not
|
|
||||||
have an active invitation, they can request to join it directly by sending a
|
|
||||||
join message to a candidate server on the list provided by the directory
|
|
||||||
service. As this list may be out of date, the HS should be prepared to retry
|
|
||||||
other candidates if the chosen one is no longer aware of the room, because it
|
|
||||||
has no users as members in it.
|
|
||||||
|
|
||||||
Once a candidate server that is aware of the room has been found, it can
|
|
||||||
broadcast an update PDU to add the member into the "m.members" key setting their
|
|
||||||
state directly to "joined" (i.e. bypassing the two-phase invite semantics),
|
|
||||||
remembering to include the new user's HS in that list.
|
|
||||||
|
|
||||||
Knocking on a Semi-Public Room
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
If a user requests to join a room but the join mode of the room is "knock", the
|
|
||||||
join is not immediately allowed. Instead, if the user wishes to proceed, they
|
|
||||||
can instead post a "knock" message, which informs other members of the room that
|
|
||||||
the would-be joiner wishes to become a member and sets their membership value to
|
|
||||||
"knocked". If any of them wish to accept this, they can then send an invitation
|
|
||||||
in the usual way described above. Knowing that the user has already knocked and
|
|
||||||
expressed an interest in joining, the invited user's home server should
|
|
||||||
immediately accept that invitation on the user's behalf, and go on to join the
|
|
||||||
room in the usual way.
|
|
||||||
|
|
||||||
[[NOTE(Erik): Though this may confuse users who expect 'X has joined' to
|
|
||||||
actually be a user initiated action, i.e. they may expect that 'X' is actually
|
|
||||||
looking at synapse right now?]]
|
|
||||||
|
|
||||||
[[NOTE(paul): Yes, a fair point maybe we should suggest HSes don't do that, and
|
|
||||||
just offer an invite to the user as normal]]
|
|
||||||
|
|
||||||
Private and Non-Existent Rooms
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
If a user requests to join a room but the room is either unknown by the home
|
|
||||||
server receiving the request, or is known by the join mode is "invite" and the
|
|
||||||
user has not been invited, the server must respond that the room does not exist.
|
|
||||||
This is to prevent leaking information about the existence and identity of
|
|
||||||
private rooms.
|
|
||||||
|
|
||||||
|
|
||||||
Outstanding Questions
|
|
||||||
=====================
|
|
||||||
|
|
||||||
* Do invitations or knocks time out and expire at some point? If so when? Time
|
|
||||||
is hard in distributed systems.
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
===========
|
|
||||||
Rooms Model
|
|
||||||
===========
|
|
||||||
|
|
||||||
A description of the general data model used to implement Rooms, and the
|
|
||||||
user-level visible effects and implications.
|
|
||||||
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
"Rooms" in Synapse are shared messaging channels over which all the participant
|
|
||||||
users can exchange messages. Rooms have an opaque persistent identify, a
|
|
||||||
globally-replicated set of state (consisting principly of a membership set of
|
|
||||||
users, and other management and miscellaneous metadata), and a message history.
|
|
||||||
|
|
||||||
|
|
||||||
Room Identity and Naming
|
|
||||||
========================
|
|
||||||
|
|
||||||
Rooms can be arbitrarily created by any user on any home server; at which point
|
|
||||||
the home server will sign the message that creates the channel, and the
|
|
||||||
fingerprint of this signature becomes the strong persistent identify of the
|
|
||||||
room. This now identifies the room to any home server in the network regardless
|
|
||||||
of its original origin. This allows the identify of the room to outlive any
|
|
||||||
particular server. Subject to appropriate permissions [to be discussed later],
|
|
||||||
any current member of a room can invite others to join it, can post messages
|
|
||||||
that become part of its history, and can change the persistent state of the room
|
|
||||||
(including its current set of permissions).
|
|
||||||
|
|
||||||
Home servers can provide a directory service, allowing a lookup from a
|
|
||||||
convenient human-readable form of room label to a room ID. This mapping is
|
|
||||||
scoped to the particular home server domain and so simply represents that server
|
|
||||||
administrator's opinion of what room should take that label; it does not have to
|
|
||||||
be globally replicated and does not form part of the stored state of that room.
|
|
||||||
|
|
||||||
This room name takes the form
|
|
||||||
|
|
||||||
#localname:some.domain.name
|
|
||||||
|
|
||||||
for similarity and consistency with user names on directories.
|
|
||||||
|
|
||||||
To join a room (and therefore to be allowed to inspect past history, post new
|
|
||||||
messages to it, and read its state), a user must become aware of the room's
|
|
||||||
fingerprint ID. There are two mechanisms to allow this:
|
|
||||||
|
|
||||||
* An invite message from someone else in the room
|
|
||||||
|
|
||||||
* A referral from a room directory service
|
|
||||||
|
|
||||||
As room IDs are opaque and ephemeral, they can serve as a mechanism to create
|
|
||||||
"ad-hoc" rooms deliberately unnamed, for small group-chats or even private
|
|
||||||
one-to-one message exchange.
|
|
||||||
|
|
||||||
|
|
||||||
Stored State and Permissions
|
|
||||||
============================
|
|
||||||
|
|
||||||
Every room has a globally-replicated set of stored state. This state is a set of
|
|
||||||
key/value or key/subkey/value pairs. The value of every (sub)key is a
|
|
||||||
JSON-representable object. The main key of a piece of stored state establishes
|
|
||||||
its meaning; some keys store sub-keys to allow a sub-structure within them [more
|
|
||||||
detail below]. Some keys have special meaning to Synapse, as they relate to
|
|
||||||
management details of the room itself, storing such details as user membership,
|
|
||||||
and permissions of users to alter the state of the room itself. Other keys may
|
|
||||||
store information to present to users, which the system does not directly rely
|
|
||||||
on. The key space itself is namespaced, allowing 3rd party extensions, subject
|
|
||||||
to suitable permission.
|
|
||||||
|
|
||||||
Permission management is based on the concept of "power-levels". Every user
|
|
||||||
within a room has an integer assigned, being their "power-level" within that
|
|
||||||
room. Along with its actual data value, each key (or subkey) also stores the
|
|
||||||
minimum power-level a user must have in order to write to that key, the
|
|
||||||
power-level of the last user who actually did write to it, and the PDU ID of
|
|
||||||
that state change.
|
|
||||||
|
|
||||||
To be accepted as valid, a change must NOT:
|
|
||||||
|
|
||||||
* Be made by a user having a power-level lower than required to write to the
|
|
||||||
state key
|
|
||||||
|
|
||||||
* Alter the required power-level for that state key to a value higher than the
|
|
||||||
user has
|
|
||||||
|
|
||||||
* Increase that user's own power-level
|
|
||||||
|
|
||||||
* Grant any other user a power-level higher than the level of the user making
|
|
||||||
the change
|
|
||||||
|
|
||||||
[[TODO(paul): consider if relaxations should be allowed; e.g. is the current
|
|
||||||
outright-winner allowed to raise their own level, to allow for "inflation"?]]
|
|
||||||
|
|
||||||
|
|
||||||
Room State Keys
|
|
||||||
===============
|
|
||||||
|
|
||||||
[[TODO(paul): if this list gets too big it might become necessary to move it
|
|
||||||
into its own doc]]
|
|
||||||
|
|
||||||
The following keys have special semantics or meaning to Synapse itself:
|
|
||||||
|
|
||||||
m.member (has subkeys)
|
|
||||||
Stores a sub-key for every Synapse User ID which is currently a member of
|
|
||||||
this room. Its value gives the membership type ("knocked", "invited",
|
|
||||||
"joined").
|
|
||||||
|
|
||||||
m.power_levels
|
|
||||||
Stores a mapping from Synapse User IDs to their power-level in the room. If
|
|
||||||
they are not present in this mapping, the default applies.
|
|
||||||
|
|
||||||
The reason to store this as a single value rather than a value with subkeys
|
|
||||||
is that updates to it are atomic; allowing a number of colliding-edit
|
|
||||||
problems to be avoided.
|
|
||||||
|
|
||||||
m.default_level
|
|
||||||
Gives the default power-level for members of the room that do not have one
|
|
||||||
specified in their membership key.
|
|
||||||
|
|
||||||
m.invite_level
|
|
||||||
If set, gives the minimum power-level required for members to invite others
|
|
||||||
to join, or to accept knock requests from non-members requesting access. If
|
|
||||||
absent, then invites are not allowed. An invitation involves setting their
|
|
||||||
membership type to "invited", in addition to sending the invite message.
|
|
||||||
|
|
||||||
m.join_rules
|
|
||||||
Encodes the rules on how non-members can join the room. Has the following
|
|
||||||
possibilities:
|
|
||||||
"public" - a non-member can join the room directly
|
|
||||||
"knock" - a non-member cannot join the room, but can post a single "knock"
|
|
||||||
message requesting access, which existing members may approve or deny
|
|
||||||
"invite" - non-members cannot join the room without an invite from an
|
|
||||||
existing member
|
|
||||||
"private" - nobody who is not in the 'may_join' list or already a member
|
|
||||||
may join by any mechanism
|
|
||||||
|
|
||||||
In any of the first three modes, existing members with sufficient permission
|
|
||||||
can send invites to non-members if allowed by the "m.invite_level" key. A
|
|
||||||
"private" room is not allowed to have the "m.invite_level" set.
|
|
||||||
|
|
||||||
A client may use the value of this key to hint at the user interface
|
|
||||||
expectations to provide; in particular, a private chat with one other use
|
|
||||||
might warrant specific handling in the client.
|
|
||||||
|
|
||||||
m.may_join
|
|
||||||
A list of User IDs that are always allowed to join the room, regardless of any
|
|
||||||
of the prevailing join rules and invite levels. These apply even to private
|
|
||||||
rooms. These are stored in a single list with normal update-powerlevel
|
|
||||||
permissions applied; users cannot arbitrarily remove themselves from the list.
|
|
||||||
|
|
||||||
m.add_state_level
|
|
||||||
The power-level required for a user to be able to add new state keys.
|
|
||||||
|
|
||||||
m.public_history
|
|
||||||
If set and true, anyone can request the history of the room, without needing
|
|
||||||
to be a member of the room.
|
|
||||||
|
|
||||||
m.archive_servers
|
|
||||||
For "public" rooms with public history, gives a list of home servers that
|
|
||||||
should be included in message distribution to the room, even if no users on
|
|
||||||
that server are present. These ensure that a public room can still persist
|
|
||||||
even if no users are currently members of it. This list should be consulted by
|
|
||||||
the dirctory servers as the candidate list they respond with.
|
|
||||||
|
|
||||||
The following keys are provided by Synapse for user benefit, but their value is
|
|
||||||
not otherwise used by Synapse.
|
|
||||||
|
|
||||||
m.name
|
|
||||||
Stores a short human-readable name for the room, such that clients can display
|
|
||||||
to a user to assist in identifying which room is which.
|
|
||||||
|
|
||||||
This name specifically is not the strong ID used by the message transport
|
|
||||||
system to refer to the room, because it may be changed from time to time.
|
|
||||||
|
|
||||||
m.topic
|
|
||||||
Stores the current human-readable topic
|
|
||||||
|
|
||||||
|
|
||||||
Room Creation Templates
|
|
||||||
=======================
|
|
||||||
|
|
||||||
A client (or maybe home server?) could offer a few templates for the creation of
|
|
||||||
new rooms. For example, for a simple private one-to-one chat the channel could
|
|
||||||
assign the creator a power-level of 1, requiring a level of 1 to invite, and
|
|
||||||
needing an invite before members can join. An invite is then sent to the other
|
|
||||||
party, and if accepted and the other user joins, the creator's power-level can
|
|
||||||
now be reduced to 0. This now leaves a room with two participants in it being
|
|
||||||
unable to add more.
|
|
||||||
|
|
||||||
|
|
||||||
Rooms that Continue History
|
|
||||||
===========================
|
|
||||||
|
|
||||||
An option that could be considered for room creation, is that when a new room is
|
|
||||||
created the creator could specify a PDU ID into an existing room, as the history
|
|
||||||
continuation point. This would be stored as an extra piece of meta-data on the
|
|
||||||
initial PDU of the room's creation. (It does not appear in the normal previous
|
|
||||||
PDU linkage).
|
|
||||||
|
|
||||||
This would allow users in rooms to "fork" a room, if it is considered that the
|
|
||||||
conversations in the room no longer fit its original purpose, and wish to
|
|
||||||
diverge. Existing permissions on the original room would continue to apply of
|
|
||||||
course, for viewing that history. If both rooms are considered "public" we might
|
|
||||||
also want to define a message to post into the original room to represent this
|
|
||||||
fork point, and give a reference to the new room.
|
|
||||||
|
|
||||||
|
|
||||||
User Direct Message Rooms
|
|
||||||
=========================
|
|
||||||
|
|
||||||
There is no need to build a mechanism for directly sending messages between
|
|
||||||
users, because a room can handle this ability. To allow direct user-to-user chat
|
|
||||||
messaging we simply need to be able to create rooms with specific set of
|
|
||||||
permissions to allow this direct messaging.
|
|
||||||
|
|
||||||
Between any given pair of user IDs that wish to exchange private messages, there
|
|
||||||
will exist a single shared Room, created lazily by either side. These rooms will
|
|
||||||
need a certain amount of special handling in both home servers and display on
|
|
||||||
clients, but as much as possible should be treated by the lower layers of code
|
|
||||||
the same as other rooms.
|
|
||||||
|
|
||||||
Specially, a client would likely offer a special menu choice associated with
|
|
||||||
another user (in room member lists, presence list, etc..) as "direct chat". That
|
|
||||||
would perform all the necessary steps to create the private chat room. Receiving
|
|
||||||
clients should display these in a special way too as the room name is not
|
|
||||||
important; instead it should distinguish them on the Display Name of the other
|
|
||||||
party.
|
|
||||||
|
|
||||||
Home Servers will need a client-API option to request setting up a new user-user
|
|
||||||
chat room, which will then need special handling within the server. It will
|
|
||||||
create a new room with the following
|
|
||||||
|
|
||||||
m.member: the proposing user
|
|
||||||
m.join_rules: "private"
|
|
||||||
m.may_join: both users
|
|
||||||
m.power_levels: empty
|
|
||||||
m.default_level: 0
|
|
||||||
m.add_state_level: 0
|
|
||||||
m.public_history: False
|
|
||||||
|
|
||||||
Having created the room, it can send an invite message to the other user in the
|
|
||||||
normal way - the room permissions state that no users can be set to the invited
|
|
||||||
state, but because they're in the may_join list then they'd be allowed to join
|
|
||||||
anyway.
|
|
||||||
|
|
||||||
In this arrangement there is now a room with both users may join but neither has
|
|
||||||
the power to invite any others. Both users now have the confidence that (at
|
|
||||||
least within the messaging system itself) their messages remain private and
|
|
||||||
cannot later be provably leaked to a third party. They can freely set the topic
|
|
||||||
or name if they choose and add or edit any other state of the room. The update
|
|
||||||
powerlevel of each of these fixed properties should be 1, to lock out the users
|
|
||||||
from being able to alter them.
|
|
||||||
|
|
||||||
|
|
||||||
Anti-Glare
|
|
||||||
==========
|
|
||||||
|
|
||||||
There exists the possibility of a race condition if two users who have no chat
|
|
||||||
history with each other simultaneously create a room and invite the other to it.
|
|
||||||
This is called a "glare" situation. There are two possible ideas for how to
|
|
||||||
resolve this:
|
|
||||||
|
|
||||||
* Each Home Server should persist the mapping of (user ID pair) to room ID, so
|
|
||||||
that duplicate requests can be suppressed. On receipt of a room creation
|
|
||||||
request that the HS thinks there already exists a room for, the invitation to
|
|
||||||
join can be rejected if:
|
|
||||||
a) the HS believes the sending user is already a member of the room (and
|
|
||||||
maybe their HS has forgotten this fact), or
|
|
||||||
b) the proposed room has a lexicographically-higher ID than the existing
|
|
||||||
room (to resolve true race condition conflicts)
|
|
||||||
|
|
||||||
* The room ID for a private 1:1 chat has a special form, determined by
|
|
||||||
concatenting the User IDs of both members in a deterministic order, such that
|
|
||||||
it doesn't matter which side creates it first; the HSes can just ignore
|
|
||||||
(or merge?) received PDUs that create the room twice.
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
===========
|
|
||||||
Terminology
|
|
||||||
===========
|
|
||||||
|
|
||||||
A list of definitions of specific terminology used among these documents.
|
|
||||||
These terms were originally taken from the server-server documentation, and may
|
|
||||||
not currently match the exact meanings used in other places; though as a
|
|
||||||
medium-term goal we should encourage the unification of this terminology.
|
|
||||||
|
|
||||||
|
|
||||||
Terms
|
|
||||||
=====
|
|
||||||
|
|
||||||
Backfilling:
|
|
||||||
The process of synchronising historic state from one home server to another,
|
|
||||||
to backfill the event storage so that scrollback can be presented to the
|
|
||||||
client(s). (Formerly, and confusingly, called 'pagination')
|
|
||||||
|
|
||||||
Context:
|
|
||||||
A single human-level entity of interest (currently, a chat room)
|
|
||||||
|
|
||||||
EDU (Ephemeral Data Unit):
|
|
||||||
A message that relates directly to a given pair of home servers that are
|
|
||||||
exchanging it. EDUs are short-lived messages that related only to one single
|
|
||||||
pair of servers; they are not persisted for a long time and are not forwarded
|
|
||||||
on to other servers. Because of this, they have no internal ID nor previous
|
|
||||||
EDUs reference chain.
|
|
||||||
|
|
||||||
Event:
|
|
||||||
A record of activity that records a single thing that happened on to a context
|
|
||||||
(currently, a chat room). These are the "chat messages" that Synapse makes
|
|
||||||
available.
|
|
||||||
[[NOTE(paul): The current server-server implementation calls these simply
|
|
||||||
"messages" but the term is too ambiguous here; I've called them Events]]
|
|
||||||
|
|
||||||
PDU (Persistent Data Unit):
|
|
||||||
A message that relates to a single context, irrespective of the server that
|
|
||||||
is communicating it. PDUs either encode a single Event, or a single State
|
|
||||||
change. A PDU is referred to by its PDU ID; the pair of its origin server
|
|
||||||
and local reference from that server.
|
|
||||||
|
|
||||||
PDU ID:
|
|
||||||
The pair of PDU Origin and PDU Reference, that together globally uniquely
|
|
||||||
refers to a specific PDU.
|
|
||||||
|
|
||||||
PDU Origin:
|
|
||||||
The name of the origin server that generated a given PDU. This may not be the
|
|
||||||
server from which it has been received, due to the way they are copied around
|
|
||||||
from server to server. The origin always records the original server that
|
|
||||||
created it.
|
|
||||||
|
|
||||||
PDU Reference:
|
|
||||||
A local ID used to refer to a specific PDU from a given origin server. These
|
|
||||||
references are opaque at the protocol level, but may optionally have some
|
|
||||||
structured meaning within a given origin server or implementation.
|
|
||||||
|
|
||||||
Presence:
|
|
||||||
The concept of whether a user is currently online, how available they declare
|
|
||||||
they are, and so on. See also: doc/model/presence
|
|
||||||
|
|
||||||
Profile:
|
|
||||||
A set of metadata about a user, such as a display name, provided for the
|
|
||||||
benefit of other users. See also: doc/model/profiles
|
|
||||||
|
|
||||||
Room ID:
|
|
||||||
An opaque string (of as-yet undecided format) that identifies a particular
|
|
||||||
room and used in PDUs referring to it.
|
|
||||||
|
|
||||||
Room Alias:
|
|
||||||
A human-readable string of the form #name:some.domain that users can use as a
|
|
||||||
pointer to identify a room; a Directory Server will map this to its Room ID
|
|
||||||
|
|
||||||
State:
|
|
||||||
A set of metadata maintained about a Context, which is replicated among the
|
|
||||||
servers in addition to the history of Events.
|
|
||||||
|
|
||||||
User ID:
|
|
||||||
A string of the form @localpart:domain.name that identifies a user for
|
|
||||||
wire-protocol purposes. The localpart is meaningless outside of a particular
|
|
||||||
home server. This takes a human-readable form that end-users can use directly
|
|
||||||
if they so wish, avoiding the 3PIDs.
|
|
||||||
|
|
||||||
Transaction:
|
|
||||||
A message which relates to the communication between a given pair of servers.
|
|
||||||
A transaction contains possibly-empty lists of PDUs and EDUs.
|
|
||||||
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
======================
|
|
||||||
Third Party Identities
|
|
||||||
======================
|
|
||||||
|
|
||||||
A description of how email addresses, mobile phone numbers and other third
|
|
||||||
party identifiers can be used to authenticate and discover users in Matrix.
|
|
||||||
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
New users need to authenticate their account. An email or SMS text message can
|
|
||||||
be a convenient form of authentication. Users already have email addresses
|
|
||||||
and phone numbers for contacts in their address book. They want to communicate
|
|
||||||
with those contacts in Matrix without manually exchanging a Matrix User ID with
|
|
||||||
them.
|
|
||||||
|
|
||||||
Third Party IDs
|
|
||||||
---------------
|
|
||||||
|
|
||||||
[[TODO(markjh): Describe the format of a 3PID]]
|
|
||||||
|
|
||||||
|
|
||||||
Third Party ID Associations
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
An Associaton is a binding between a Matrix User ID and a Third Party ID (3PID).
|
|
||||||
Each 3PID can be associated with one Matrix User ID at a time.
|
|
||||||
|
|
||||||
[[TODO(markjh): JSON format of the association.]]
|
|
||||||
|
|
||||||
Verification
|
|
||||||
------------
|
|
||||||
|
|
||||||
An Assocation must be verified by a trusted Verification Server. Email
|
|
||||||
addresses and phone numbers can be verified by sending a token to the address
|
|
||||||
which a client can supply to the verifier to confirm ownership.
|
|
||||||
|
|
||||||
An email Verification Server may be capable of verifying all email 3PIDs or may
|
|
||||||
be restricted to verifying addresses for a particular domain. A phone number
|
|
||||||
Verification Server may be capable of verifying all phone numbers or may be
|
|
||||||
restricted to verifying numbers for a given country or phone prefix.
|
|
||||||
|
|
||||||
Verification Servers fulfil a similar role to Certificate Authorities in PKI so
|
|
||||||
a similar level of vetting should be required before clients trust their
|
|
||||||
signatures.
|
|
||||||
|
|
||||||
A Verification Server may wish to check for existing Associations for a 3PID
|
|
||||||
before creating a new Association.
|
|
||||||
|
|
||||||
Discovery
|
|
||||||
---------
|
|
||||||
|
|
||||||
Users can discover Associations using a trusted Identity Server. Each
|
|
||||||
Association will be signed by the Identity Server. An Identity Server may store
|
|
||||||
the entire space of Associations or may delegate to other Identity Servers when
|
|
||||||
looking up Associations.
|
|
||||||
|
|
||||||
Each Association returned from an Identity Server must be signed by a
|
|
||||||
Verification Server. Clients should check these signatures.
|
|
||||||
|
|
||||||
Identity Servers fulfil a similar role to DNS servers.
|
|
||||||
|
|
||||||
Privacy
|
|
||||||
-------
|
|
||||||
|
|
||||||
A User may publish the association between their phone number and Matrix User ID
|
|
||||||
on the Identity Server without publishing the number in their Profile hosted on
|
|
||||||
their Home Server.
|
|
||||||
|
|
||||||
Identity Servers should refrain from publishing reverse mappings and should
|
|
||||||
take steps, such as rate limiting, to prevent attackers enumerating the space of
|
|
||||||
mappings.
|
|
||||||
|
|
||||||
Federation
|
|
||||||
==========
|
|
||||||
|
|
||||||
Delegation
|
|
||||||
----------
|
|
||||||
|
|
||||||
Verification Servers could delegate signing to another server by issuing
|
|
||||||
certificate to that server allowing it to verify and sign a subset of 3PID on
|
|
||||||
its behalf. It would be necessary to provide a language for describing which
|
|
||||||
subset of 3PIDs that server had authority to validate. Alternatively it could
|
|
||||||
delegate the verification step to another server but sign the resulting
|
|
||||||
association itself.
|
|
||||||
|
|
||||||
The 3PID space will have a heirachical structure like DNS so Identity Servers
|
|
||||||
can delegate lookups to other servers. An Identity Server should be prepared
|
|
||||||
to host or delegate any valid association within the subset of the 3PIDs it is
|
|
||||||
resonsible for.
|
|
||||||
|
|
||||||
Multiple Root Verification Servers
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
There can be multiple root Verification Servers and an Association could be
|
|
||||||
signed by multiple servers if different clients trust different subsets of
|
|
||||||
the verification servers.
|
|
||||||
|
|
||||||
Multiple Root Identity Servers
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
There can be be multiple root Identity Servers. Clients will add each
|
|
||||||
Association to all root Identity Servers.
|
|
||||||
|
|
||||||
[[TODO(markjh): Describe how clients find the list of root Identity Servers]]
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
|
|
||||||
Transaction
|
|
||||||
===========
|
|
||||||
|
|
||||||
Required keys:
|
|
||||||
|
|
||||||
============ =================== ===============================================
|
|
||||||
Key Type Description
|
|
||||||
============ =================== ===============================================
|
|
||||||
origin String DNS name of homeserver making this transaction.
|
|
||||||
ts Integer Timestamp in milliseconds on originating
|
|
||||||
homeserver when this transaction started.
|
|
||||||
previous_ids List of Strings List of transactions that were sent immediately
|
|
||||||
prior to this transaction.
|
|
||||||
pdus List of Objects List of updates contained in this transaction.
|
|
||||||
============ =================== ===============================================
|
|
||||||
|
|
||||||
|
|
||||||
PDU
|
|
||||||
===
|
|
||||||
|
|
||||||
Required keys:
|
|
||||||
|
|
||||||
============ ================== ================================================
|
|
||||||
Key Type Description
|
|
||||||
============ ================== ================================================
|
|
||||||
context String Event context identifier
|
|
||||||
origin String DNS name of homeserver that created this PDU.
|
|
||||||
pdu_id String Unique identifier for PDU within the context for
|
|
||||||
the originating homeserver.
|
|
||||||
ts Integer Timestamp in milliseconds on originating
|
|
||||||
homeserver when this PDU was created.
|
|
||||||
pdu_type String PDU event type.
|
|
||||||
prev_pdus List of Pairs The originating homeserver and PDU ids of the
|
|
||||||
of Strings most recent PDUs the homeserver was aware of for
|
|
||||||
this context when it made this PDU.
|
|
||||||
depth Integer The maximum depth of the previous PDUs plus one.
|
|
||||||
============ ================== ================================================
|
|
||||||
|
|
||||||
Keys for state updates:
|
|
||||||
|
|
||||||
================== ============ ================================================
|
|
||||||
Key Type Description
|
|
||||||
================== ============ ================================================
|
|
||||||
is_state Boolean True if this PDU is updating state.
|
|
||||||
state_key String Optional key identifying the updated state within
|
|
||||||
the context.
|
|
||||||
power_level Integer The asserted power level of the user performing
|
|
||||||
the update.
|
|
||||||
min_update Integer The required power level needed to replace this
|
|
||||||
update.
|
|
||||||
prev_state_id String The homeserver of the update this replaces
|
|
||||||
prev_state_origin String The PDU id of the update this replaces.
|
|
||||||
user String The user updating the state.
|
|
||||||
================== ============ ================================================
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
Scope
|
|
||||||
-----
|
|
||||||
|
|
||||||
This document considers threats specific to the server to server federation
|
|
||||||
synapse protocol.
|
|
||||||
|
|
||||||
|
|
||||||
Attacker
|
|
||||||
--------
|
|
||||||
|
|
||||||
It is assumed that the attacker can see and manipulate all network traffic
|
|
||||||
between any of the servers and may be in control of one or more homeservers
|
|
||||||
participating in the federation protocol.
|
|
||||||
|
|
||||||
Threat Model
|
|
||||||
============
|
|
||||||
|
|
||||||
Denial of Service
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The attacker could attempt to prevent delivery of messages to or from the
|
|
||||||
victim in order to:
|
|
||||||
|
|
||||||
* Disrupt service or marketing campaign of a commercial competitor.
|
|
||||||
* Censor a discussion or censor a participant in a discussion.
|
|
||||||
* Perform general vandalism.
|
|
||||||
|
|
||||||
Threat: Resource Exhaustion
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could cause the victims server to exhaust a particular resource
|
|
||||||
(e.g. open TCP connections, CPU, memory, disk storage)
|
|
||||||
|
|
||||||
Threat: Unrecoverable Consistency Violations
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could send messages which created an unrecoverable "split-brain"
|
|
||||||
state in the cluster such that the victim's servers could no longer dervive a
|
|
||||||
consistent view of the chatroom state.
|
|
||||||
|
|
||||||
Threat: Bad History
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could convince the victim to accept invalid messages which the
|
|
||||||
victim would then include in their view of the chatroom history. Other servers
|
|
||||||
in the chatroom would reject the invalid messages and potentially reject the
|
|
||||||
victims messages as well since they depended on the invalid messages.
|
|
||||||
|
|
||||||
Threat: Block Network Traffic
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could try to firewall traffic between the victim's server and some
|
|
||||||
or all of the other servers in the chatroom.
|
|
||||||
|
|
||||||
Threat: High Volume of Messages
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could send large volumes of messages to a chatroom with the victim
|
|
||||||
making the chatroom unusable.
|
|
||||||
|
|
||||||
Threat: Banning users without necessary authorisation
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could attempt to ban a user from a chatroom with the necessary
|
|
||||||
authorisation.
|
|
||||||
|
|
||||||
Spoofing
|
|
||||||
--------
|
|
||||||
|
|
||||||
An attacker could try to send a message claiming to be from the victim without
|
|
||||||
the victim having sent the message in order to:
|
|
||||||
|
|
||||||
* Impersonate the victim while performing illict activity.
|
|
||||||
* Obtain privileges of the victim.
|
|
||||||
|
|
||||||
Threat: Altering Message Contents
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could try to alter the contents of an existing message from the
|
|
||||||
victim.
|
|
||||||
|
|
||||||
Threat: Fake Message "origin" Field
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could try to send a new message purporting to be from the victim
|
|
||||||
with a phony "origin" field.
|
|
||||||
|
|
||||||
Spamming
|
|
||||||
--------
|
|
||||||
|
|
||||||
The attacker could try to send a high volume of solicicted or unsolicted
|
|
||||||
messages to the victim in order to:
|
|
||||||
|
|
||||||
* Find victims for scams.
|
|
||||||
* Market unwanted products.
|
|
||||||
|
|
||||||
Threat: Unsoliticted Messages
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could try to send messages to victims who do not wish to receive
|
|
||||||
them.
|
|
||||||
|
|
||||||
Threat: Abusive Messages
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could send abusive or threatening messages to the victim
|
|
||||||
|
|
||||||
Spying
|
|
||||||
------
|
|
||||||
|
|
||||||
The attacker could try to access message contents or metadata for messages sent
|
|
||||||
by the victim or to the victim that were not intended to reach the attacker in
|
|
||||||
order to:
|
|
||||||
|
|
||||||
* Gain sensitive personal or commercial information.
|
|
||||||
* Impersonate the victim using credentials contained in the messages.
|
|
||||||
(e.g. password reset messages)
|
|
||||||
* Discover who the victim was talking to and when.
|
|
||||||
|
|
||||||
Threat: Disclosure during Transmission
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could try to expose the message contents or metadata during
|
|
||||||
transmission between the servers.
|
|
||||||
|
|
||||||
Threat: Disclosure to Servers Outside Chatroom
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could try to convince servers within a chatroom to send messages to
|
|
||||||
a server it controls that was not authorised to be within the chatroom.
|
|
||||||
|
|
||||||
Threat: Disclosure to Servers Within Chatroom
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
An attacker could take control of a server within a chatroom to expose message
|
|
||||||
contents or metadata for messages in that room.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
===========================
|
|
||||||
Matrix Server-to-Server API
|
|
||||||
===========================
|
|
||||||
|
|
||||||
A description of the protocol used to communicate between Matrix home servers;
|
|
||||||
also known as Federation.
|
|
||||||
|
|
||||||
|
|
||||||
Overview
|
|
||||||
========
|
|
||||||
|
|
||||||
The server-server API is a mechanism by which two home servers can exchange
|
|
||||||
Matrix event messages, both as a real-time push of current events, and as a
|
|
||||||
historic fetching mechanism to synchronise past history for clients to view. It
|
|
||||||
uses HTTP connections between each pair of servers involved as the underlying
|
|
||||||
transport. Messages are exchanged between servers in real-time by active pushing
|
|
||||||
from each server's HTTP client into the server of the other. Queries to fetch
|
|
||||||
historic data for the purpose of back-filling scrollback buffers and the like
|
|
||||||
can also be performed.
|
|
||||||
|
|
||||||
|
|
||||||
{ Matrix clients } { Matrix clients }
|
|
||||||
^ | ^ |
|
|
||||||
| events | | events |
|
|
||||||
| V | V
|
|
||||||
+------------------+ +------------------+
|
|
||||||
| |---------( HTTP )---------->| |
|
|
||||||
| Home Server | | Home Server |
|
|
||||||
| |<--------( HTTP )-----------| |
|
|
||||||
+------------------+ +------------------+
|
|
||||||
|
|
||||||
There are three main kinds of communication that occur between home servers:
|
|
||||||
|
|
||||||
* Queries
|
|
||||||
These are single request/response interactions between a given pair of
|
|
||||||
servers, initiated by one side sending an HTTP request to obtain some
|
|
||||||
information, and responded by the other. They are not persisted and contain
|
|
||||||
no long-term significant history. They simply request a snapshot state at the
|
|
||||||
instant the query is made.
|
|
||||||
|
|
||||||
* EDUs - Ephemeral Data Units
|
|
||||||
These are notifications of events that are pushed from one home server to
|
|
||||||
another. They are not persisted and contain no long-term significant history,
|
|
||||||
nor does the receiving home server have to reply to them.
|
|
||||||
|
|
||||||
* PDUs - Persisted Data Units
|
|
||||||
These are notifications of events that are broadcast from one home server to
|
|
||||||
any others that are interested in the same "context" (namely, a Room ID).
|
|
||||||
They are persisted to long-term storage and form the record of history for
|
|
||||||
that context.
|
|
||||||
|
|
||||||
Where Queries are presented directly across the HTTP connection as GET requests
|
|
||||||
to specific URLs, EDUs and PDUs are further wrapped in an envelope called a
|
|
||||||
Transaction, which is transferred from the origin to the destination home server
|
|
||||||
using a PUT request.
|
|
||||||
|
|
||||||
|
|
||||||
Transactions and EDUs/PDUs
|
|
||||||
==========================
|
|
||||||
|
|
||||||
The transfer of EDUs and PDUs between home servers is performed by an exchange
|
|
||||||
of Transaction messages, which are encoded as JSON objects with a dict as the
|
|
||||||
top-level element, passed over an HTTP PUT request. A Transaction is meaningful
|
|
||||||
only to the pair of home servers that exchanged it; they are not globally-
|
|
||||||
meaningful.
|
|
||||||
|
|
||||||
Each transaction has an opaque ID and timestamp (UNIX epoch time in
|
|
||||||
milliseconds) generated by its origin server, an origin and destination server
|
|
||||||
name, a list of "previous IDs", and a list of PDUs - the actual message payload
|
|
||||||
that the Transaction carries.
|
|
||||||
|
|
||||||
{"transaction_id":"916d630ea616342b42e98a3be0b74113",
|
|
||||||
"ts":1404835423000,
|
|
||||||
"origin":"red",
|
|
||||||
"destination":"blue",
|
|
||||||
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
|
|
||||||
"pdus":[...],
|
|
||||||
"edus":[...]}
|
|
||||||
|
|
||||||
The "previous IDs" field will contain a list of previous transaction IDs that
|
|
||||||
the origin server has sent to this destination. Its purpose is to act as a
|
|
||||||
sequence checking mechanism - the destination server can check whether it has
|
|
||||||
successfully received that Transaction, or ask for a retransmission if not.
|
|
||||||
|
|
||||||
The "pdus" field of a transaction is a list, containing zero or more PDUs.[*]
|
|
||||||
Each PDU is itself a dict containing a number of keys, the exact details of
|
|
||||||
which will vary depending on the type of PDU. Similarly, the "edus" field is
|
|
||||||
another list containing the EDUs. This key may be entirely absent if there are
|
|
||||||
no EDUs to transfer.
|
|
||||||
|
|
||||||
(* Normally the PDU list will be non-empty, but the server should cope with
|
|
||||||
receiving an "empty" transaction, as this is useful for informing peers of other
|
|
||||||
transaction IDs they should be aware of. This effectively acts as a push
|
|
||||||
mechanism to encourage peers to continue to replicate content.)
|
|
||||||
|
|
||||||
All PDUs have an ID, a context, a declaration of their type, a list of other PDU
|
|
||||||
IDs that have been seen recently on that context (regardless of which origin
|
|
||||||
sent them), and a nested content field containing the actual event content.
|
|
||||||
|
|
||||||
[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
|
|
||||||
[origin,ref] pair like the prev_pdus are]]
|
|
||||||
|
|
||||||
{"pdu_id":"a4ecee13e2accdadf56c1025af232176",
|
|
||||||
"context":"#example.green",
|
|
||||||
"origin":"green",
|
|
||||||
"ts":1404838188000,
|
|
||||||
"pdu_type":"m.text",
|
|
||||||
"prev_pdus":[["blue","99d16afbc857975916f1d73e49e52b65"]],
|
|
||||||
"content":...
|
|
||||||
"is_state":false}
|
|
||||||
|
|
||||||
In contrast to the transaction layer, it is important to note that the prev_pdus
|
|
||||||
field of a PDU refers to PDUs that any origin server has sent, rather than
|
|
||||||
previous IDs that this origin has sent. This list may refer to other PDUs sent
|
|
||||||
by the same origin as the current one, or other origins.
|
|
||||||
|
|
||||||
Because of the distributed nature of participants in a Matrix conversation, it
|
|
||||||
is impossible to establish a globally-consistent total ordering on the events.
|
|
||||||
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
|
||||||
has received, a partial ordering can be constructed allowing causallity
|
|
||||||
relationships to be preserved. A client can then display these messages to the
|
|
||||||
end-user in some order consistent with their content and ensure that no message
|
|
||||||
that is semantically in reply of an earlier one is ever displayed before it.
|
|
||||||
|
|
||||||
PDUs fall into two main categories: those that deliver Events, and those that
|
|
||||||
synchronise State. For PDUs that relate to State synchronisation, additional
|
|
||||||
keys exist to support this:
|
|
||||||
|
|
||||||
{...,
|
|
||||||
"is_state":true,
|
|
||||||
"state_key":TODO
|
|
||||||
"power_level":TODO
|
|
||||||
"prev_state_id":TODO
|
|
||||||
"prev_state_origin":TODO}
|
|
||||||
|
|
||||||
[[TODO(paul): At this point we should probably have a long description of how
|
|
||||||
State management works, with descriptions of clobbering rules, power levels, etc
|
|
||||||
etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
|
|
||||||
so on. This part needs refining. And writing in its own document as the details
|
|
||||||
relate to the server/system as a whole, not specifically to server-server
|
|
||||||
federation.]]
|
|
||||||
|
|
||||||
EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
|
|
||||||
"previous" IDs. The only mandatory fields for these are the type, origin and
|
|
||||||
destination home server names, and the actual nested content.
|
|
||||||
|
|
||||||
{"edu_type":"m.presence",
|
|
||||||
"origin":"blue",
|
|
||||||
"destination":"orange",
|
|
||||||
"content":...}
|
|
||||||
|
|
||||||
|
|
||||||
Protocol URLs
|
|
||||||
=============
|
|
||||||
|
|
||||||
All these URLs are namespaced within a prefix of
|
|
||||||
|
|
||||||
/matrix/federation/v1/...
|
|
||||||
|
|
||||||
For active pushing of messages representing live activity "as it happens":
|
|
||||||
|
|
||||||
PUT .../send/:transaction_id/
|
|
||||||
Body: JSON encoding of a single Transaction
|
|
||||||
|
|
||||||
Response: [[TODO(paul): I don't actually understand what
|
|
||||||
ReplicationLayer.on_transaction() is doing here, so I'm not sure what the
|
|
||||||
response ought to be]]
|
|
||||||
|
|
||||||
The transaction_id path argument will override any ID given in the JSON body.
|
|
||||||
The destination name will be set to that of the receiving server itself. Each
|
|
||||||
embedded PDU in the transaction body will be processed.
|
|
||||||
|
|
||||||
|
|
||||||
To fetch a particular PDU:
|
|
||||||
|
|
||||||
GET .../pdu/:origin/:pdu_id/
|
|
||||||
|
|
||||||
Response: JSON encoding of a single Transaction containing one PDU
|
|
||||||
|
|
||||||
Retrieves a given PDU from the server. The response will contain a single new
|
|
||||||
Transaction, inside which will be the requested PDU.
|
|
||||||
|
|
||||||
|
|
||||||
To fetch all the state of a given context:
|
|
||||||
|
|
||||||
GET .../state/:context/
|
|
||||||
|
|
||||||
Response: JSON encoding of a single Transaction containing multiple PDUs
|
|
||||||
|
|
||||||
Retrieves a snapshot of the entire current state of the given context. The
|
|
||||||
response will contain a single Transaction, inside which will be a list of
|
|
||||||
PDUs that encode the state.
|
|
||||||
|
|
||||||
|
|
||||||
To backfill events on a given context:
|
|
||||||
|
|
||||||
GET .../backfill/:context/
|
|
||||||
Query args: v, limit
|
|
||||||
|
|
||||||
Response: JSON encoding of a single Transaction containing multiple PDUs
|
|
||||||
|
|
||||||
Retrieves a sliding-window history of previous PDUs that occurred on the
|
|
||||||
given context. Starting from the PDU ID(s) given in the "v" argument, the
|
|
||||||
PDUs that preceeded it are retrieved, up to a total number given by the
|
|
||||||
"limit" argument. These are then returned in a new Transaction containing all
|
|
||||||
off the PDUs.
|
|
||||||
|
|
||||||
|
|
||||||
To stream events all the events:
|
|
||||||
|
|
||||||
GET .../pull/
|
|
||||||
Query args: origin, v
|
|
||||||
|
|
||||||
Response: JSON encoding of a single Transaction consisting of multiple PDUs
|
|
||||||
|
|
||||||
Retrieves all of the transactions later than any version given by the "v"
|
|
||||||
arguments. [[TODO(paul): I'm not sure what the "origin" argument does because
|
|
||||||
I think at some point in the code it's got swapped around.]]
|
|
||||||
|
|
||||||
|
|
||||||
To make a query:
|
|
||||||
|
|
||||||
GET .../query/:query_type
|
|
||||||
Query args: as specified by the individual query types
|
|
||||||
|
|
||||||
Response: JSON encoding of a response object
|
|
||||||
|
|
||||||
Performs a single query request on the receiving home server. The Query Type
|
|
||||||
part of the path specifies the kind of query being made, and its query
|
|
||||||
arguments have a meaning specific to that kind of query. The response is a
|
|
||||||
JSON-encoded object whose meaning also depends on the kind of query.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
Versioning is, like, hard for backfilling backwards because of the number of Home Servers involved.
|
|
||||||
|
|
||||||
The way we solve this is by doing versioning as an acyclic directed graph of PDUs. For backfilling purposes, this is done on a per context basis.
|
|
||||||
When we send a PDU we include all PDUs that have been received for that context that hasn't been subsequently listed in a later PDU. The trivial case is a simple list of PDUs, e.g. A <- B <- C. However, if two servers send out a PDU at the same to, both B and C would point at A - a later PDU would then list both B and C.
|
|
||||||
|
|
||||||
Problems with opaque version strings:
|
|
||||||
- How do you do clustering without mandating that a cluster can only have one transaction in flight to a given remote home server at a time.
|
|
||||||
If you have multiple transactions sent at once, then you might drop one transaction, receive another with a version that is later than the dropped transaction and which point ARGH WE LOST A TRANSACTION.
|
|
||||||
- How do you do backfilling? A version string defines a point in a stream w.r.t. a single home server, not a point in the context.
|
|
||||||
|
|
||||||
We only need to store the ends of the directed graph, we DO NOT need to do the whole one table of nodes and one of edges.
|
|
||||||
@@ -1,839 +0,0 @@
|
|||||||
Matrix Specification
|
|
||||||
====================
|
|
||||||
|
|
||||||
TODO(Introduction) : Matthew
|
|
||||||
- Similar to intro paragraph from README.
|
|
||||||
- Explaining the overall mission, what this spec describes...
|
|
||||||
- "What is Matrix?"
|
|
||||||
- Draw parallels with email?
|
|
||||||
|
|
||||||
Architecture
|
|
||||||
============
|
|
||||||
|
|
||||||
Clients transmit data to other clients through home servers (HSes). Clients do not communicate with each
|
|
||||||
other directly.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
How data flows between clients
|
|
||||||
==============================
|
|
||||||
|
|
||||||
{ Matrix client A } { Matrix client B }
|
|
||||||
^ | ^ |
|
|
||||||
| events | | events |
|
|
||||||
| V | V
|
|
||||||
+------------------+ +------------------+
|
|
||||||
| |---------( HTTP )---------->| |
|
|
||||||
| Home Server | | Home Server |
|
|
||||||
| |<--------( HTTP )-----------| |
|
|
||||||
+------------------+ Federation +------------------+
|
|
||||||
|
|
||||||
A "Client" is an end-user, typically a human using a web application or mobile app. Clients use the
|
|
||||||
"Client-to-Server" (C-S) API to communicate with their home server. A single Client is usually
|
|
||||||
responsible for a single user account. A user account is represented by their "User ID". This ID is
|
|
||||||
namespaced to the home server which allocated the account and looks like::
|
|
||||||
|
|
||||||
@localpart:domain
|
|
||||||
|
|
||||||
The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user.
|
|
||||||
|
|
||||||
|
|
||||||
A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes.
|
|
||||||
It is typically responsible for multiple clients. "Federation" is the term used to describe the
|
|
||||||
sharing of data between two or more home servers.
|
|
||||||
|
|
||||||
Data in Matrix is encapsulated in an "Event". An event is an action within the system. Typically each
|
|
||||||
action (e.g. sending a message) correlates with exactly one event. Each event has a ``type`` which is
|
|
||||||
used to differentiate different kinds of data. ``type`` values SHOULD be namespaced according to standard
|
|
||||||
Java package naming conventions, e.g. ``com.example.myapp.event``. Events are usually sent in the context
|
|
||||||
of a "Room".
|
|
||||||
|
|
||||||
Room structure
|
|
||||||
--------------
|
|
||||||
|
|
||||||
A room is a conceptual place where users can send and receive events. Rooms
|
|
||||||
can be created, joined and left. Events are sent to a room, and all
|
|
||||||
participants in that room will receive the event. Rooms are uniquely
|
|
||||||
identified via a "Room ID", which look like::
|
|
||||||
|
|
||||||
!opaque_id:domain
|
|
||||||
|
|
||||||
There is exactly one room ID for each room. Whilst the room ID does contain a
|
|
||||||
domain, it is simply for namespacing room IDs. The room does NOT reside on the
|
|
||||||
domain specified. Room IDs are not meant to be human readable.
|
|
||||||
|
|
||||||
The following diagram shows an ``m.room.message`` event being sent in the room
|
|
||||||
``!qporfwt:matrix.org``::
|
|
||||||
|
|
||||||
{ @alice:matrix.org } { @bob:domain.com }
|
|
||||||
| ^
|
|
||||||
| |
|
|
||||||
Room ID: !qporfwt:matrix.org Room ID: !qporfwt:matrix.org
|
|
||||||
Event type: m.room.message Event type: m.room.message
|
|
||||||
Content: { JSON object } Content: { JSON object }
|
|
||||||
| |
|
|
||||||
V |
|
|
||||||
+------------------+ +------------------+
|
|
||||||
| Home Server | | Home Server |
|
|
||||||
| matrix.org |<-------Federation------->| domain.com |
|
|
||||||
+------------------+ +------------------+
|
|
||||||
| ................................. |
|
|
||||||
|______| Partially Shared State |_______|
|
|
||||||
| Room ID: !qporfwt:matrix.org |
|
|
||||||
| Servers: matrix.org, domain.com |
|
|
||||||
| Members: |
|
|
||||||
| - @alice:matrix.org |
|
|
||||||
| - @bob:domain.com |
|
|
||||||
|.................................|
|
|
||||||
|
|
||||||
Federation maintains shared state between multiple home servers, such that when an event is
|
|
||||||
sent to a room, the home server knows where to forward the event on to, and how to process
|
|
||||||
the event. Home servers do not need to have completely shared state in order to participate
|
|
||||||
in a room. State is scoped to a single room, and federation ensures that all home servers
|
|
||||||
have the information they need, even if that means the home server has to request more
|
|
||||||
information from another home server before processing the event.
|
|
||||||
|
|
||||||
Room Aliases
|
|
||||||
------------
|
|
||||||
|
|
||||||
Each room can also have multiple "Room Aliases", which looks like::
|
|
||||||
|
|
||||||
#room_alias:domain
|
|
||||||
|
|
||||||
A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained
|
|
||||||
by visiting the domain specified. Room aliases are designed to be human readable strings
|
|
||||||
which can be used to publicise rooms. Note that the mapping from a room alias to a
|
|
||||||
room ID is not fixed, and may change over time to point to a different room ID. For this
|
|
||||||
reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on
|
|
||||||
subsequent requests.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
GET
|
|
||||||
#matrix:domain.com !aaabaa:matrix.org
|
|
||||||
| ^
|
|
||||||
| |
|
|
||||||
_______V____________________|____
|
|
||||||
| domain.com |
|
|
||||||
| Mappings: |
|
|
||||||
| #matrix >> !aaabaa:matrix.org |
|
|
||||||
| #golf >> !wfeiofh:sport.com |
|
|
||||||
| #bike >> !4rguxf:matrix.org |
|
|
||||||
|________________________________|
|
|
||||||
|
|
||||||
|
|
||||||
Identity
|
|
||||||
--------
|
|
||||||
- Identity in relation to 3PIDs. Discovery of users based on 3PIDs.
|
|
||||||
- Identity servers; trusted clique of servers which replicate content.
|
|
||||||
- They govern the mapping of 3PIDs to user IDs and the creation of said mappings.
|
|
||||||
- Not strictly required in order to communicate.
|
|
||||||
|
|
||||||
|
|
||||||
API Standards
|
|
||||||
-------------
|
|
||||||
- All HTTP[S]
|
|
||||||
- Uses JSON as HTTP bodies
|
|
||||||
- Standard error response format { errcode: M_WHATEVER, error: "some message" }
|
|
||||||
- C-S API provides POST for operations, or PUT with txn IDs. Explain txn IDs.
|
|
||||||
|
|
||||||
Receiving live updates on a client
|
|
||||||
----------------------------------
|
|
||||||
- C-S longpoll event stream
|
|
||||||
- Concept of start/end tokens.
|
|
||||||
- Mention /initialSync to get token.
|
|
||||||
|
|
||||||
|
|
||||||
Rooms
|
|
||||||
=====
|
|
||||||
- How are they created? PDU anchor point: "root of the tree".
|
|
||||||
- Adding / removing aliases.
|
|
||||||
- Invite/join dance
|
|
||||||
- State and non-state data (+extensibility)
|
|
||||||
|
|
||||||
TODO : Room permissions / config / power levels.
|
|
||||||
|
|
||||||
Messages
|
|
||||||
========
|
|
||||||
|
|
||||||
This specification outlines several standard event types, all of which are
|
|
||||||
prefixed with ``m.``
|
|
||||||
|
|
||||||
State messages
|
|
||||||
--------------
|
|
||||||
- m.room.name
|
|
||||||
- m.room.topic
|
|
||||||
- m.room.member
|
|
||||||
- m.room.config
|
|
||||||
- m.room.invite_join
|
|
||||||
|
|
||||||
What are they, when are they used, what do they contain, how should they be used
|
|
||||||
|
|
||||||
Non-state messages
|
|
||||||
------------------
|
|
||||||
- m.room.message
|
|
||||||
- m.room.message.feedback (and compressed format)
|
|
||||||
|
|
||||||
What are they, when are they used, what do they contain, how should they be used
|
|
||||||
|
|
||||||
m.room.message msgtypes
|
|
||||||
-----------------------
|
|
||||||
Each ``m.room.message`` MUST have a ``msgtype`` key which identifies the type of
|
|
||||||
message being sent. Each type has their own required and optional keys, as outlined
|
|
||||||
below:
|
|
||||||
|
|
||||||
``m.text``
|
|
||||||
Required keys:
|
|
||||||
- ``body`` : "string" - The body of the message.
|
|
||||||
Optional keys:
|
|
||||||
None.
|
|
||||||
Example:
|
|
||||||
``{ "msgtype": "m.text", "body": "I am a fish" }``
|
|
||||||
|
|
||||||
``m.emote``
|
|
||||||
Required keys:
|
|
||||||
- ``body`` : "string" - The emote action to perform.
|
|
||||||
Optional keys:
|
|
||||||
None.
|
|
||||||
Example:
|
|
||||||
``{ "msgtype": "m.emote", "body": "tries to come up with a witty explanation" }``
|
|
||||||
|
|
||||||
``m.image``
|
|
||||||
Required keys:
|
|
||||||
- ``url`` : "string" - The URL to the image.
|
|
||||||
Optional keys:
|
|
||||||
- ``info`` : "string" - info : JSON object (ImageInfo) - The image info for image
|
|
||||||
referred to in ``url``.
|
|
||||||
- ``thumbnail_url`` : "string" - The URL to the thumbnail.
|
|
||||||
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
|
|
||||||
referred to in ``thumbnail_url``.
|
|
||||||
- ``body`` : "string" - The alt text of the image, or some kind of content
|
|
||||||
description for accessibility e.g. "image attachment".
|
|
||||||
|
|
||||||
ImageInfo:
|
|
||||||
Information about an image::
|
|
||||||
|
|
||||||
{
|
|
||||||
"size" : integer (size of image in bytes),
|
|
||||||
"w" : integer (width of image in pixels),
|
|
||||||
"h" : integer (height of image in pixels),
|
|
||||||
"mimetype" : "string (e.g. image/jpeg)",
|
|
||||||
}
|
|
||||||
|
|
||||||
``m.audio``
|
|
||||||
Required keys:
|
|
||||||
- ``url`` : "string" - The URL to the audio.
|
|
||||||
Optional keys:
|
|
||||||
- ``info`` : JSON object (AudioInfo) - The audio info for the audio referred to in
|
|
||||||
``url``.
|
|
||||||
- ``body`` : "string" - A description of the audio e.g. "Bee Gees -
|
|
||||||
Stayin' Alive", or some kind of content description for accessibility e.g.
|
|
||||||
"audio attachment".
|
|
||||||
AudioInfo:
|
|
||||||
Information about a piece of audio::
|
|
||||||
|
|
||||||
{
|
|
||||||
"mimetype" : "string (e.g. audio/aac)",
|
|
||||||
"size" : integer (size of audio in bytes),
|
|
||||||
"duration" : integer (duration of audio in milliseconds),
|
|
||||||
}
|
|
||||||
|
|
||||||
``m.video``
|
|
||||||
Required keys:
|
|
||||||
- ``url`` : "string" - The URL to the video.
|
|
||||||
Optional keys:
|
|
||||||
- ``info`` : JSON object (VideoInfo) - The video info for the video referred to in
|
|
||||||
``url``.
|
|
||||||
- ``body`` : "string" - A description of the video e.g. "Gangnam style",
|
|
||||||
or some kind of content description for accessibility e.g. "video attachment".
|
|
||||||
|
|
||||||
VideoInfo:
|
|
||||||
Information about a video::
|
|
||||||
|
|
||||||
{
|
|
||||||
"mimetype" : "string (e.g. video/mp4)",
|
|
||||||
"size" : integer (size of video in bytes),
|
|
||||||
"duration" : integer (duration of video in milliseconds),
|
|
||||||
"w" : integer (width of video in pixels),
|
|
||||||
"h" : integer (height of video in pixels),
|
|
||||||
"thumbnail_url" : "string (URL to image)",
|
|
||||||
"thumbanil_info" : JSON object (ImageInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
``m.location``
|
|
||||||
Required keys:
|
|
||||||
- ``geo_uri`` : "string" - The geo URI representing the location.
|
|
||||||
Optional keys:
|
|
||||||
- ``thumbnail_url`` : "string" - The URL to a thumnail of the location being
|
|
||||||
represented.
|
|
||||||
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
|
|
||||||
referred to in ``thumbnail_url``.
|
|
||||||
- ``body`` : "string" - A description of the location e.g. "Big Ben,
|
|
||||||
London, UK", or some kind of content description for accessibility e.g.
|
|
||||||
"location attachment".
|
|
||||||
|
|
||||||
The following keys can be attached to any ``m.room.message``:
|
|
||||||
|
|
||||||
Optional keys:
|
|
||||||
- ``sender_ts`` : integer - A timestamp (ms resolution) representing the
|
|
||||||
wall-clock time when the message was sent from the client.
|
|
||||||
|
|
||||||
Presence
|
|
||||||
========
|
|
||||||
|
|
||||||
Each user has the concept of presence information. This encodes the
|
|
||||||
"availability" of that user, suitable for display on other user's clients. This
|
|
||||||
is transmitted as an ``m.presence`` event and is one of the few events which
|
|
||||||
are sent *outside the context of a room*. The basic piece of presence information
|
|
||||||
is represented by the ``state`` key, which is an enum of one of the following:
|
|
||||||
|
|
||||||
- ``online`` : The default state when the user is connected to an event stream.
|
|
||||||
- ``unavailable`` : The user is not reachable at this time.
|
|
||||||
- ``offline`` : The user is not connected to an event stream.
|
|
||||||
- ``free_for_chat`` : The user is generally willing to receive messages
|
|
||||||
moreso than default.
|
|
||||||
- ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
|
|
||||||
state anyway and generally interact with client features.
|
|
||||||
|
|
||||||
This basic ``state`` field applies to the user as a whole, regardless of how many
|
|
||||||
client devices they have connected. The home server should synchronise this
|
|
||||||
status choice among multiple devices to ensure the user gets a consistent
|
|
||||||
experience.
|
|
||||||
|
|
||||||
Idle Time
|
|
||||||
---------
|
|
||||||
As well as the basic ``state`` field, the presence information can also show a sense
|
|
||||||
of an "idle timer". This should be maintained individually by the user's
|
|
||||||
clients, and the home server can take the highest reported time as that to
|
|
||||||
report. When a user is offline, the home server can still report when the user was last
|
|
||||||
seen online.
|
|
||||||
|
|
||||||
Transmission
|
|
||||||
------------
|
|
||||||
- Transmitted as an EDU.
|
|
||||||
- Presence lists determine who to send to.
|
|
||||||
|
|
||||||
Presence List
|
|
||||||
-------------
|
|
||||||
Each user's home server stores a "presence list" for that user. This stores a
|
|
||||||
list of other user IDs the user has chosen to add to it. To be added to this
|
|
||||||
list, the user being added must receive permission from the list owner. Once
|
|
||||||
granted, both user's HS(es) store this information. Since such subscriptions
|
|
||||||
are likely to be bidirectional, HSes may wish to automatically accept requests
|
|
||||||
when a reverse subscription already exists.
|
|
||||||
|
|
||||||
Presence and Permissions
|
|
||||||
------------------------
|
|
||||||
For a viewing user to be allowed to see the presence information of a target
|
|
||||||
user, either:
|
|
||||||
|
|
||||||
- The target user has allowed the viewing user to add them to their presence
|
|
||||||
list, or
|
|
||||||
- The two users share at least one room in common
|
|
||||||
|
|
||||||
In the latter case, this allows for clients to display some minimal sense of
|
|
||||||
presence information in a user list for a room.
|
|
||||||
|
|
||||||
Typing notifications
|
|
||||||
====================
|
|
||||||
|
|
||||||
TODO : Leo
|
|
||||||
|
|
||||||
Voice over IP
|
|
||||||
=============
|
|
||||||
|
|
||||||
TODO : Dave
|
|
||||||
|
|
||||||
Profiles
|
|
||||||
========
|
|
||||||
|
|
||||||
Internally within Matrix users are referred to by their user ID, which is not a
|
|
||||||
human-friendly string. Profiles grant users the ability to see human-readable
|
|
||||||
names for other users that are in some way meaningful to them. Additionally,
|
|
||||||
profiles can publish additional information, such as the user's age or location.
|
|
||||||
|
|
||||||
A Profile consists of a display name, an avatar picture, and a set of other
|
|
||||||
metadata fields that the user may wish to publish (email address, phone
|
|
||||||
numbers, website URLs, etc...). This specification puts no requirements on the
|
|
||||||
display name other than it being a valid unicode string.
|
|
||||||
|
|
||||||
- Metadata extensibility
|
|
||||||
- Bundled with which events? e.g. m.room.member
|
|
||||||
- Generate own events? What type?
|
|
||||||
|
|
||||||
Registration and login
|
|
||||||
======================
|
|
||||||
|
|
||||||
Clients must register with a home server in order to use Matrix. After
|
|
||||||
registering, the client will be given an access token which must be used in ALL
|
|
||||||
requests to that home server as a query parameter 'access_token'.
|
|
||||||
|
|
||||||
- TODO Kegan : Make registration like login (just omit the "user" key on the
|
|
||||||
initial request?)
|
|
||||||
|
|
||||||
If the client has already registered, they need to be able to login to their
|
|
||||||
account. The home server may provide many different ways of logging in, such
|
|
||||||
as user/password auth, login via a social network (OAuth2), login by confirming
|
|
||||||
a token sent to their email address, etc. This specification does not define how
|
|
||||||
home servers should authorise their users who want to login to their existing
|
|
||||||
accounts, but instead defines the standard interface which implementations
|
|
||||||
should follow so that ANY client can login to ANY home server.
|
|
||||||
|
|
||||||
The login process breaks down into the following:
|
|
||||||
1. Determine the requirements for logging in.
|
|
||||||
2. Submit the login stage credentials.
|
|
||||||
3. Get credentials or be told the next stage in the login process and repeat
|
|
||||||
step 2.
|
|
||||||
|
|
||||||
As each home server may have different ways of logging in, the client needs to know how
|
|
||||||
they should login. All distinct login stages MUST have a corresponding ``type``.
|
|
||||||
A ``type`` is a namespaced string which details the mechanism for logging in.
|
|
||||||
|
|
||||||
A client may be able to login via multiple valid login flows, and should choose a single
|
|
||||||
flow when logging in. A flow is a series of login stages. The home server MUST respond
|
|
||||||
with all the valid login flows when requested::
|
|
||||||
|
|
||||||
The client can login via 3 paths: 1a and 1b, 2a and 2b, or 3. The client should
|
|
||||||
select one of these paths.
|
|
||||||
|
|
||||||
{
|
|
||||||
"flows": [
|
|
||||||
{
|
|
||||||
"type": "<login type1a>",
|
|
||||||
"stages": [ "<login type 1a>", "<login type 1b>" ]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "<login type2a>",
|
|
||||||
"stages": [ "<login type 2a>", "<login type 2b>" ]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "<login type3>"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
After the login is completed, the client's fully-qualified user ID and a new access
|
|
||||||
token MUST be returned::
|
|
||||||
|
|
||||||
{
|
|
||||||
"user_id": "@user:matrix.org",
|
|
||||||
"access_token": "abcdef0123456789"
|
|
||||||
}
|
|
||||||
|
|
||||||
The ``user_id`` key is particularly useful if the home server wishes to support
|
|
||||||
localpart entry of usernames (e.g. "user" rather than "@user:matrix.org"), as the
|
|
||||||
client may not be able to determine its ``user_id`` in this case.
|
|
||||||
|
|
||||||
If a login has multiple requests, the home server may wish to create a session. If
|
|
||||||
a home server responds with a 'session' key to a request, clients MUST submit it in
|
|
||||||
subsequent requests until the login is completed::
|
|
||||||
|
|
||||||
{
|
|
||||||
"session": "<session id>"
|
|
||||||
}
|
|
||||||
|
|
||||||
This specification defines the following login types:
|
|
||||||
- ``m.login.password``
|
|
||||||
- ``m.login.oauth2``
|
|
||||||
- ``m.login.email.code``
|
|
||||||
- ``m.login.email.url``
|
|
||||||
|
|
||||||
|
|
||||||
Password-based
|
|
||||||
--------------
|
|
||||||
:Type:
|
|
||||||
m.login.password
|
|
||||||
:Description:
|
|
||||||
Login is supported via a username and password.
|
|
||||||
|
|
||||||
To respond to this type, reply with::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.password",
|
|
||||||
"user": "<user_id or user localpart>",
|
|
||||||
"password": "<password>"
|
|
||||||
}
|
|
||||||
|
|
||||||
The home server MUST respond with either new credentials, the next stage of the login
|
|
||||||
process, or a standard error response.
|
|
||||||
|
|
||||||
OAuth2-based
|
|
||||||
------------
|
|
||||||
:Type:
|
|
||||||
m.login.oauth2
|
|
||||||
:Description:
|
|
||||||
Login is supported via OAuth2 URLs. This login consists of multiple requests.
|
|
||||||
|
|
||||||
To respond to this type, reply with::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.oauth2",
|
|
||||||
"user": "<user_id or user localpart>"
|
|
||||||
}
|
|
||||||
|
|
||||||
The server MUST respond with::
|
|
||||||
|
|
||||||
{
|
|
||||||
"uri": <Authorization Request URI OR service selection URI>
|
|
||||||
}
|
|
||||||
|
|
||||||
The home server acts as a 'confidential' client for the purposes of OAuth2.
|
|
||||||
If the uri is a ``sevice selection URI``, it MUST point to a webpage which prompts the
|
|
||||||
user to choose which service to authorize with. On selection of a service, this
|
|
||||||
MUST link through to an ``Authorization Request URI``. If there is only 1 service which the
|
|
||||||
home server accepts when logging in, this indirection can be skipped and the
|
|
||||||
"uri" key can be the ``Authorization Request URI``.
|
|
||||||
|
|
||||||
The client then visits the ``Authorization Request URI``, which then shows the OAuth2
|
|
||||||
Allow/Deny prompt. Hitting 'Allow' returns the ``redirect URI`` with the auth code.
|
|
||||||
Home servers can choose any path for the ``redirect URI``. The client should visit
|
|
||||||
the ``redirect URI``, which will then finish the OAuth2 login process, granting the
|
|
||||||
home server an access token for the chosen service. When the home server gets
|
|
||||||
this access token, it verifies that the cilent has authorised with the 3rd party, and
|
|
||||||
can now complete the login. The OAuth2 ``redirect URI`` (with auth code) MUST respond
|
|
||||||
with either new credentials, the next stage of the login process, or a standard error
|
|
||||||
response.
|
|
||||||
|
|
||||||
For example, if a home server accepts OAuth2 from Google, it would return the
|
|
||||||
Authorization Request URI for Google::
|
|
||||||
|
|
||||||
{
|
|
||||||
"uri": "https://accounts.google.com/o/oauth2/auth?response_type=code&
|
|
||||||
client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos"
|
|
||||||
}
|
|
||||||
|
|
||||||
The client then visits this URI and authorizes the home server. The client then
|
|
||||||
visits the REDIRECT_URI with the auth code= query parameter which returns::
|
|
||||||
|
|
||||||
{
|
|
||||||
"user_id": "@user:matrix.org",
|
|
||||||
"access_token": "0123456789abcdef"
|
|
||||||
}
|
|
||||||
|
|
||||||
Email-based (code)
|
|
||||||
------------------
|
|
||||||
:Type:
|
|
||||||
m.login.email.code
|
|
||||||
:Description:
|
|
||||||
Login is supported by typing in a code which is sent in an email. This login
|
|
||||||
consists of multiple requests.
|
|
||||||
|
|
||||||
To respond to this type, reply with::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.email.code",
|
|
||||||
"user": "<user_id or user localpart>",
|
|
||||||
"email": "<email address>"
|
|
||||||
}
|
|
||||||
|
|
||||||
After validating the email address, the home server MUST send an email containing
|
|
||||||
an authentication code and return::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.email.code",
|
|
||||||
"session": "<session id>"
|
|
||||||
}
|
|
||||||
|
|
||||||
The second request in this login stage involves sending this authentication code::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.email.code",
|
|
||||||
"session": "<session id>",
|
|
||||||
"code": "<code in email sent>"
|
|
||||||
}
|
|
||||||
|
|
||||||
The home server MUST respond to this with either new credentials, the next stage of
|
|
||||||
the login process, or a standard error response.
|
|
||||||
|
|
||||||
Email-based (url)
|
|
||||||
-----------------
|
|
||||||
:Type:
|
|
||||||
m.login.email.url
|
|
||||||
:Description:
|
|
||||||
Login is supported by clicking on a URL in an email. This login consists of
|
|
||||||
multiple requests.
|
|
||||||
|
|
||||||
To respond to this type, reply with::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.email.url",
|
|
||||||
"user": "<user_id or user localpart>",
|
|
||||||
"email": "<email address>"
|
|
||||||
}
|
|
||||||
|
|
||||||
After validating the email address, the home server MUST send an email containing
|
|
||||||
an authentication URL and return::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.email.url",
|
|
||||||
"session": "<session id>"
|
|
||||||
}
|
|
||||||
|
|
||||||
The email contains a URL which must be clicked. After it has been clicked, the
|
|
||||||
client should perform another request::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.email.url",
|
|
||||||
"session": "<session id>"
|
|
||||||
}
|
|
||||||
|
|
||||||
The home server MUST respond to this with either new credentials, the next stage of
|
|
||||||
the login process, or a standard error response.
|
|
||||||
|
|
||||||
A common client implementation will be to periodically poll until the link is clicked.
|
|
||||||
If the link has not been visited yet, a standard error response with an errcode of
|
|
||||||
``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned.
|
|
||||||
|
|
||||||
|
|
||||||
N-Factor Authentication
|
|
||||||
-----------------------
|
|
||||||
Multiple login stages can be combined to create N-factor authentication during login.
|
|
||||||
|
|
||||||
This can be achieved by responding with the ``next`` login type on completion of a
|
|
||||||
previous login stage::
|
|
||||||
|
|
||||||
{
|
|
||||||
"next": "<next login type>"
|
|
||||||
}
|
|
||||||
|
|
||||||
If a home server implements N-factor authentication, it MUST respond with all
|
|
||||||
``stages`` when initially queried for their login requirements::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "<1st login type>",
|
|
||||||
"stages": [ <1st login type>, <2nd login type>, ... , <Nth login type> ]
|
|
||||||
}
|
|
||||||
|
|
||||||
This can be represented conceptually as::
|
|
||||||
|
|
||||||
_______________________
|
|
||||||
| Login Stage 1 |
|
|
||||||
| type: "<login type1>" |
|
|
||||||
| ___________________ |
|
|
||||||
| |_Request_1_________| | <-- Returns "session" key which is used throughout.
|
|
||||||
| ___________________ |
|
|
||||||
| |_Request_2_________| | <-- Returns a "next" value of "login type2"
|
|
||||||
|_______________________|
|
|
||||||
|
|
|
||||||
|
|
|
||||||
_________V_____________
|
|
||||||
| Login Stage 2 |
|
|
||||||
| type: "<login type2>" |
|
|
||||||
| ___________________ |
|
|
||||||
| |_Request_1_________| |
|
|
||||||
| ___________________ |
|
|
||||||
| |_Request_2_________| |
|
|
||||||
| ___________________ |
|
|
||||||
| |_Request_3_________| | <-- Returns a "next" value of "login type3"
|
|
||||||
|_______________________|
|
|
||||||
|
|
|
||||||
|
|
|
||||||
_________V_____________
|
|
||||||
| Login Stage 3 |
|
|
||||||
| type: "<login type3>" |
|
|
||||||
| ___________________ |
|
|
||||||
| |_Request_1_________| | <-- Returns user credentials
|
|
||||||
|_______________________|
|
|
||||||
|
|
||||||
Fallback
|
|
||||||
--------
|
|
||||||
Clients cannot be expected to be able to know how to process every single
|
|
||||||
login type. If a client determines it does not know how to handle a given
|
|
||||||
login type, it should request a login fallback page::
|
|
||||||
|
|
||||||
GET matrix/client/api/v1/login/fallback
|
|
||||||
|
|
||||||
This MUST return an HTML page which can perform the entire login process.
|
|
||||||
|
|
||||||
Identity
|
|
||||||
========
|
|
||||||
|
|
||||||
TODO : Dave
|
|
||||||
- 3PIDs and identity server, functions
|
|
||||||
|
|
||||||
Federation
|
|
||||||
==========
|
|
||||||
|
|
||||||
Federation is the term used to describe how to communicate between Matrix home
|
|
||||||
servers. Federation is a mechanism by which two home servers can exchange
|
|
||||||
Matrix event messages, both as a real-time push of current events, and as a
|
|
||||||
historic fetching mechanism to synchronise past history for clients to view. It
|
|
||||||
uses HTTP connections between each pair of servers involved as the underlying
|
|
||||||
transport. Messages are exchanged between servers in real-time by active pushing
|
|
||||||
from each server's HTTP client into the server of the other. Queries to fetch
|
|
||||||
historic data for the purpose of back-filling scrollback buffers and the like
|
|
||||||
can also be performed.
|
|
||||||
|
|
||||||
There are three main kinds of communication that occur between home servers:
|
|
||||||
|
|
||||||
:Queries:
|
|
||||||
These are single request/response interactions between a given pair of
|
|
||||||
servers, initiated by one side sending an HTTP GET request to obtain some
|
|
||||||
information, and responded by the other. They are not persisted and contain
|
|
||||||
no long-term significant history. They simply request a snapshot state at the
|
|
||||||
instant the query is made.
|
|
||||||
|
|
||||||
:Ephemeral Data Units (EDUs):
|
|
||||||
These are notifications of events that are pushed from one home server to
|
|
||||||
another. They are not persisted and contain no long-term significant history,
|
|
||||||
nor does the receiving home server have to reply to them.
|
|
||||||
|
|
||||||
:Persisted Data Units (PDUs):
|
|
||||||
These are notifications of events that are broadcast from one home server to
|
|
||||||
any others that are interested in the same "context" (namely, a Room ID).
|
|
||||||
They are persisted to long-term storage and form the record of history for
|
|
||||||
that context.
|
|
||||||
|
|
||||||
EDUs and PDUs are further wrapped in an envelope called a Transaction, which is
|
|
||||||
transferred from the origin to the destination home server using an HTTP PUT request.
|
|
||||||
|
|
||||||
|
|
||||||
Transactions
|
|
||||||
------------
|
|
||||||
The transfer of EDUs and PDUs between home servers is performed by an exchange
|
|
||||||
of Transaction messages, which are encoded as JSON objects, passed over an
|
|
||||||
HTTP PUT request. A Transaction is meaningful only to the pair of home servers that
|
|
||||||
exchanged it; they are not globally-meaningful.
|
|
||||||
|
|
||||||
Each transaction has:
|
|
||||||
- An opaque transaction ID.
|
|
||||||
- A timestamp (UNIX epoch time in milliseconds) generated by its origin server.
|
|
||||||
- An origin and destination server name.
|
|
||||||
- A list of "previous IDs".
|
|
||||||
- A list of PDUs and EDUs - the actual message payload that the Transaction carries.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
{
|
|
||||||
"transaction_id":"916d630ea616342b42e98a3be0b74113",
|
|
||||||
"ts":1404835423000,
|
|
||||||
"origin":"red",
|
|
||||||
"destination":"blue",
|
|
||||||
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
|
|
||||||
"pdus":[...],
|
|
||||||
"edus":[...]
|
|
||||||
}
|
|
||||||
|
|
||||||
The ``prev_ids`` field contains a list of previous transaction IDs that
|
|
||||||
the ``origin`` server has sent to this ``destination``. Its purpose is to act as a
|
|
||||||
sequence checking mechanism - the destination server can check whether it has
|
|
||||||
successfully received that Transaction, or ask for a retransmission if not.
|
|
||||||
|
|
||||||
The ``pdus`` field of a transaction is a list, containing zero or more PDUs.[*]
|
|
||||||
Each PDU is itself a JSON object containing a number of keys, the exact details of
|
|
||||||
which will vary depending on the type of PDU. Similarly, the ``edus`` field is
|
|
||||||
another list containing the EDUs. This key may be entirely absent if there are
|
|
||||||
no EDUs to transfer.
|
|
||||||
|
|
||||||
(* Normally the PDU list will be non-empty, but the server should cope with
|
|
||||||
receiving an "empty" transaction, as this is useful for informing peers of other
|
|
||||||
transaction IDs they should be aware of. This effectively acts as a push
|
|
||||||
mechanism to encourage peers to continue to replicate content.)
|
|
||||||
|
|
||||||
PDUs and EDUs
|
|
||||||
-------------
|
|
||||||
|
|
||||||
All PDUs have:
|
|
||||||
- An ID
|
|
||||||
- A context
|
|
||||||
- A declaration of their type
|
|
||||||
- A list of other PDU IDs that have been seen recently on that context (regardless of which origin
|
|
||||||
sent them)
|
|
||||||
|
|
||||||
[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
|
|
||||||
[origin,ref] pair like the prev_pdus are]]
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
{
|
|
||||||
"pdu_id":"a4ecee13e2accdadf56c1025af232176",
|
|
||||||
"context":"#example.green",
|
|
||||||
"origin":"green",
|
|
||||||
"ts":1404838188000,
|
|
||||||
"pdu_type":"m.text",
|
|
||||||
"prev_pdus":[["blue","99d16afbc857975916f1d73e49e52b65"]],
|
|
||||||
"content":...
|
|
||||||
"is_state":false
|
|
||||||
}
|
|
||||||
|
|
||||||
In contrast to Transactions, it is important to note that the ``prev_pdus``
|
|
||||||
field of a PDU refers to PDUs that any origin server has sent, rather than
|
|
||||||
previous IDs that this ``origin`` has sent. This list may refer to other PDUs sent
|
|
||||||
by the same origin as the current one, or other origins.
|
|
||||||
|
|
||||||
Because of the distributed nature of participants in a Matrix conversation, it
|
|
||||||
is impossible to establish a globally-consistent total ordering on the events.
|
|
||||||
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
|
||||||
has received, a partial ordering can be constructed allowing causallity
|
|
||||||
relationships to be preserved. A client can then display these messages to the
|
|
||||||
end-user in some order consistent with their content and ensure that no message
|
|
||||||
that is semantically in reply of an earlier one is ever displayed before it.
|
|
||||||
|
|
||||||
PDUs fall into two main categories: those that deliver Events, and those that
|
|
||||||
synchronise State. For PDUs that relate to State synchronisation, additional
|
|
||||||
keys exist to support this:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
{...,
|
|
||||||
"is_state":true,
|
|
||||||
"state_key":TODO
|
|
||||||
"power_level":TODO
|
|
||||||
"prev_state_id":TODO
|
|
||||||
"prev_state_origin":TODO}
|
|
||||||
|
|
||||||
[[TODO(paul): At this point we should probably have a long description of how
|
|
||||||
State management works, with descriptions of clobbering rules, power levels, etc
|
|
||||||
etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
|
|
||||||
so on. This part needs refining. And writing in its own document as the details
|
|
||||||
relate to the server/system as a whole, not specifically to server-server
|
|
||||||
federation.]]
|
|
||||||
|
|
||||||
EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
|
|
||||||
"previous" IDs. The only mandatory fields for these are the type, origin and
|
|
||||||
destination home server names, and the actual nested content.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
{"edu_type":"m.presence",
|
|
||||||
"origin":"blue",
|
|
||||||
"destination":"orange",
|
|
||||||
"content":...}
|
|
||||||
|
|
||||||
Backfilling
|
|
||||||
-----------
|
|
||||||
- What it is, when is it used, how is it done
|
|
||||||
|
|
||||||
SRV Records
|
|
||||||
-----------
|
|
||||||
- Why it is needed
|
|
||||||
|
|
||||||
Security
|
|
||||||
========
|
|
||||||
- rate limiting
|
|
||||||
- crypto (s-s auth)
|
|
||||||
- E2E
|
|
||||||
- Lawful intercept + Key Escrow
|
|
||||||
|
|
||||||
TODO Mark
|
|
||||||
|
|
||||||
Policy Servers
|
|
||||||
==============
|
|
||||||
TODO
|
|
||||||
|
|
||||||
Content repository
|
|
||||||
==================
|
|
||||||
- thumbnail paths
|
|
||||||
|
|
||||||
Address book repository
|
|
||||||
=======================
|
|
||||||
- format
|
|
||||||
|
|
||||||
|
|
||||||
Glossary
|
|
||||||
========
|
|
||||||
- domain specific words/acronyms with definitions
|
|
||||||
|
|
||||||
User ID:
|
|
||||||
An opaque ID which identifies an end-user, which consists of some opaque
|
|
||||||
localpart combined with the domain name of their home server.
|
|
||||||
1
docs/sphinx/README.rst
Normal file
1
docs/sphinx/README.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
TODO: how (if at all) is this actually maintained?
|
||||||
93
docs/turn-howto.rst
Normal file
93
docs/turn-howto.rst
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
How to enable VoIP relaying on your Home Server with TURN
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
The synapse Matrix Home Server supports integration with TURN server via the
|
||||||
|
TURN server REST API
|
||||||
|
(http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00). This allows
|
||||||
|
the Home Server to generate credentials that are valid for use on the TURN
|
||||||
|
server through the use of a secret shared between the Home Server and the
|
||||||
|
TURN server.
|
||||||
|
|
||||||
|
This document described how to install coturn
|
||||||
|
(https://code.google.com/p/coturn/) which also supports the TURN REST API,
|
||||||
|
and integrate it with synapse.
|
||||||
|
|
||||||
|
coturn Setup
|
||||||
|
============
|
||||||
|
|
||||||
|
1. Check out coturn::
|
||||||
|
svn checkout http://coturn.googlecode.com/svn/trunk/ coturn
|
||||||
|
cd coturn
|
||||||
|
|
||||||
|
2. Configure it::
|
||||||
|
./configure
|
||||||
|
|
||||||
|
You may need to install libevent2: if so, you should do so
|
||||||
|
in the way recommended by your operating system.
|
||||||
|
You can ignore warnings about lack of database support: a
|
||||||
|
database is unnecessary for this purpose.
|
||||||
|
|
||||||
|
3. Build and install it::
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
|
||||||
|
4. Make a config file in /etc/turnserver.conf. You can customise
|
||||||
|
a config file from turnserver.conf.default. The relevant
|
||||||
|
lines, with example values, are::
|
||||||
|
|
||||||
|
lt-cred-mech
|
||||||
|
use-auth-secret
|
||||||
|
static-auth-secret=[your secret key here]
|
||||||
|
realm=turn.myserver.org
|
||||||
|
|
||||||
|
See turnserver.conf.default for explanations of the options.
|
||||||
|
One way to generate the static-auth-secret is with pwgen::
|
||||||
|
|
||||||
|
pwgen -s 64 1
|
||||||
|
|
||||||
|
5. Ensure youe firewall allows traffic into the TURN server on
|
||||||
|
the ports you've configured it to listen on (remember to allow
|
||||||
|
both TCP and UDP if you've enabled both).
|
||||||
|
|
||||||
|
6. If you've configured coturn to support TLS/DTLS, generate or
|
||||||
|
import your private key and certificate.
|
||||||
|
|
||||||
|
7. Start the turn server::
|
||||||
|
bin/turnserver -o
|
||||||
|
|
||||||
|
|
||||||
|
synapse Setup
|
||||||
|
=============
|
||||||
|
|
||||||
|
Your home server configuration file needs the following extra keys:
|
||||||
|
|
||||||
|
1. "turn_uris": This needs to be a yaml list
|
||||||
|
of public-facing URIs for your TURN server to be given out
|
||||||
|
to your clients. Add separate entries for each transport your
|
||||||
|
TURN server supports.
|
||||||
|
|
||||||
|
2. "turn_shared_secret": This is the secret shared between your Home
|
||||||
|
server and your TURN server, so you should set it to the same
|
||||||
|
string you used in turnserver.conf.
|
||||||
|
|
||||||
|
3. "turn_user_lifetime": This is the amount of time credentials
|
||||||
|
generated by your Home Server are valid for (in milliseconds).
|
||||||
|
Shorter times offer less potential for abuse at the expense
|
||||||
|
of increased traffic between web clients and your home server
|
||||||
|
to refresh credentials. The TURN REST API specification recommends
|
||||||
|
one day (86400000).
|
||||||
|
|
||||||
|
As an example, here is the relevant section of the config file for
|
||||||
|
matrix.org::
|
||||||
|
|
||||||
|
turn_uris: turn:turn.matrix.org:3478?transport=udp,turn:turn.matrix.org:3478?transport=tcp
|
||||||
|
turn_shared_secret: n0t4ctuAllymatr1Xd0TorgSshar3d5ecret4obvIousreAsons
|
||||||
|
turn_user_lifetime: 86400000
|
||||||
|
|
||||||
|
Now, restart synapse::
|
||||||
|
|
||||||
|
cd /where/you/run/synapse
|
||||||
|
./synctl restart
|
||||||
|
|
||||||
|
...and your Home Server now supports VoIP relaying!
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -120,7 +120,7 @@ def make_graph(pdus, room, filename_prefix):
|
|||||||
def get_pdus(host, room):
|
def get_pdus(host, room):
|
||||||
transaction = json.loads(
|
transaction = json.loads(
|
||||||
urllib2.urlopen(
|
urllib2.urlopen(
|
||||||
"http://%s/matrix/federation/v1/context/%s/" % (host, room)
|
"http://%s/_matrix/federation/v1/context/%s/" % (host, room)
|
||||||
).read()
|
).read()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8080</p>
|
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8008</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="loginForm">
|
<form class="loginForm">
|
||||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ $('.login').live('click', function() {
|
|||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -19,17 +19,23 @@ $('.login').live('click', function() {
|
|||||||
showLoggedIn(data);
|
showLoggedIn(data);
|
||||||
},
|
},
|
||||||
error: function(err) {
|
error: function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var getCurrentRoomList = function() {
|
var getCurrentRoomList = function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.length; ++i) {
|
var rooms = data.rooms;
|
||||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
addRoom(data[i]);
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
|
addRoom(rooms[i]);
|
||||||
}
|
}
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
@@ -43,7 +49,7 @@ $('.createRoom').live('click', function() {
|
|||||||
data.room_alias_name = roomAlias;
|
data.room_alias_name = roomAlias;
|
||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
@@ -78,11 +84,9 @@ $('.sendMessage').live('click', function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
|
||||||
url = url.replace("$msgid", msgId);
|
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
@@ -91,7 +95,7 @@ $('.sendMessage').live('click', function() {
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<p>This event stream demo requires a home server to be running on http://localhost:8080</p>
|
<p>This event stream demo requires a home server to be running on http://localhost:8008</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="loginForm">
|
<form class="loginForm">
|
||||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ var eventStreamInfo = {
|
|||||||
var roomInfo = [];
|
var roomInfo = [];
|
||||||
|
|
||||||
var longpollEventStream = function() {
|
var longpollEventStream = function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
|
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$from", eventStreamInfo.from);
|
url = url.replace("$from", eventStreamInfo.from);
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ $('.login').live('click', function() {
|
|||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -58,21 +58,27 @@ $('.login').live('click', function() {
|
|||||||
showLoggedIn(data);
|
showLoggedIn(data);
|
||||||
},
|
},
|
||||||
error: function(err) {
|
error: function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var getCurrentRoomList = function() {
|
var getCurrentRoomList = function() {
|
||||||
$("#roomId").val("");
|
$("#roomId").val("");
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.length; ++i) {
|
var rooms = data.rooms;
|
||||||
if ("messages" in data[i]) {
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
if ("messages" in rooms[i]) {
|
||||||
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomInfo = data;
|
roomInfo = rooms;
|
||||||
setRooms(roomInfo);
|
setRooms(roomInfo);
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
@@ -92,17 +98,14 @@ $('.sendMessage').live('click', function() {
|
|||||||
|
|
||||||
var sendMessage = function(roomId) {
|
var sendMessage = function(roomId) {
|
||||||
var body = "jsfiddle message @" + $.now();
|
var body = "jsfiddle message @" + $.now();
|
||||||
var msgId = $.now();
|
|
||||||
|
|
||||||
if (roomId.length === 0) {
|
if (roomId.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
|
||||||
url = url.replace("$msgid", msgId);
|
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
@@ -111,7 +114,7 @@ var sendMessage = function(roomId) {
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
|
|||||||
7
jsfiddles/example_app/demo.details
Normal file
7
jsfiddles/example_app/demo.details
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name: Example Matrix Client
|
||||||
|
description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists.
|
||||||
|
authors:
|
||||||
|
- matrix.org
|
||||||
|
resources:
|
||||||
|
- http://matrix.org
|
||||||
|
normalize_css: no
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="signUp">
|
<div class="signUp">
|
||||||
<p>Matrix example application: Requires a local home server running at http://localhost:8080</p>
|
<p>Matrix example application: Requires a local home server running at http://localhost:8008</p>
|
||||||
<form class="registrationForm">
|
<form class="registrationForm">
|
||||||
<p>No account? Register:</p>
|
<p>No account? Register:</p>
|
||||||
<input type="text" id="userReg" placeholder="Username"></input>
|
<input type="text" id="userReg" placeholder="Username"></input>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ var viewingRoomId;
|
|||||||
|
|
||||||
// ************** Event Streaming **************
|
// ************** Event Streaming **************
|
||||||
var longpollEventStream = function() {
|
var longpollEventStream = function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
|
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$from", eventStreamInfo.from);
|
url = url.replace("$from", eventStreamInfo.from);
|
||||||
|
|
||||||
@@ -38,8 +38,9 @@ var longpollEventStream = function() {
|
|||||||
else if (data.chunk[i].type === "m.room.member") {
|
else if (data.chunk[i].type === "m.room.member") {
|
||||||
if (viewingRoomId === data.chunk[i].room_id) {
|
if (viewingRoomId === data.chunk[i].room_id) {
|
||||||
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
|
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
|
||||||
|
addMessage(data.chunk[i]);
|
||||||
for (j=0; j<memberInfo.length; ++j) {
|
for (j=0; j<memberInfo.length; ++j) {
|
||||||
if (memberInfo[j].target_user_id === data.chunk[i].target_user_id) {
|
if (memberInfo[j].state_key === data.chunk[i].state_key) {
|
||||||
memberInfo[j] = data.chunk[i];
|
memberInfo[j] = data.chunk[i];
|
||||||
updatedMemberList = true;
|
updatedMemberList = true;
|
||||||
break;
|
break;
|
||||||
@@ -50,7 +51,7 @@ var longpollEventStream = function() {
|
|||||||
updatedMemberList = true;
|
updatedMemberList = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.chunk[i].target_user_id === accountInfo.user_id) {
|
if (data.chunk[i].state_key === accountInfo.user_id) {
|
||||||
getCurrentRoomList(); // update our join/invite list
|
getCurrentRoomList(); // update our join/invite list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +89,7 @@ $('.login').live('click', function() {
|
|||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -106,10 +107,10 @@ $('.register').live('click', function() {
|
|||||||
var user = $("#userReg").val();
|
var user = $("#userReg").val();
|
||||||
var password = $("#passwordReg").val();
|
var password = $("#passwordReg").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/register",
|
url: "http://localhost:8008/_matrix/client/api/v1/register",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user_id: user, password: password }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
onLoggedIn(data);
|
onLoggedIn(data);
|
||||||
@@ -133,7 +134,7 @@ $('.createRoom').live('click', function() {
|
|||||||
data.room_alias_name = roomAlias;
|
data.room_alias_name = roomAlias;
|
||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
@@ -154,14 +155,15 @@ $('.createRoom').live('click', function() {
|
|||||||
|
|
||||||
// ************** Getting current state **************
|
// ************** Getting current state **************
|
||||||
var getCurrentRoomList = function() {
|
var getCurrentRoomList = function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.length; ++i) {
|
var rooms = data.rooms;
|
||||||
if ("messages" in data[i]) {
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
if ("messages" in rooms[i]) {
|
||||||
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomInfo = data;
|
roomInfo = rooms;
|
||||||
setRooms(roomInfo);
|
setRooms(roomInfo);
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
@@ -179,7 +181,8 @@ var loadRoomContent = function(roomId) {
|
|||||||
|
|
||||||
var getMessages = function(roomId) {
|
var getMessages = function(roomId) {
|
||||||
$("#messages").empty();
|
$("#messages").empty();
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/messages/list?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=10";
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" +
|
||||||
|
encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=data.chunk.length-1; i>=0; --i) {
|
for (var i=data.chunk.length-1; i>=0; --i) {
|
||||||
addMessage(data.chunk[i]);
|
addMessage(data.chunk[i]);
|
||||||
@@ -190,7 +193,8 @@ var getMessages = function(roomId) {
|
|||||||
var getMemberList = function(roomId) {
|
var getMemberList = function(roomId) {
|
||||||
$("#members").empty();
|
$("#members").empty();
|
||||||
memberInfo = [];
|
memberInfo = [];
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/members/list?access_token=" + accountInfo.access_token;
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" +
|
||||||
|
encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.chunk.length; ++i) {
|
for (var i=0; i<data.chunk.length; ++i) {
|
||||||
memberInfo.push(data.chunk[i]);
|
memberInfo.push(data.chunk[i]);
|
||||||
@@ -212,11 +216,9 @@ $('.sendMessage').live('click', function() {
|
|||||||
var sendMessage = function(roomId, body) {
|
var sendMessage = function(roomId, body) {
|
||||||
var msgId = $.now();
|
var msgId = $.now();
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
|
||||||
url = url.replace("$msgid", msgId);
|
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
@@ -225,7 +227,7 @@ var sendMessage = function(roomId, body) {
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
@@ -260,13 +262,12 @@ var setRooms = function(roomList) {
|
|||||||
var membership = $(this).find('td:eq(1)').text();
|
var membership = $(this).find('td:eq(1)').text();
|
||||||
if (membership !== "join") {
|
if (membership !== "join") {
|
||||||
console.log("Joining room " + roomId);
|
console.log("Joining room " + roomId);
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token";
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({membership: "join"}),
|
data: JSON.stringify({membership: "join"}),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
@@ -286,16 +287,39 @@ var setRooms = function(roomList) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var addMessage = function(data) {
|
var addMessage = function(data) {
|
||||||
|
|
||||||
|
var msg = data.content.body;
|
||||||
|
if (data.type === "m.room.member") {
|
||||||
|
if (data.content.membership === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.content.membership === "invite") {
|
||||||
|
msg = "<em>invited " + data.state_key + " to the room</em>";
|
||||||
|
}
|
||||||
|
else if (data.content.membership === "join") {
|
||||||
|
msg = "<em>joined the room</em>";
|
||||||
|
}
|
||||||
|
else if (data.content.membership === "leave") {
|
||||||
|
msg = "<em>left the room</em>";
|
||||||
|
}
|
||||||
|
else if (data.content.membership === "ban") {
|
||||||
|
msg = "<em>was banned from the room</em>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var row = "<tr>" +
|
var row = "<tr>" +
|
||||||
"<td>"+data.user_id+"</td>" +
|
"<td>"+data.user_id+"</td>" +
|
||||||
"<td>"+data.content.body+"</td>" +
|
"<td>"+msg+"</td>" +
|
||||||
"</tr>";
|
"</tr>";
|
||||||
$("#messages").append(row);
|
$("#messages").append(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
var addMember = function(data) {
|
var addMember = function(data) {
|
||||||
var row = "<tr>" +
|
var row = "<tr>" +
|
||||||
"<td>"+data.target_user_id+"</td>" +
|
"<td>"+data.state_key+"</td>" +
|
||||||
"<td>"+data.content.membership+"</td>" +
|
"<td>"+data.content.membership+"</td>" +
|
||||||
"</tr>";
|
"</tr>";
|
||||||
$("#members").append(row);
|
$("#members").append(row);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<p>This registration/login demo requires a home server to be running on http://localhost:8080</p>
|
<p>This registration/login demo requires a home server to be running on http://localhost:8008</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="registrationForm">
|
<form class="registrationForm">
|
||||||
<input type="text" id="user" placeholder="Username"></input>
|
<input type="text" id="user" placeholder="Username"></input>
|
||||||
|
|||||||
@@ -11,23 +11,7 @@ $('.register').live('click', function() {
|
|||||||
var user = $("#user").val();
|
var user = $("#user").val();
|
||||||
var password = $("#password").val();
|
var password = $("#password").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/register",
|
url: "http://localhost:8008/_matrix/client/api/v1/register",
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
data: JSON.stringify({ user_id: user, password: password }),
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
showLoggedIn(data);
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var login = function(user, password) {
|
|
||||||
$.ajax({
|
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -36,7 +20,33 @@ var login = function(user, password) {
|
|||||||
showLoggedIn(data);
|
showLoggedIn(data);
|
||||||
},
|
},
|
||||||
error: function(err) {
|
error: function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var login = function(user, password) {
|
||||||
|
$.ajax({
|
||||||
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
showLoggedIn(data);
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -44,8 +54,8 @@ var login = function(user, password) {
|
|||||||
$('.login').live('click', function() {
|
$('.login').live('click', function() {
|
||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.getJSON("http://localhost:8080/matrix/client/api/v1/login", function(data) {
|
$.getJSON("http://localhost:8008/_matrix/client/api/v1/login", function(data) {
|
||||||
if (data.type !== "m.login.password") {
|
if (data.flows[0].type !== "m.login.password") {
|
||||||
alert("I don't know how to login with this type: " + data.type);
|
alert("I don't know how to login with this type: " + data.type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -60,7 +70,7 @@ $('.logout').live('click', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('.testToken').live('click', function() {
|
$('.testToken').live('click', function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
|
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<p>This room membership demo requires a home server to be running on http://localhost:8080</p>
|
<p>This room membership demo requires a home server to be running on http://localhost:8008</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="loginForm">
|
<form class="loginForm">
|
||||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
<input type="text" id="roomId" placeholder="Room ID"></input>
|
<input type="text" id="roomId" placeholder="Room ID"></input>
|
||||||
<input type="text" id="targetUser" placeholder="Target User ID"></input>
|
<input type="text" id="targetUser" placeholder="Target User ID"></input>
|
||||||
<select id="membership">
|
<select id="membership">
|
||||||
<option value="invite">Invite</option>
|
<option value="invite">invite</option>
|
||||||
<option value="join">Join</option>
|
<option value="join">join</option>
|
||||||
<option value="leave">Leave</option>
|
<option value="leave">leave</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="button" class="changeMembership" value="Change Membership"></input>
|
<input type="button" class="changeMembership" value="Change Membership"></input>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,13 +4,21 @@ var showLoggedIn = function(data) {
|
|||||||
accountInfo = data;
|
accountInfo = data;
|
||||||
getCurrentRoomList();
|
getCurrentRoomList();
|
||||||
$(".loggedin").css({visibility: "visible"});
|
$(".loggedin").css({visibility: "visible"});
|
||||||
|
$("#membership").change(function() {
|
||||||
|
if ($("#membership").val() === "invite") {
|
||||||
|
$("#targetUser").css({visibility: "visible"});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$("#targetUser").css({visibility: "hidden"});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$('.login').live('click', function() {
|
$('.login').live('click', function() {
|
||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -20,7 +28,12 @@ $('.login').live('click', function() {
|
|||||||
showLoggedIn(data);
|
showLoggedIn(data);
|
||||||
},
|
},
|
||||||
error: function(err) {
|
error: function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
var errMsg = "To try this, you need a home server running!";
|
||||||
|
var errJson = $.parseJSON(err.responseText);
|
||||||
|
if (errJson) {
|
||||||
|
errMsg = JSON.stringify(errJson);
|
||||||
|
}
|
||||||
|
alert(errMsg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -31,10 +44,11 @@ var getCurrentRoomList = function() {
|
|||||||
// solution but that is out of scope of this fiddle.
|
// solution but that is out of scope of this fiddle.
|
||||||
$("#rooms").find("tr:gt(0)").remove();
|
$("#rooms").find("tr:gt(0)").remove();
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.length; ++i) {
|
var rooms = data.rooms;
|
||||||
addRoom(data[i]);
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
|
addRoom(rooms[i]);
|
||||||
}
|
}
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
@@ -44,7 +58,7 @@ var getCurrentRoomList = function() {
|
|||||||
$('.createRoom').live('click', function() {
|
$('.createRoom').live('click', function() {
|
||||||
var data = {};
|
var data = {};
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
@@ -78,54 +92,42 @@ $('.changeMembership').live('click', function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token";
|
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(member));
|
url = url.replace("$membership", membership);
|
||||||
|
|
||||||
if (membership === "leave") {
|
var data = {};
|
||||||
$.ajax({
|
|
||||||
url: url,
|
if (membership === "invite") {
|
||||||
type: "DELETE",
|
data = {
|
||||||
contentType: "application/json; charset=utf-8",
|
user_id: member
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
getCurrentRoomList();
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var data = {
|
|
||||||
membership: membership
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: url,
|
|
||||||
type: "PUT",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
getCurrentRoomList();
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
contentType: "application/json; charset=utf-8",
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
dataType: "json",
|
||||||
|
success: function(data) {
|
||||||
|
getCurrentRoomList();
|
||||||
|
},
|
||||||
|
error: function(err) {
|
||||||
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.joinAlias').live('click', function() {
|
$('.joinAlias').live('click', function() {
|
||||||
var roomAlias = $("#roomAlias").val();
|
var roomAlias = $("#roomAlias").val();
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/join/$roomalias?access_token=$token";
|
var url = "http://localhost:8008/_matrix/client/api/v1/join/$roomalias?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
|
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({}),
|
data: JSON.stringify({}),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
|
|||||||
280
pylint.cfg
Normal file
280
pylint.cfg
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
[MASTER]
|
||||||
|
|
||||||
|
# Specify a configuration file.
|
||||||
|
#rcfile=
|
||||||
|
|
||||||
|
# Python code to execute, usually for sys.path manipulation such as
|
||||||
|
# pygtk.require().
|
||||||
|
#init-hook=
|
||||||
|
|
||||||
|
# Profiled execution.
|
||||||
|
profile=no
|
||||||
|
|
||||||
|
# Add files or directories to the blacklist. They should be base names, not
|
||||||
|
# paths.
|
||||||
|
ignore=CVS
|
||||||
|
|
||||||
|
# Pickle collected data for later comparisons.
|
||||||
|
persistent=yes
|
||||||
|
|
||||||
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
|
# usually to register additional checkers.
|
||||||
|
load-plugins=
|
||||||
|
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
|
||||||
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
# multiple time. See also the "--disable" option for examples.
|
||||||
|
#enable=
|
||||||
|
|
||||||
|
# Disable the message, report, category or checker with the given id(s). You
|
||||||
|
# can either give multiple identifiers separated by comma (,) or put this
|
||||||
|
# option multiple times (only on the command line, not in the configuration
|
||||||
|
# file where it should appear only once).You can also use "--disable=all" to
|
||||||
|
# disable everything first and then reenable specific checks. For example, if
|
||||||
|
# you want to run only the similarities checker, you can use "--disable=all
|
||||||
|
# --enable=similarities". If you want to run only the classes checker, but have
|
||||||
|
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||||
|
# --disable=W"
|
||||||
|
disable=missing-docstring
|
||||||
|
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
|
||||||
|
# Set the output format. Available formats are text, parseable, colorized, msvs
|
||||||
|
# (visual studio) and html. You can also give a reporter class, eg
|
||||||
|
# mypackage.mymodule.MyReporterClass.
|
||||||
|
output-format=text
|
||||||
|
|
||||||
|
# Put messages in a separate file for each module / package specified on the
|
||||||
|
# command line instead of printing them on stdout. Reports (if any) will be
|
||||||
|
# written in a file name "pylint_global.[txt|html]".
|
||||||
|
files-output=no
|
||||||
|
|
||||||
|
# Tells whether to display a full report or only the messages
|
||||||
|
reports=yes
|
||||||
|
|
||||||
|
# Python expression which should return a note less than 10 (10 is the highest
|
||||||
|
# note). You have access to the variables errors warning, statement which
|
||||||
|
# respectively contain the number of errors / warnings messages and the total
|
||||||
|
# number of statements analyzed. This is used by the global evaluation report
|
||||||
|
# (RP0004).
|
||||||
|
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||||
|
|
||||||
|
# Add a comment according to your evaluation note. This is used by the global
|
||||||
|
# evaluation report (RP0004).
|
||||||
|
comment=no
|
||||||
|
|
||||||
|
# Template used to display messages. This is a python new-style format string
|
||||||
|
# used to format the message information. See doc for all details
|
||||||
|
#msg-template=
|
||||||
|
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
|
||||||
|
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||||
|
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||||
|
ignore-mixin-members=yes
|
||||||
|
|
||||||
|
# List of classes names for which member attributes should not be checked
|
||||||
|
# (useful for classes with attributes dynamically set).
|
||||||
|
ignored-classes=SQLObject
|
||||||
|
|
||||||
|
# When zope mode is activated, add a predefined set of Zope acquired attributes
|
||||||
|
# to generated-members.
|
||||||
|
zope=no
|
||||||
|
|
||||||
|
# List of members which are set dynamically and missed by pylint inference
|
||||||
|
# system, and so shouldn't trigger E0201 when accessed. Python regular
|
||||||
|
# expressions are accepted.
|
||||||
|
generated-members=REQUEST,acl_users,aq_parent
|
||||||
|
|
||||||
|
|
||||||
|
[MISCELLANEOUS]
|
||||||
|
|
||||||
|
# List of note tags to take in consideration, separated by a comma.
|
||||||
|
notes=FIXME,XXX,TODO
|
||||||
|
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
|
||||||
|
# Minimum lines number of a similarity.
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
# Ignore comments when computing similarities.
|
||||||
|
ignore-comments=yes
|
||||||
|
|
||||||
|
# Ignore docstrings when computing similarities.
|
||||||
|
ignore-docstrings=yes
|
||||||
|
|
||||||
|
# Ignore imports when computing similarities.
|
||||||
|
ignore-imports=no
|
||||||
|
|
||||||
|
|
||||||
|
[VARIABLES]
|
||||||
|
|
||||||
|
# Tells whether we should check for unused import in __init__ files.
|
||||||
|
init-import=no
|
||||||
|
|
||||||
|
# A regular expression matching the beginning of the name of dummy variables
|
||||||
|
# (i.e. not used).
|
||||||
|
dummy-variables-rgx=_$|dummy
|
||||||
|
|
||||||
|
# List of additional names supposed to be defined in builtins. Remember that
|
||||||
|
# you should avoid to define new builtins when possible.
|
||||||
|
additional-builtins=
|
||||||
|
|
||||||
|
|
||||||
|
[BASIC]
|
||||||
|
|
||||||
|
# Required attributes for module, separated by a comma
|
||||||
|
required-attributes=
|
||||||
|
|
||||||
|
# List of builtins function names that should not be used, separated by a comma
|
||||||
|
bad-functions=map,filter,apply,input
|
||||||
|
|
||||||
|
# Regular expression which should only match correct module names
|
||||||
|
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct module level names
|
||||||
|
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct class names
|
||||||
|
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct function names
|
||||||
|
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct method names
|
||||||
|
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct instance attribute names
|
||||||
|
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct argument names
|
||||||
|
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct variable names
|
||||||
|
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct attribute names in class
|
||||||
|
# bodies
|
||||||
|
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||||
|
|
||||||
|
# Regular expression which should only match correct list comprehension /
|
||||||
|
# generator expression variable names
|
||||||
|
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||||
|
|
||||||
|
# Good variable names which should always be accepted, separated by a comma
|
||||||
|
good-names=i,j,k,ex,Run,_
|
||||||
|
|
||||||
|
# Bad variable names which should always be refused, separated by a comma
|
||||||
|
bad-names=foo,bar,baz,toto,tutu,tata
|
||||||
|
|
||||||
|
# Regular expression which should only match function or class names that do
|
||||||
|
# not require a docstring.
|
||||||
|
no-docstring-rgx=__.*__
|
||||||
|
|
||||||
|
# Minimum line length for functions/classes that require docstrings, shorter
|
||||||
|
# ones are exempt.
|
||||||
|
docstring-min-length=-1
|
||||||
|
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
|
||||||
|
# Maximum number of characters on a single line.
|
||||||
|
max-line-length=80
|
||||||
|
|
||||||
|
# Regexp for a line that is allowed to be longer than the limit.
|
||||||
|
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||||
|
|
||||||
|
# Allow the body of an if to be on the same line as the test if there is no
|
||||||
|
# else.
|
||||||
|
single-line-if-stmt=no
|
||||||
|
|
||||||
|
# List of optional constructs for which whitespace checking is disabled
|
||||||
|
no-space-check=trailing-comma,dict-separator
|
||||||
|
|
||||||
|
# Maximum number of lines in a module
|
||||||
|
max-module-lines=1000
|
||||||
|
|
||||||
|
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||||
|
# tab).
|
||||||
|
indent-string=' '
|
||||||
|
|
||||||
|
|
||||||
|
[DESIGN]
|
||||||
|
|
||||||
|
# Maximum number of arguments for function / method
|
||||||
|
max-args=5
|
||||||
|
|
||||||
|
# Argument names that match this expression will be ignored. Default to name
|
||||||
|
# with leading underscore
|
||||||
|
ignored-argument-names=_.*
|
||||||
|
|
||||||
|
# Maximum number of locals for function / method body
|
||||||
|
max-locals=15
|
||||||
|
|
||||||
|
# Maximum number of return / yield for function / method body
|
||||||
|
max-returns=6
|
||||||
|
|
||||||
|
# Maximum number of branch for function / method body
|
||||||
|
max-branches=12
|
||||||
|
|
||||||
|
# Maximum number of statements in function / method body
|
||||||
|
max-statements=50
|
||||||
|
|
||||||
|
# Maximum number of parents for a class (see R0901).
|
||||||
|
max-parents=7
|
||||||
|
|
||||||
|
# Maximum number of attributes for a class (see R0902).
|
||||||
|
max-attributes=7
|
||||||
|
|
||||||
|
# Minimum number of public methods for a class (see R0903).
|
||||||
|
min-public-methods=2
|
||||||
|
|
||||||
|
# Maximum number of public methods for a class (see R0904).
|
||||||
|
max-public-methods=20
|
||||||
|
|
||||||
|
|
||||||
|
[IMPORTS]
|
||||||
|
|
||||||
|
# Deprecated modules which should not be used, separated by a comma
|
||||||
|
deprecated-modules=regsub,TERMIOS,Bastion,rexec
|
||||||
|
|
||||||
|
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||||
|
# given file (report RP0402 must not be disabled)
|
||||||
|
import-graph=
|
||||||
|
|
||||||
|
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled)
|
||||||
|
ext-import-graph=
|
||||||
|
|
||||||
|
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled)
|
||||||
|
int-import-graph=
|
||||||
|
|
||||||
|
|
||||||
|
[CLASSES]
|
||||||
|
|
||||||
|
# List of interface methods to ignore, separated by a comma. This is used for
|
||||||
|
# instance to not check methods defines in Zope's Interface base class.
|
||||||
|
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
|
||||||
|
|
||||||
|
# List of method names used to declare (i.e. assign) instance attributes.
|
||||||
|
defining-attr-methods=__init__,__new__,setUp
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a class method.
|
||||||
|
valid-classmethod-first-arg=cls
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a metaclass class method.
|
||||||
|
valid-metaclass-classmethod-first-arg=mcs
|
||||||
|
|
||||||
|
|
||||||
|
[EXCEPTIONS]
|
||||||
|
|
||||||
|
# Exceptions that will emit a warning when being caught. Defaults to
|
||||||
|
# "Exception"
|
||||||
|
overgeneral-exceptions=Exception
|
||||||
47
scripts/check_event_hash.py
Normal file
47
scripts/check_event_hash.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from synapse.crypto.event_signing import *
|
||||||
|
from syutil.base64util import encode_base64
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class dictobj(dict):
|
||||||
|
def __init__(self, *args, **kargs):
|
||||||
|
dict.__init__(self, *args, **kargs)
|
||||||
|
self.__dict__ = self
|
||||||
|
|
||||||
|
def get_dict(self):
|
||||||
|
return dict(self)
|
||||||
|
|
||||||
|
def get_full_dict(self):
|
||||||
|
return dict(self)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("input_json", nargs="?", type=argparse.FileType('r'),
|
||||||
|
default=sys.stdin)
|
||||||
|
args = parser.parse_args()
|
||||||
|
logging.basicConfig()
|
||||||
|
|
||||||
|
event_json = dictobj(json.load(args.input_json))
|
||||||
|
|
||||||
|
algorithms = {
|
||||||
|
"sha256": hashlib.sha256,
|
||||||
|
}
|
||||||
|
|
||||||
|
for alg_name in event_json.hashes:
|
||||||
|
if check_event_content_hash(event_json, algorithms[alg_name]):
|
||||||
|
print "PASS content hash %s" % (alg_name,)
|
||||||
|
else:
|
||||||
|
print "FAIL content hash %s" % (alg_name,)
|
||||||
|
|
||||||
|
for algorithm in algorithms.values():
|
||||||
|
name, h_bytes = compute_event_reference_hash(event_json, algorithm)
|
||||||
|
print "Reference hash %s: %s" % (name, encode_base64(h_bytes))
|
||||||
|
|
||||||
|
if __name__=="__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
73
scripts/check_signature.py
Normal file
73
scripts/check_signature.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
from syutil.crypto.jsonsign import verify_signed_json
|
||||||
|
from syutil.crypto.signing_key import (
|
||||||
|
decode_verify_key_bytes, write_signing_keys
|
||||||
|
)
|
||||||
|
from syutil.base64util import decode_base64
|
||||||
|
|
||||||
|
import urllib2
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import dns.resolver
|
||||||
|
import pprint
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
def get_targets(server_name):
|
||||||
|
if ":" in server_name:
|
||||||
|
target, port = server_name.split(":")
|
||||||
|
yield (target, int(port))
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
answers = dns.resolver.query("_matrix._tcp." + server_name, "SRV")
|
||||||
|
for srv in answers:
|
||||||
|
yield (srv.target, srv.port)
|
||||||
|
except dns.resolver.NXDOMAIN:
|
||||||
|
yield (server_name, 8448)
|
||||||
|
|
||||||
|
def get_server_keys(server_name, target, port):
|
||||||
|
url = "https://%s:%i/_matrix/key/v1" % (target, port)
|
||||||
|
keys = json.load(urllib2.urlopen(url))
|
||||||
|
verify_keys = {}
|
||||||
|
for key_id, key_base64 in keys["verify_keys"].items():
|
||||||
|
verify_key = decode_verify_key_bytes(key_id, decode_base64(key_base64))
|
||||||
|
verify_signed_json(keys, server_name, verify_key)
|
||||||
|
verify_keys[key_id] = verify_key
|
||||||
|
return verify_keys
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("signature_name")
|
||||||
|
parser.add_argument("input_json", nargs="?", type=argparse.FileType('r'),
|
||||||
|
default=sys.stdin)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
logging.basicConfig()
|
||||||
|
|
||||||
|
server_name = args.signature_name
|
||||||
|
keys = {}
|
||||||
|
for target, port in get_targets(server_name):
|
||||||
|
try:
|
||||||
|
keys = get_server_keys(server_name, target, port)
|
||||||
|
print "Using keys from https://%s:%s/_matrix/key/v1" % (target, port)
|
||||||
|
write_signing_keys(sys.stdout, keys.values())
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
logging.exception("Error talking to %s:%s", target, port)
|
||||||
|
|
||||||
|
json_to_check = json.load(args.input_json)
|
||||||
|
print "Checking JSON:"
|
||||||
|
for key_id in json_to_check["signatures"][args.signature_name]:
|
||||||
|
try:
|
||||||
|
key = keys[key_id]
|
||||||
|
verify_signed_json(json_to_check, args.signature_name, key)
|
||||||
|
print "PASS %s" % (key_id,)
|
||||||
|
except:
|
||||||
|
logging.exception("Check for key %s failed" % (key_id,))
|
||||||
|
print "FAIL %s" % (key_id,)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/perl -pi
|
#!/usr/bin/perl -pi
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
$copyright = <<EOT;
|
$copyright = <<EOT;
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
69
scripts/hash_history.py
Normal file
69
scripts/hash_history.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from synapse.storage.pdu import PduStore
|
||||||
|
from synapse.storage.signatures import SignatureStore
|
||||||
|
from synapse.storage._base import SQLBaseStore
|
||||||
|
from synapse.federation.units import Pdu
|
||||||
|
from synapse.crypto.event_signing import (
|
||||||
|
add_event_pdu_content_hash, compute_pdu_event_reference_hash
|
||||||
|
)
|
||||||
|
from synapse.api.events.utils import prune_pdu
|
||||||
|
from syutil.base64util import encode_base64, decode_base64
|
||||||
|
from syutil.jsonutil import encode_canonical_json
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class Store(object):
|
||||||
|
_get_pdu_tuples = PduStore.__dict__["_get_pdu_tuples"]
|
||||||
|
_get_pdu_content_hashes_txn = SignatureStore.__dict__["_get_pdu_content_hashes_txn"]
|
||||||
|
_get_prev_pdu_hashes_txn = SignatureStore.__dict__["_get_prev_pdu_hashes_txn"]
|
||||||
|
_get_pdu_origin_signatures_txn = SignatureStore.__dict__["_get_pdu_origin_signatures_txn"]
|
||||||
|
_store_pdu_content_hash_txn = SignatureStore.__dict__["_store_pdu_content_hash_txn"]
|
||||||
|
_store_pdu_reference_hash_txn = SignatureStore.__dict__["_store_pdu_reference_hash_txn"]
|
||||||
|
_store_prev_pdu_hash_txn = SignatureStore.__dict__["_store_prev_pdu_hash_txn"]
|
||||||
|
_simple_insert_txn = SQLBaseStore.__dict__["_simple_insert_txn"]
|
||||||
|
|
||||||
|
|
||||||
|
store = Store()
|
||||||
|
|
||||||
|
|
||||||
|
def select_pdus(cursor):
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT pdu_id, origin FROM pdus ORDER BY depth ASC"
|
||||||
|
)
|
||||||
|
|
||||||
|
ids = cursor.fetchall()
|
||||||
|
|
||||||
|
pdu_tuples = store._get_pdu_tuples(cursor, ids)
|
||||||
|
|
||||||
|
pdus = [Pdu.from_pdu_tuple(p) for p in pdu_tuples]
|
||||||
|
|
||||||
|
reference_hashes = {}
|
||||||
|
|
||||||
|
for pdu in pdus:
|
||||||
|
try:
|
||||||
|
if pdu.prev_pdus:
|
||||||
|
print "PROCESS", pdu.pdu_id, pdu.origin, pdu.prev_pdus
|
||||||
|
for pdu_id, origin, hashes in pdu.prev_pdus:
|
||||||
|
ref_alg, ref_hsh = reference_hashes[(pdu_id, origin)]
|
||||||
|
hashes[ref_alg] = encode_base64(ref_hsh)
|
||||||
|
store._store_prev_pdu_hash_txn(cursor, pdu.pdu_id, pdu.origin, pdu_id, origin, ref_alg, ref_hsh)
|
||||||
|
print "SUCCESS", pdu.pdu_id, pdu.origin, pdu.prev_pdus
|
||||||
|
pdu = add_event_pdu_content_hash(pdu)
|
||||||
|
ref_alg, ref_hsh = compute_pdu_event_reference_hash(pdu)
|
||||||
|
reference_hashes[(pdu.pdu_id, pdu.origin)] = (ref_alg, ref_hsh)
|
||||||
|
store._store_pdu_reference_hash_txn(cursor, pdu.pdu_id, pdu.origin, ref_alg, ref_hsh)
|
||||||
|
|
||||||
|
for alg, hsh_base64 in pdu.hashes.items():
|
||||||
|
print alg, hsh_base64
|
||||||
|
store._store_pdu_content_hash_txn(cursor, pdu.pdu_id, pdu.origin, alg, decode_base64(hsh_base64))
|
||||||
|
|
||||||
|
except:
|
||||||
|
print "FAILED_", pdu.pdu_id, pdu.origin, pdu.prev_pdus
|
||||||
|
|
||||||
|
def main():
|
||||||
|
conn = sqlite3.connect(sys.argv[1])
|
||||||
|
cursor = conn.cursor()
|
||||||
|
select_pdus(cursor)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
main()
|
||||||
22
setup.py
22
setup.py
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -26,31 +26,39 @@ def read(fname):
|
|||||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="SynapseHomeServer",
|
name="matrix-synapse",
|
||||||
version="0.0.1",
|
version=read("VERSION").strip(),
|
||||||
packages=find_packages(exclude=["tests"]),
|
packages=find_packages(exclude=["tests", "tests.*"]),
|
||||||
description="Reference Synapse Home Server",
|
description="Reference Synapse Home Server",
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"syutil==0.0.1",
|
"syutil==0.0.2",
|
||||||
|
"matrix_angular_sdk==0.5.1",
|
||||||
"Twisted>=14.0.0",
|
"Twisted>=14.0.0",
|
||||||
"service_identity>=1.0.0",
|
"service_identity>=1.0.0",
|
||||||
|
"pyopenssl>=0.14",
|
||||||
|
"pyyaml",
|
||||||
"pyasn1",
|
"pyasn1",
|
||||||
"pynacl",
|
"pynacl",
|
||||||
"daemonize",
|
"daemonize",
|
||||||
"py-bcrypt",
|
"py-bcrypt",
|
||||||
],
|
],
|
||||||
dependency_links=[
|
dependency_links=[
|
||||||
"git+ssh://git@github.com/matrix-org/syutil.git#egg=syutil-0.0.1",
|
"https://github.com/matrix-org/syutil/tarball/v0.0.2#egg=syutil-0.0.2",
|
||||||
|
"https://github.com/pyca/pynacl/tarball/d4d3175589b892f6ea7c22f466e0e223853516fa#egg=pynacl-0.3.0",
|
||||||
|
"https://github.com/matrix-org/matrix-angular-sdk/tarball/v0.5.1/#egg=matrix_angular_sdk-0.5.1",
|
||||||
],
|
],
|
||||||
setup_requires=[
|
setup_requires=[
|
||||||
"setuptools_trial",
|
"setuptools_trial",
|
||||||
"setuptools>=1.0.0", # Needs setuptools that supports git+ssh. It's not obvious when support for this was introduced.
|
"setuptools>=1.0.0", # Needs setuptools that supports git+ssh.
|
||||||
|
# TODO: Do we need this now? we don't use git+ssh.
|
||||||
"mock"
|
"mock"
|
||||||
],
|
],
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
zip_safe=False,
|
||||||
long_description=read("README.rst"),
|
long_description=read("README.rst"),
|
||||||
entry_points="""
|
entry_points="""
|
||||||
[console_scripts]
|
[console_scripts]
|
||||||
|
synctl=synapse.app.synctl:main
|
||||||
synapse-homeserver=synapse.app.homeserver:run
|
synapse-homeserver=synapse.app.homeserver:run
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -16,4 +16,4 @@
|
|||||||
""" This is a reference implementation of a synapse home server.
|
""" This is a reference implementation of a synapse home server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.1.1"
|
__version__ = "0.5.3c"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -12,4 +12,3 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,9 +17,14 @@
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import Membership
|
from synapse.api.constants import Membership, JoinRules
|
||||||
from synapse.api.errors import AuthError, StoreError, Codes
|
from synapse.api.errors import AuthError, StoreError, Codes, SynapseError
|
||||||
from synapse.api.events.room import RoomMemberEvent
|
from synapse.api.events.room import (
|
||||||
|
RoomMemberEvent, RoomPowerLevelsEvent, RoomRedactionEvent,
|
||||||
|
RoomJoinRulesEvent, RoomCreateEvent, RoomAliasesEvent,
|
||||||
|
)
|
||||||
|
from synapse.util.logutils import log_function
|
||||||
|
from syutil.base64util import encode_base64
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -31,50 +36,96 @@ class Auth(object):
|
|||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
|
self.state = hs.get_state_handler()
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
def check(self, event, auth_events):
|
||||||
def check(self, event, snapshot, raises=False):
|
|
||||||
""" Checks if this event is correctly authed.
|
""" Checks if this event is correctly authed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the auth checks pass.
|
True if the auth checks pass.
|
||||||
Raises:
|
|
||||||
AuthError if there was a problem authorising this event. This will
|
|
||||||
be raised only if raises=True.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if hasattr(event, "room_id"):
|
if not hasattr(event, "room_id"):
|
||||||
if event.type == RoomMemberEvent.TYPE:
|
raise AuthError(500, "Event has no room_id: %s" % event)
|
||||||
allowed = yield self.is_membership_change_allowed(event)
|
if auth_events is None:
|
||||||
defer.returnValue(allowed)
|
# Oh, we don't know what the state of the room was, so we
|
||||||
|
# are trusting that this is allowed (at least for now)
|
||||||
|
logger.warn("Trusting event: %s", event.event_id)
|
||||||
|
return True
|
||||||
|
|
||||||
|
if event.type == RoomCreateEvent.TYPE:
|
||||||
|
# FIXME
|
||||||
|
return True
|
||||||
|
|
||||||
|
# FIXME: Temp hack
|
||||||
|
if event.type == RoomAliasesEvent.TYPE:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
|
allowed = self.is_membership_change_allowed(
|
||||||
|
event, auth_events
|
||||||
|
)
|
||||||
|
if allowed:
|
||||||
|
logger.debug("Allowing! %s", event)
|
||||||
else:
|
else:
|
||||||
self._check_joined_room(
|
logger.debug("Denying! %s", event)
|
||||||
member=snapshot.membership_state,
|
return allowed
|
||||||
user_id=snapshot.user_id,
|
|
||||||
room_id=snapshot.room_id,
|
self.check_event_sender_in_room(event, auth_events)
|
||||||
)
|
self._can_send_event(event, auth_events)
|
||||||
defer.returnValue(True)
|
|
||||||
else:
|
if event.type == RoomPowerLevelsEvent.TYPE:
|
||||||
raise AuthError(500, "Unknown event: %s" % event)
|
self._check_power_levels(event, auth_events)
|
||||||
|
|
||||||
|
if event.type == RoomRedactionEvent.TYPE:
|
||||||
|
self._check_redaction(event, auth_events)
|
||||||
|
|
||||||
|
logger.debug("Allowing! %s", event)
|
||||||
except AuthError as e:
|
except AuthError as e:
|
||||||
logger.info("Event auth check failed on event %s with msg: %s",
|
logger.info(
|
||||||
event, e.msg)
|
"Event auth check failed on event %s with msg: %s",
|
||||||
if raises:
|
event, e.msg
|
||||||
raise e
|
)
|
||||||
defer.returnValue(False)
|
logger.info("Denying! %s", event)
|
||||||
|
raise
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_joined_room(self, room_id, user_id):
|
def check_joined_room(self, room_id, user_id):
|
||||||
try:
|
member = yield self.state.get_current_state(
|
||||||
member = yield self.store.get_room_member(
|
room_id=room_id,
|
||||||
room_id=room_id,
|
event_type=RoomMemberEvent.TYPE,
|
||||||
user_id=user_id
|
state_key=user_id
|
||||||
)
|
)
|
||||||
self._check_joined_room(member, user_id, room_id)
|
self._check_joined_room(member, user_id, room_id)
|
||||||
defer.returnValue(member)
|
defer.returnValue(member)
|
||||||
except AttributeError:
|
|
||||||
pass
|
@defer.inlineCallbacks
|
||||||
defer.returnValue(None)
|
def check_host_in_room(self, room_id, host):
|
||||||
|
curr_state = yield self.state.get_current_state(room_id)
|
||||||
|
|
||||||
|
for event in curr_state:
|
||||||
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
|
try:
|
||||||
|
if self.hs.parse_userid(event.state_key).domain != host:
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
logger.warn("state_key not user_id: %s", event.state_key)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if event.content["membership"] == Membership.JOIN:
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
def check_event_sender_in_room(self, event, auth_events):
|
||||||
|
key = (RoomMemberEvent.TYPE, event.user_id, )
|
||||||
|
member_event = auth_events.get(key)
|
||||||
|
|
||||||
|
return self._check_joined_room(
|
||||||
|
member_event,
|
||||||
|
event.user_id,
|
||||||
|
event.room_id
|
||||||
|
)
|
||||||
|
|
||||||
def _check_joined_room(self, member, user_id, room_id):
|
def _check_joined_room(self, member, user_id, room_id):
|
||||||
if not member or member.membership != Membership.JOIN:
|
if not member or member.membership != Membership.JOIN:
|
||||||
@@ -82,39 +133,79 @@ class Auth(object):
|
|||||||
user_id, room_id, repr(member)
|
user_id, room_id, repr(member)
|
||||||
))
|
))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@log_function
|
||||||
def is_membership_change_allowed(self, event):
|
def is_membership_change_allowed(self, event, auth_events):
|
||||||
target_user_id = event.state_key
|
|
||||||
|
|
||||||
# does this room even exist
|
|
||||||
room = yield self.store.get_room(event.room_id)
|
|
||||||
if not room:
|
|
||||||
raise AuthError(403, "Room does not exist")
|
|
||||||
|
|
||||||
# get info about the caller
|
|
||||||
try:
|
|
||||||
caller = yield self.store.get_room_member(
|
|
||||||
user_id=event.user_id,
|
|
||||||
room_id=event.room_id)
|
|
||||||
except:
|
|
||||||
caller = None
|
|
||||||
caller_in_room = caller and caller.membership == "join"
|
|
||||||
|
|
||||||
# get info about the target
|
|
||||||
try:
|
|
||||||
target = yield self.store.get_room_member(
|
|
||||||
user_id=target_user_id,
|
|
||||||
room_id=event.room_id)
|
|
||||||
except:
|
|
||||||
target = None
|
|
||||||
target_in_room = target and target.membership == "join"
|
|
||||||
|
|
||||||
membership = event.content["membership"]
|
membership = event.content["membership"]
|
||||||
|
|
||||||
|
# Check if this is the room creator joining:
|
||||||
|
if len(event.prev_events) == 1 and Membership.JOIN == membership:
|
||||||
|
# Get room creation event:
|
||||||
|
key = (RoomCreateEvent.TYPE, "", )
|
||||||
|
create = auth_events.get(key)
|
||||||
|
if create and event.prev_events[0][0] == create.event_id:
|
||||||
|
if create.content["creator"] == event.state_key:
|
||||||
|
return True
|
||||||
|
|
||||||
|
target_user_id = event.state_key
|
||||||
|
|
||||||
|
# get info about the caller
|
||||||
|
key = (RoomMemberEvent.TYPE, event.user_id, )
|
||||||
|
caller = auth_events.get(key)
|
||||||
|
|
||||||
|
caller_in_room = caller and caller.membership == Membership.JOIN
|
||||||
|
caller_invited = caller and caller.membership == Membership.INVITE
|
||||||
|
|
||||||
|
# get info about the target
|
||||||
|
key = (RoomMemberEvent.TYPE, target_user_id, )
|
||||||
|
target = auth_events.get(key)
|
||||||
|
|
||||||
|
target_in_room = target and target.membership == Membership.JOIN
|
||||||
|
|
||||||
|
key = (RoomJoinRulesEvent.TYPE, "", )
|
||||||
|
join_rule_event = auth_events.get(key)
|
||||||
|
if join_rule_event:
|
||||||
|
join_rule = join_rule_event.content.get(
|
||||||
|
"join_rule", JoinRules.INVITE
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
join_rule = JoinRules.INVITE
|
||||||
|
|
||||||
|
user_level = self._get_power_level_from_event_state(
|
||||||
|
event,
|
||||||
|
event.user_id,
|
||||||
|
auth_events,
|
||||||
|
)
|
||||||
|
|
||||||
|
ban_level, kick_level, redact_level = (
|
||||||
|
self._get_ops_level_from_event_state(
|
||||||
|
event,
|
||||||
|
auth_events,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"is_membership_change_allowed: %s",
|
||||||
|
{
|
||||||
|
"caller_in_room": caller_in_room,
|
||||||
|
"caller_invited": caller_invited,
|
||||||
|
"target_in_room": target_in_room,
|
||||||
|
"membership": membership,
|
||||||
|
"join_rule": join_rule,
|
||||||
|
"target_user_id": target_user_id,
|
||||||
|
"event.user_id": event.user_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if Membership.INVITE == membership:
|
if Membership.INVITE == membership:
|
||||||
|
# TODO (erikj): We should probably handle this more intelligently
|
||||||
|
# PRIVATE join rules.
|
||||||
|
|
||||||
# Invites are valid iff caller is in the room and target isn't.
|
# Invites are valid iff caller is in the room and target isn't.
|
||||||
if not caller_in_room: # caller isn't joined
|
if not caller_in_room: # caller isn't joined
|
||||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"%s not in room %s." % (event.user_id, event.room_id,)
|
||||||
|
)
|
||||||
elif target_in_room: # the target is already in the room.
|
elif target_in_room: # the target is already in the room.
|
||||||
raise AuthError(403, "%s is already in the room." %
|
raise AuthError(403, "%s is already in the room." %
|
||||||
target_user_id)
|
target_user_id)
|
||||||
@@ -124,23 +215,76 @@ class Auth(object):
|
|||||||
# joined: It's a NOOP
|
# joined: It's a NOOP
|
||||||
if event.user_id != target_user_id:
|
if event.user_id != target_user_id:
|
||||||
raise AuthError(403, "Cannot force another user to join.")
|
raise AuthError(403, "Cannot force another user to join.")
|
||||||
elif room.is_public:
|
elif join_rule == JoinRules.PUBLIC:
|
||||||
pass # anyone can join public rooms.
|
pass
|
||||||
elif (not caller or caller.membership not in
|
elif join_rule == JoinRules.INVITE:
|
||||||
[Membership.INVITE, Membership.JOIN]):
|
if not caller_in_room and not caller_invited:
|
||||||
raise AuthError(403, "You are not invited to this room.")
|
raise AuthError(403, "You are not invited to this room.")
|
||||||
|
else:
|
||||||
|
# TODO (erikj): may_join list
|
||||||
|
# TODO (erikj): private rooms
|
||||||
|
raise AuthError(403, "You are not allowed to join this room")
|
||||||
elif Membership.LEAVE == membership:
|
elif Membership.LEAVE == membership:
|
||||||
|
# TODO (erikj): Implement kicks.
|
||||||
|
|
||||||
if not caller_in_room: # trying to leave a room you aren't joined
|
if not caller_in_room: # trying to leave a room you aren't joined
|
||||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"%s not in room %s." % (target_user_id, event.room_id,)
|
||||||
|
)
|
||||||
elif target_user_id != event.user_id:
|
elif target_user_id != event.user_id:
|
||||||
# trying to force another user to leave
|
if kick_level:
|
||||||
raise AuthError(403, "Cannot force %s to leave." %
|
kick_level = int(kick_level)
|
||||||
target_user_id)
|
else:
|
||||||
|
kick_level = 50 # FIXME (erikj): What should we do here?
|
||||||
|
|
||||||
|
if user_level < kick_level:
|
||||||
|
raise AuthError(
|
||||||
|
403, "You cannot kick user %s." % target_user_id
|
||||||
|
)
|
||||||
|
elif Membership.BAN == membership:
|
||||||
|
if ban_level:
|
||||||
|
ban_level = int(ban_level)
|
||||||
|
else:
|
||||||
|
ban_level = 50 # FIXME (erikj): What should we do here?
|
||||||
|
|
||||||
|
if user_level < ban_level:
|
||||||
|
raise AuthError(403, "You don't have permission to ban")
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown membership %s" % membership)
|
raise AuthError(500, "Unknown membership %s" % membership)
|
||||||
|
|
||||||
defer.returnValue(True)
|
return True
|
||||||
|
|
||||||
|
def _get_power_level_from_event_state(self, event, user_id, auth_events):
|
||||||
|
key = (RoomPowerLevelsEvent.TYPE, "", )
|
||||||
|
power_level_event = auth_events.get(key)
|
||||||
|
level = None
|
||||||
|
if power_level_event:
|
||||||
|
level = power_level_event.content.get("users", {}).get(user_id)
|
||||||
|
if not level:
|
||||||
|
level = power_level_event.content.get("users_default", 0)
|
||||||
|
else:
|
||||||
|
key = (RoomCreateEvent.TYPE, "", )
|
||||||
|
create_event = auth_events.get(key)
|
||||||
|
if (create_event is not None and
|
||||||
|
create_event.content["creator"] == user_id):
|
||||||
|
return 100
|
||||||
|
|
||||||
|
return level
|
||||||
|
|
||||||
|
def _get_ops_level_from_event_state(self, event, auth_events):
|
||||||
|
key = (RoomPowerLevelsEvent.TYPE, "", )
|
||||||
|
power_level_event = auth_events.get(key)
|
||||||
|
|
||||||
|
if power_level_event:
|
||||||
|
return (
|
||||||
|
power_level_event.content.get("ban", 50),
|
||||||
|
power_level_event.content.get("kick", 50),
|
||||||
|
power_level_event.content.get("redact", 50),
|
||||||
|
)
|
||||||
|
return None, None, None,
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
def get_user_by_req(self, request):
|
def get_user_by_req(self, request):
|
||||||
""" Get a registered user's ID.
|
""" Get a registered user's ID.
|
||||||
|
|
||||||
@@ -153,7 +297,25 @@ class Auth(object):
|
|||||||
"""
|
"""
|
||||||
# Can optionally look elsewhere in the request (e.g. headers)
|
# Can optionally look elsewhere in the request (e.g. headers)
|
||||||
try:
|
try:
|
||||||
return self.get_user_by_token(request.args["access_token"][0])
|
access_token = request.args["access_token"][0]
|
||||||
|
user_info = yield self.get_user_by_token(access_token)
|
||||||
|
user = user_info["user"]
|
||||||
|
|
||||||
|
ip_addr = self.hs.get_ip_from_request(request)
|
||||||
|
user_agent = request.requestHeaders.getRawHeaders(
|
||||||
|
"User-Agent",
|
||||||
|
default=[""]
|
||||||
|
)[0]
|
||||||
|
if user and access_token and ip_addr:
|
||||||
|
yield self.store.insert_client_ip(
|
||||||
|
user=user,
|
||||||
|
access_token=access_token,
|
||||||
|
device_id=user_info["device_id"],
|
||||||
|
ip=ip_addr,
|
||||||
|
user_agent=user_agent
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(user)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise AuthError(403, "Missing access token.")
|
raise AuthError(403, "Missing access token.")
|
||||||
|
|
||||||
@@ -162,17 +324,246 @@ class Auth(object):
|
|||||||
""" Get a registered user's ID.
|
""" Get a registered user's ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token (str)- The access token to get the user by.
|
token (str): The access token to get the user by.
|
||||||
Returns:
|
Returns:
|
||||||
UserID : User ID object of the user who has that access token.
|
dict : dict that includes the user, device_id, and whether the
|
||||||
|
user is a server admin.
|
||||||
Raises:
|
Raises:
|
||||||
AuthError if no user by that token exists or the token is invalid.
|
AuthError if no user by that token exists or the token is invalid.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
user_id = yield self.store.get_user_by_token(token=token)
|
ret = yield self.store.get_user_by_token(token=token)
|
||||||
if not user_id:
|
if not ret:
|
||||||
raise StoreError()
|
raise StoreError()
|
||||||
defer.returnValue(self.hs.parse_userid(user_id))
|
|
||||||
|
user_info = {
|
||||||
|
"admin": bool(ret.get("admin", False)),
|
||||||
|
"device_id": ret.get("device_id"),
|
||||||
|
"user": self.hs.parse_userid(ret.get("name")),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer.returnValue(user_info)
|
||||||
except StoreError:
|
except StoreError:
|
||||||
raise AuthError(403, "Unrecognised access token.",
|
raise AuthError(403, "Unrecognised access token.",
|
||||||
errcode=Codes.UNKNOWN_TOKEN)
|
errcode=Codes.UNKNOWN_TOKEN)
|
||||||
|
|
||||||
|
def is_server_admin(self, user):
|
||||||
|
return self.store.is_server_admin(user)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def add_auth_events(self, event):
|
||||||
|
if event.type == RoomCreateEvent.TYPE:
|
||||||
|
event.auth_events = []
|
||||||
|
return
|
||||||
|
|
||||||
|
auth_events = []
|
||||||
|
|
||||||
|
key = (RoomPowerLevelsEvent.TYPE, "", )
|
||||||
|
power_level_event = event.old_state_events.get(key)
|
||||||
|
|
||||||
|
if power_level_event:
|
||||||
|
auth_events.append(power_level_event.event_id)
|
||||||
|
|
||||||
|
key = (RoomJoinRulesEvent.TYPE, "", )
|
||||||
|
join_rule_event = event.old_state_events.get(key)
|
||||||
|
|
||||||
|
key = (RoomMemberEvent.TYPE, event.user_id, )
|
||||||
|
member_event = event.old_state_events.get(key)
|
||||||
|
|
||||||
|
key = (RoomCreateEvent.TYPE, "", )
|
||||||
|
create_event = event.old_state_events.get(key)
|
||||||
|
if create_event:
|
||||||
|
auth_events.append(create_event.event_id)
|
||||||
|
|
||||||
|
if join_rule_event:
|
||||||
|
join_rule = join_rule_event.content.get("join_rule")
|
||||||
|
is_public = join_rule == JoinRules.PUBLIC if join_rule else False
|
||||||
|
else:
|
||||||
|
is_public = False
|
||||||
|
|
||||||
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
|
e_type = event.content["membership"]
|
||||||
|
if e_type in [Membership.JOIN, Membership.INVITE]:
|
||||||
|
if join_rule_event:
|
||||||
|
auth_events.append(join_rule_event.event_id)
|
||||||
|
|
||||||
|
if member_event and not is_public:
|
||||||
|
auth_events.append(member_event.event_id)
|
||||||
|
elif member_event:
|
||||||
|
if member_event.content["membership"] == Membership.JOIN:
|
||||||
|
auth_events.append(member_event.event_id)
|
||||||
|
|
||||||
|
hashes = yield self.store.get_event_reference_hashes(
|
||||||
|
auth_events
|
||||||
|
)
|
||||||
|
hashes = [
|
||||||
|
{
|
||||||
|
k: encode_base64(v) for k, v in h.items()
|
||||||
|
if k == "sha256"
|
||||||
|
}
|
||||||
|
for h in hashes
|
||||||
|
]
|
||||||
|
event.auth_events = zip(auth_events, hashes)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def _can_send_event(self, event, auth_events):
|
||||||
|
key = (RoomPowerLevelsEvent.TYPE, "", )
|
||||||
|
send_level_event = auth_events.get(key)
|
||||||
|
send_level = None
|
||||||
|
if send_level_event:
|
||||||
|
send_level = send_level_event.content.get("events", {}).get(
|
||||||
|
event.type
|
||||||
|
)
|
||||||
|
if not send_level:
|
||||||
|
if hasattr(event, "state_key"):
|
||||||
|
send_level = send_level_event.content.get(
|
||||||
|
"state_default", 50
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
send_level = send_level_event.content.get(
|
||||||
|
"events_default", 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if send_level:
|
||||||
|
send_level = int(send_level)
|
||||||
|
else:
|
||||||
|
send_level = 0
|
||||||
|
|
||||||
|
user_level = self._get_power_level_from_event_state(
|
||||||
|
event,
|
||||||
|
event.user_id,
|
||||||
|
auth_events,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_level:
|
||||||
|
user_level = int(user_level)
|
||||||
|
else:
|
||||||
|
user_level = 0
|
||||||
|
|
||||||
|
if user_level < send_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to post that to the room. " +
|
||||||
|
"user_level (%d) < send_level (%d)" % (user_level, send_level)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check state_key
|
||||||
|
if hasattr(event, "state_key"):
|
||||||
|
if not event.state_key.startswith("_"):
|
||||||
|
if event.state_key.startswith("@"):
|
||||||
|
if event.state_key != event.user_id:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You are not allowed to set others state"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sender_domain = self.hs.parse_userid(
|
||||||
|
event.user_id
|
||||||
|
).domain
|
||||||
|
|
||||||
|
if sender_domain != event.state_key:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You are not allowed to set others state"
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _check_redaction(self, event, auth_events):
|
||||||
|
user_level = self._get_power_level_from_event_state(
|
||||||
|
event,
|
||||||
|
event.user_id,
|
||||||
|
auth_events,
|
||||||
|
)
|
||||||
|
|
||||||
|
_, _, redact_level = self._get_ops_level_from_event_state(
|
||||||
|
event,
|
||||||
|
auth_events,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_level < redact_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to redact events"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_power_levels(self, event, auth_events):
|
||||||
|
user_list = event.content.get("users", {})
|
||||||
|
# Validate users
|
||||||
|
for k, v in user_list.items():
|
||||||
|
try:
|
||||||
|
self.hs.parse_userid(k)
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Not a valid user_id: %s" % (k,))
|
||||||
|
|
||||||
|
try:
|
||||||
|
int(v)
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Not a valid power level: %s" % (v,))
|
||||||
|
|
||||||
|
key = (event.type, event.state_key, )
|
||||||
|
current_state = auth_events.get(key)
|
||||||
|
|
||||||
|
if not current_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
user_level = self._get_power_level_from_event_state(
|
||||||
|
event,
|
||||||
|
event.user_id,
|
||||||
|
auth_events,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check other levels:
|
||||||
|
levels_to_check = [
|
||||||
|
("users_default", []),
|
||||||
|
("events_default", []),
|
||||||
|
("ban", []),
|
||||||
|
("redact", []),
|
||||||
|
("kick", []),
|
||||||
|
]
|
||||||
|
|
||||||
|
old_list = current_state.content.get("users")
|
||||||
|
for user in set(old_list.keys() + user_list.keys()):
|
||||||
|
levels_to_check.append(
|
||||||
|
(user, ["users"])
|
||||||
|
)
|
||||||
|
|
||||||
|
old_list = current_state.content.get("events")
|
||||||
|
new_list = event.content.get("events")
|
||||||
|
for ev_id in set(old_list.keys() + new_list.keys()):
|
||||||
|
levels_to_check.append(
|
||||||
|
(ev_id, ["events"])
|
||||||
|
)
|
||||||
|
|
||||||
|
old_state = current_state.content
|
||||||
|
new_state = event.content
|
||||||
|
|
||||||
|
for level_to_check, dir in levels_to_check:
|
||||||
|
old_loc = old_state
|
||||||
|
for d in dir:
|
||||||
|
old_loc = old_loc.get(d, {})
|
||||||
|
|
||||||
|
new_loc = new_state
|
||||||
|
for d in dir:
|
||||||
|
new_loc = new_loc.get(d, {})
|
||||||
|
|
||||||
|
if level_to_check in old_loc:
|
||||||
|
old_level = int(old_loc[level_to_check])
|
||||||
|
else:
|
||||||
|
old_level = None
|
||||||
|
|
||||||
|
if level_to_check in new_loc:
|
||||||
|
new_level = int(new_loc[level_to_check])
|
||||||
|
else:
|
||||||
|
new_level = None
|
||||||
|
|
||||||
|
if new_level is not None and old_level is not None:
|
||||||
|
if new_level == old_level:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if old_level > user_level or new_level > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to add ops level greater "
|
||||||
|
"than your own"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -23,7 +23,8 @@ class Membership(object):
|
|||||||
JOIN = u"join"
|
JOIN = u"join"
|
||||||
KNOCK = u"knock"
|
KNOCK = u"knock"
|
||||||
LEAVE = u"leave"
|
LEAVE = u"leave"
|
||||||
LIST = (INVITE, JOIN, KNOCK, LEAVE)
|
BAN = u"ban"
|
||||||
|
LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN)
|
||||||
|
|
||||||
|
|
||||||
class Feedback(object):
|
class Feedback(object):
|
||||||
@@ -42,3 +43,19 @@ class PresenceState(object):
|
|||||||
UNAVAILABLE = u"unavailable"
|
UNAVAILABLE = u"unavailable"
|
||||||
ONLINE = u"online"
|
ONLINE = u"online"
|
||||||
FREE_FOR_CHAT = u"free_for_chat"
|
FREE_FOR_CHAT = u"free_for_chat"
|
||||||
|
|
||||||
|
|
||||||
|
class JoinRules(object):
|
||||||
|
PUBLIC = u"public"
|
||||||
|
KNOCK = u"knock"
|
||||||
|
INVITE = u"invite"
|
||||||
|
PRIVATE = u"private"
|
||||||
|
|
||||||
|
|
||||||
|
class LoginType(object):
|
||||||
|
PASSWORD = u"m.login.password"
|
||||||
|
OAUTH = u"m.login.oauth2"
|
||||||
|
EMAIL_CODE = u"m.login.email.code"
|
||||||
|
EMAIL_URL = u"m.login.email.url"
|
||||||
|
EMAIL_IDENTITY = u"m.login.email.identity"
|
||||||
|
RECAPTCHA = u"m.login.recaptcha"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,8 +17,11 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Codes(object):
|
class Codes(object):
|
||||||
|
UNAUTHORIZED = "M_UNAUTHORIZED"
|
||||||
FORBIDDEN = "M_FORBIDDEN"
|
FORBIDDEN = "M_FORBIDDEN"
|
||||||
BAD_JSON = "M_BAD_JSON"
|
BAD_JSON = "M_BAD_JSON"
|
||||||
NOT_JSON = "M_NOT_JSON"
|
NOT_JSON = "M_NOT_JSON"
|
||||||
@@ -28,31 +31,44 @@ class Codes(object):
|
|||||||
UNKNOWN = "M_UNKNOWN"
|
UNKNOWN = "M_UNKNOWN"
|
||||||
NOT_FOUND = "M_NOT_FOUND"
|
NOT_FOUND = "M_NOT_FOUND"
|
||||||
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||||
|
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
||||||
|
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
||||||
|
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(Exception):
|
class CodeMessageException(Exception):
|
||||||
"""An exception with integer code and message string attributes."""
|
"""An exception with integer code and message string attributes."""
|
||||||
|
|
||||||
def __init__(self, code, msg):
|
def __init__(self, code, msg):
|
||||||
logging.error("%s: %s, %s", type(self).__name__, code, msg)
|
logger.info("%s: %s, %s", type(self).__name__, code, msg)
|
||||||
super(CodeMessageException, self).__init__("%d: %s" % (code, msg))
|
super(CodeMessageException, self).__init__("%d: %s" % (code, msg))
|
||||||
self.code = code
|
self.code = code
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
|
self.response_code_message = None
|
||||||
|
|
||||||
|
def error_dict(self):
|
||||||
|
return cs_error(self.msg)
|
||||||
|
|
||||||
|
|
||||||
class SynapseError(CodeMessageException):
|
class SynapseError(CodeMessageException):
|
||||||
"""A base error which can be caught for all synapse events."""
|
"""A base error which can be caught for all synapse events."""
|
||||||
def __init__(self, code, msg, errcode=""):
|
def __init__(self, code, msg, errcode=Codes.UNKNOWN):
|
||||||
"""Constructs a synapse error.
|
"""Constructs a synapse error.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
code (int): The integer error code (typically an HTTP response code)
|
code (int): The integer error code (an HTTP response code)
|
||||||
msg (str): The human-readable error message.
|
msg (str): The human-readable error message.
|
||||||
err (str): The error code e.g 'M_FORBIDDEN'
|
err (str): The error code e.g 'M_FORBIDDEN'
|
||||||
"""
|
"""
|
||||||
super(SynapseError, self).__init__(code, msg)
|
super(SynapseError, self).__init__(code, msg)
|
||||||
self.errcode = errcode
|
self.errcode = errcode
|
||||||
|
|
||||||
|
def error_dict(self):
|
||||||
|
return cs_error(
|
||||||
|
self.msg,
|
||||||
|
self.errcode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoomError(SynapseError):
|
class RoomError(SynapseError):
|
||||||
"""An error raised when a room event fails."""
|
"""An error raised when a room event fails."""
|
||||||
@@ -91,15 +107,43 @@ class StoreError(SynapseError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def cs_exception(exception):
|
class InvalidCaptchaError(SynapseError):
|
||||||
if isinstance(exception, SynapseError):
|
def __init__(self, code=400, msg="Invalid captcha.", error_url=None,
|
||||||
|
errcode=Codes.CAPTCHA_INVALID):
|
||||||
|
super(InvalidCaptchaError, self).__init__(code, msg, errcode)
|
||||||
|
self.error_url = error_url
|
||||||
|
|
||||||
|
def error_dict(self):
|
||||||
return cs_error(
|
return cs_error(
|
||||||
exception.msg,
|
self.msg,
|
||||||
Codes.UNKNOWN if not exception.errcode else exception.errcode)
|
self.errcode,
|
||||||
elif isinstance(exception, CodeMessageException):
|
error_url=self.error_url,
|
||||||
return cs_error(exception.msg)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LimitExceededError(SynapseError):
|
||||||
|
"""A client has sent too many requests and is being throttled.
|
||||||
|
"""
|
||||||
|
def __init__(self, code=429, msg="Too Many Requests", retry_after_ms=None,
|
||||||
|
errcode=Codes.LIMIT_EXCEEDED):
|
||||||
|
super(LimitExceededError, self).__init__(code, msg, errcode)
|
||||||
|
self.retry_after_ms = retry_after_ms
|
||||||
|
self.response_code_message = "Too Many Requests"
|
||||||
|
|
||||||
|
def error_dict(self):
|
||||||
|
return cs_error(
|
||||||
|
self.msg,
|
||||||
|
self.errcode,
|
||||||
|
retry_after_ms=self.retry_after_ms,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cs_exception(exception):
|
||||||
|
if isinstance(exception, CodeMessageException):
|
||||||
|
return exception.error_dict()
|
||||||
else:
|
else:
|
||||||
logging.error("Unknown exception type: %s", type(exception))
|
logger.error("Unknown exception type: %s", type(exception))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
|
def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
|
||||||
@@ -117,3 +161,37 @@ def cs_error(msg, code=Codes.UNKNOWN, **kwargs):
|
|||||||
for key, value in kwargs.iteritems():
|
for key, value in kwargs.iteritems():
|
||||||
err[key] = value
|
err[key] = value
|
||||||
return err
|
return err
|
||||||
|
|
||||||
|
|
||||||
|
class FederationError(RuntimeError):
|
||||||
|
""" This class is used to inform remote home servers about erroneous
|
||||||
|
PDUs they sent us.
|
||||||
|
|
||||||
|
FATAL: The remote server could not interpret the source event.
|
||||||
|
(e.g., it was missing a required field)
|
||||||
|
ERROR: The remote server interpreted the event, but it failed some other
|
||||||
|
check (e.g. auth)
|
||||||
|
WARN: The remote server accepted the event, but believes some part of it
|
||||||
|
is wrong (e.g., it referred to an invalid event)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, level, code, reason, affected, source=None):
|
||||||
|
if level not in ["FATAL", "ERROR", "WARN"]:
|
||||||
|
raise ValueError("Level is not valid: %s" % (level,))
|
||||||
|
self.level = level
|
||||||
|
self.code = code
|
||||||
|
self.reason = reason
|
||||||
|
self.affected = affected
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
msg = "%s %s: %s" % (level, code, reason,)
|
||||||
|
super(FederationError, self).__init__(msg)
|
||||||
|
|
||||||
|
def get_dict(self):
|
||||||
|
return {
|
||||||
|
"level": self.level,
|
||||||
|
"code": self.code,
|
||||||
|
"reason": self.reason,
|
||||||
|
"affected": self.affected,
|
||||||
|
"source": self.source if self.source else self.affected,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -13,10 +13,23 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError, Codes
|
|
||||||
from synapse.util.jsonobject import JsonEncodedObject
|
from synapse.util.jsonobject import JsonEncodedObject
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_event(hs, e):
|
||||||
|
# FIXME(erikj): To handle the case of presence events and the like
|
||||||
|
if not isinstance(e, SynapseEvent):
|
||||||
|
return e
|
||||||
|
|
||||||
|
# Should this strip out None's?
|
||||||
|
d = {k: v for k, v in e.get_dict().items()}
|
||||||
|
if "age_ts" in d:
|
||||||
|
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
|
||||||
|
del d["age_ts"]
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
class SynapseEvent(JsonEncodedObject):
|
class SynapseEvent(JsonEncodedObject):
|
||||||
|
|
||||||
"""Base class for Synapse events. These are JSON objects which must abide
|
"""Base class for Synapse events. These are JSON objects which must abide
|
||||||
@@ -42,16 +55,26 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
"user_id", # sender/initiator
|
"user_id", # sender/initiator
|
||||||
"content", # HTTP body, JSON
|
"content", # HTTP body, JSON
|
||||||
"state_key",
|
"state_key",
|
||||||
|
"age_ts",
|
||||||
|
"prev_content",
|
||||||
|
"replaces_state",
|
||||||
|
"redacted_because",
|
||||||
|
"origin_server_ts",
|
||||||
]
|
]
|
||||||
|
|
||||||
internal_keys = [
|
internal_keys = [
|
||||||
"is_state",
|
"is_state",
|
||||||
"prev_events",
|
|
||||||
"prev_state",
|
|
||||||
"depth",
|
"depth",
|
||||||
"destinations",
|
"destinations",
|
||||||
"origin",
|
"origin",
|
||||||
"outlier",
|
"outlier",
|
||||||
|
"redacted",
|
||||||
|
"prev_events",
|
||||||
|
"hashes",
|
||||||
|
"signatures",
|
||||||
|
"prev_state",
|
||||||
|
"auth_events",
|
||||||
|
"state_hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
@@ -60,10 +83,12 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
"content",
|
"content",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
outlier = False
|
||||||
|
|
||||||
def __init__(self, raises=True, **kwargs):
|
def __init__(self, raises=True, **kwargs):
|
||||||
super(SynapseEvent, self).__init__(**kwargs)
|
super(SynapseEvent, self).__init__(**kwargs)
|
||||||
if "content" in kwargs:
|
# if "content" in kwargs:
|
||||||
self.check_json(self.content, raises=raises)
|
# self.check_json(self.content, raises=raises)
|
||||||
|
|
||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
""" Retrieve the JSON template for this event as a dict.
|
""" Retrieve the JSON template for this event as a dict.
|
||||||
@@ -94,61 +119,30 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError("get_content_template not implemented.")
|
raise NotImplementedError("get_content_template not implemented.")
|
||||||
|
|
||||||
def check_json(self, content, raises=True):
|
def get_pdu_json(self, time_now=None):
|
||||||
"""Checks the given JSON content abides by the rules of the template.
|
pdu_json = self.get_full_dict()
|
||||||
|
pdu_json.pop("destinations", None)
|
||||||
|
pdu_json.pop("outlier", None)
|
||||||
|
pdu_json.pop("replaces_state", None)
|
||||||
|
pdu_json.pop("redacted", None)
|
||||||
|
pdu_json.pop("prev_content", None)
|
||||||
|
state_hash = pdu_json.pop("state_hash", None)
|
||||||
|
if state_hash is not None:
|
||||||
|
pdu_json.setdefault("unsigned", {})["state_hash"] = state_hash
|
||||||
|
content = pdu_json.get("content", {})
|
||||||
|
content.pop("prev", None)
|
||||||
|
if time_now is not None and "age_ts" in pdu_json:
|
||||||
|
age = time_now - pdu_json["age_ts"]
|
||||||
|
pdu_json.setdefault("unsigned", {})["age"] = int(age)
|
||||||
|
del pdu_json["age_ts"]
|
||||||
|
user_id = pdu_json.pop("user_id")
|
||||||
|
pdu_json["sender"] = user_id
|
||||||
|
return pdu_json
|
||||||
|
|
||||||
Args:
|
|
||||||
content : A JSON object to check.
|
|
||||||
raises: True to raise a SynapseError if the check fails.
|
|
||||||
Returns:
|
|
||||||
True if the content passes the template. Returns False if the check
|
|
||||||
fails and raises=False.
|
|
||||||
Raises:
|
|
||||||
SynapseError if the check fails and raises=True.
|
|
||||||
"""
|
|
||||||
# recursively call to inspect each layer
|
|
||||||
err_msg = self._check_json(content, self.get_content_template())
|
|
||||||
if err_msg:
|
|
||||||
if raises:
|
|
||||||
raise SynapseError(400, err_msg, Codes.BAD_JSON)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _check_json(self, content, template):
|
class SynapseStateEvent(SynapseEvent):
|
||||||
"""Check content and template matches.
|
|
||||||
|
|
||||||
If the template is a dict, each key in the dict will be validated with
|
def __init__(self, **kwargs):
|
||||||
the content, else it will just compare the types of content and
|
if "state_key" not in kwargs:
|
||||||
template. This basic type check is required because this function will
|
kwargs["state_key"] = ""
|
||||||
be recursively called and could be called with just strs or ints.
|
super(SynapseStateEvent, self).__init__(**kwargs)
|
||||||
|
|
||||||
Args:
|
|
||||||
content: The content to validate.
|
|
||||||
template: The validation template.
|
|
||||||
Returns:
|
|
||||||
str: An error message if the validation fails, else None.
|
|
||||||
"""
|
|
||||||
if type(content) != type(template):
|
|
||||||
return "Mismatched types: %s" % template
|
|
||||||
|
|
||||||
if type(template) == dict:
|
|
||||||
for key in template:
|
|
||||||
if key not in content:
|
|
||||||
return "Missing %s key" % key
|
|
||||||
|
|
||||||
if type(content[key]) != type(template[key]):
|
|
||||||
return "Key %s is of the wrong type." % key
|
|
||||||
|
|
||||||
if type(content[key]) == dict:
|
|
||||||
# we must go deeper
|
|
||||||
msg = self._check_json(content[key], template[key])
|
|
||||||
if msg:
|
|
||||||
return msg
|
|
||||||
elif type(content[key]) == list:
|
|
||||||
# make sure each item type in content matches the template
|
|
||||||
for entry in content[key]:
|
|
||||||
msg = self._check_json(entry, template[key][0])
|
|
||||||
if msg:
|
|
||||||
return msg
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -16,8 +16,13 @@
|
|||||||
from synapse.api.events.room import (
|
from synapse.api.events.room import (
|
||||||
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
||||||
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
||||||
|
RoomPowerLevelsEvent, RoomJoinRulesEvent,
|
||||||
|
RoomCreateEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from synapse.types import EventID
|
||||||
|
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +35,11 @@ class EventFactory(object):
|
|||||||
RoomMemberEvent,
|
RoomMemberEvent,
|
||||||
FeedbackEvent,
|
FeedbackEvent,
|
||||||
InviteJoinEvent,
|
InviteJoinEvent,
|
||||||
RoomConfigEvent
|
RoomConfigEvent,
|
||||||
|
RoomPowerLevelsEvent,
|
||||||
|
RoomJoinRulesEvent,
|
||||||
|
RoomCreateEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
@@ -39,14 +48,39 @@ class EventFactory(object):
|
|||||||
self._event_list[event_class.TYPE] = event_class
|
self._event_list[event_class.TYPE] = event_class
|
||||||
|
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
|
self.hs = hs
|
||||||
|
|
||||||
|
self.event_id_count = 0
|
||||||
|
|
||||||
|
def create_event_id(self):
|
||||||
|
i = str(self.event_id_count)
|
||||||
|
self.event_id_count += 1
|
||||||
|
|
||||||
|
local_part = str(int(self.clock.time())) + i + random_string(5)
|
||||||
|
|
||||||
|
e_id = EventID.create_local(local_part, self.hs)
|
||||||
|
|
||||||
|
return e_id.to_string()
|
||||||
|
|
||||||
def create_event(self, etype=None, **kwargs):
|
def create_event(self, etype=None, **kwargs):
|
||||||
kwargs["type"] = etype
|
kwargs["type"] = etype
|
||||||
if "event_id" not in kwargs:
|
if "event_id" not in kwargs:
|
||||||
kwargs["event_id"] = random_string(10)
|
kwargs["event_id"] = self.create_event_id()
|
||||||
|
kwargs["origin"] = self.hs.hostname
|
||||||
|
else:
|
||||||
|
ev_id = self.hs.parse_eventid(kwargs["event_id"])
|
||||||
|
kwargs["origin"] = ev_id.domain
|
||||||
|
|
||||||
if "ts" not in kwargs:
|
if "origin_server_ts" not in kwargs:
|
||||||
kwargs["ts"] = int(self.clock.time_msec())
|
kwargs["origin_server_ts"] = int(self.clock.time_msec())
|
||||||
|
|
||||||
|
# The "age" key is a delta timestamp that should be converted into an
|
||||||
|
# absolute timestamp the minute we see it.
|
||||||
|
if "age" in kwargs:
|
||||||
|
kwargs["age_ts"] = int(self.clock.time_msec()) - int(kwargs["age"])
|
||||||
|
del kwargs["age"]
|
||||||
|
elif "age_ts" not in kwargs:
|
||||||
|
kwargs["age_ts"] = int(self.clock.time_msec())
|
||||||
|
|
||||||
if etype in self._event_list:
|
if etype in self._event_list:
|
||||||
handler = self._event_list[etype]
|
handler = self._event_list[etype]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
from synapse.api.constants import Feedback, Membership
|
from synapse.api.constants import Feedback, Membership
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from . import SynapseEvent
|
from . import SynapseEvent, SynapseStateEvent
|
||||||
|
|
||||||
|
|
||||||
class GenericEvent(SynapseEvent):
|
class GenericEvent(SynapseEvent):
|
||||||
@@ -103,8 +103,7 @@ class FeedbackEvent(SynapseEvent):
|
|||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
return {
|
return {
|
||||||
"type": u"string",
|
"type": u"string",
|
||||||
"target_event_id": u"string",
|
"target_event_id": u"string"
|
||||||
"msg_sender_id": u"string"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -132,3 +131,40 @@ class RoomConfigEvent(SynapseEvent):
|
|||||||
|
|
||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomCreateEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.create"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomJoinRulesEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.join_rules"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomPowerLevelsEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.power_levels"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomAliasesEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.aliases"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomRedactionEvent(SynapseEvent):
|
||||||
|
TYPE = "m.room.redaction"
|
||||||
|
|
||||||
|
valid_keys = SynapseEvent.valid_keys + ["redacts"]
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|||||||
85
synapse/api/events/utils.py
Normal file
85
synapse/api/events/utils.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .room import (
|
||||||
|
RoomMemberEvent, RoomJoinRulesEvent, RoomPowerLevelsEvent,
|
||||||
|
RoomAliasesEvent, RoomCreateEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prune_event(event):
|
||||||
|
""" Returns a pruned version of the given event, which removes all keys we
|
||||||
|
don't know about or think could potentially be dodgy.
|
||||||
|
|
||||||
|
This is used when we "redact" an event. We want to remove all fields that
|
||||||
|
the user has specified, but we do want to keep necessary information like
|
||||||
|
type, state_key etc.
|
||||||
|
"""
|
||||||
|
event_type = event.type
|
||||||
|
|
||||||
|
allowed_keys = [
|
||||||
|
"event_id",
|
||||||
|
"user_id",
|
||||||
|
"room_id",
|
||||||
|
"hashes",
|
||||||
|
"signatures",
|
||||||
|
"content",
|
||||||
|
"type",
|
||||||
|
"state_key",
|
||||||
|
"depth",
|
||||||
|
"prev_events",
|
||||||
|
"prev_state",
|
||||||
|
"auth_events",
|
||||||
|
"origin",
|
||||||
|
"origin_server_ts",
|
||||||
|
]
|
||||||
|
|
||||||
|
new_content = {}
|
||||||
|
|
||||||
|
def add_fields(*fields):
|
||||||
|
for field in fields:
|
||||||
|
if field in event.content:
|
||||||
|
new_content[field] = event.content[field]
|
||||||
|
|
||||||
|
if event_type == RoomMemberEvent.TYPE:
|
||||||
|
add_fields("membership")
|
||||||
|
elif event_type == RoomCreateEvent.TYPE:
|
||||||
|
add_fields("creator")
|
||||||
|
elif event_type == RoomJoinRulesEvent.TYPE:
|
||||||
|
add_fields("join_rule")
|
||||||
|
elif event_type == RoomPowerLevelsEvent.TYPE:
|
||||||
|
add_fields(
|
||||||
|
"users",
|
||||||
|
"users_default",
|
||||||
|
"events",
|
||||||
|
"events_default",
|
||||||
|
"events_default",
|
||||||
|
"state_default",
|
||||||
|
"ban",
|
||||||
|
"kick",
|
||||||
|
"redact",
|
||||||
|
)
|
||||||
|
elif event_type == RoomAliasesEvent.TYPE:
|
||||||
|
add_fields("aliases")
|
||||||
|
|
||||||
|
allowed_fields = {
|
||||||
|
k: v
|
||||||
|
for k, v in event.get_full_dict().items()
|
||||||
|
if k in allowed_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
allowed_fields["content"] = new_content
|
||||||
|
|
||||||
|
return type(event)(**allowed_fields)
|
||||||
87
synapse/api/events/validator.py
Normal file
87
synapse/api/events/validator.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from synapse.api.errors import SynapseError, Codes
|
||||||
|
|
||||||
|
|
||||||
|
class EventValidator(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def validate(self, event):
|
||||||
|
"""Checks the given JSON content abides by the rules of the template.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content : A JSON object to check.
|
||||||
|
raises: True to raise a SynapseError if the check fails.
|
||||||
|
Returns:
|
||||||
|
True if the content passes the template. Returns False if the check
|
||||||
|
fails and raises=False.
|
||||||
|
Raises:
|
||||||
|
SynapseError if the check fails and raises=True.
|
||||||
|
"""
|
||||||
|
# recursively call to inspect each layer
|
||||||
|
err_msg = self._check_json_template(
|
||||||
|
event.content,
|
||||||
|
event.get_content_template()
|
||||||
|
)
|
||||||
|
if err_msg:
|
||||||
|
raise SynapseError(400, err_msg, Codes.BAD_JSON)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _check_json_template(self, content, template):
|
||||||
|
"""Check content and template matches.
|
||||||
|
|
||||||
|
If the template is a dict, each key in the dict will be validated with
|
||||||
|
the content, else it will just compare the types of content and
|
||||||
|
template. This basic type check is required because this function will
|
||||||
|
be recursively called and could be called with just strs or ints.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: The content to validate.
|
||||||
|
template: The validation template.
|
||||||
|
Returns:
|
||||||
|
str: An error message if the validation fails, else None.
|
||||||
|
"""
|
||||||
|
if type(content) != type(template):
|
||||||
|
return "Mismatched types: %s" % template
|
||||||
|
|
||||||
|
if type(template) == dict:
|
||||||
|
for key in template:
|
||||||
|
if key not in content:
|
||||||
|
return "Missing %s key" % key
|
||||||
|
|
||||||
|
if type(content[key]) != type(template[key]):
|
||||||
|
return "Key %s is of the wrong type (got %s, want %s)" % (
|
||||||
|
key, type(content[key]), type(template[key]))
|
||||||
|
|
||||||
|
if type(content[key]) == dict:
|
||||||
|
# we must go deeper
|
||||||
|
msg = self._check_json_template(
|
||||||
|
content[key],
|
||||||
|
template[key]
|
||||||
|
)
|
||||||
|
if msg:
|
||||||
|
return msg
|
||||||
|
elif type(content[key]) == list:
|
||||||
|
# make sure each item type in content matches the template
|
||||||
|
for entry in content[key]:
|
||||||
|
msg = self._check_json_template(
|
||||||
|
entry,
|
||||||
|
template[key][0]
|
||||||
|
)
|
||||||
|
if msg:
|
||||||
|
return msg
|
||||||
79
synapse/api/ratelimiting.py
Normal file
79
synapse/api/ratelimiting.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
class Ratelimiter(object):
|
||||||
|
"""
|
||||||
|
Ratelimit message sending by user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.message_counts = collections.OrderedDict()
|
||||||
|
|
||||||
|
def send_message(self, user_id, time_now_s, msg_rate_hz, burst_count):
|
||||||
|
"""Can the user send a message?
|
||||||
|
Args:
|
||||||
|
user_id: The user sending a message.
|
||||||
|
time_now_s: The time now.
|
||||||
|
msg_rate_hz: The long term number of messages a user can send in a
|
||||||
|
second.
|
||||||
|
burst_count: How many messages the user can send before being
|
||||||
|
limited.
|
||||||
|
Returns:
|
||||||
|
A pair of a bool indicating if they can send a message now and a
|
||||||
|
time in seconds of when they can next send a message.
|
||||||
|
"""
|
||||||
|
self.prune_message_counts(time_now_s)
|
||||||
|
message_count, time_start, _ignored = self.message_counts.pop(
|
||||||
|
user_id, (0., time_now_s, None),
|
||||||
|
)
|
||||||
|
time_delta = time_now_s - time_start
|
||||||
|
sent_count = message_count - time_delta * msg_rate_hz
|
||||||
|
if sent_count < 0:
|
||||||
|
allowed = True
|
||||||
|
time_start = time_now_s
|
||||||
|
message_count = 1.
|
||||||
|
elif sent_count > burst_count - 1.:
|
||||||
|
allowed = False
|
||||||
|
else:
|
||||||
|
allowed = True
|
||||||
|
message_count += 1
|
||||||
|
|
||||||
|
self.message_counts[user_id] = (
|
||||||
|
message_count, time_start, msg_rate_hz
|
||||||
|
)
|
||||||
|
|
||||||
|
if msg_rate_hz > 0:
|
||||||
|
time_allowed = (
|
||||||
|
time_start + (message_count - burst_count + 1) / msg_rate_hz
|
||||||
|
)
|
||||||
|
if time_allowed < time_now_s:
|
||||||
|
time_allowed = time_now_s
|
||||||
|
else:
|
||||||
|
time_allowed = -1
|
||||||
|
|
||||||
|
return allowed, time_allowed
|
||||||
|
|
||||||
|
def prune_message_counts(self, time_now_s):
|
||||||
|
for user_id in self.message_counts.keys():
|
||||||
|
message_count, time_start, msg_rate_hz = (
|
||||||
|
self.message_counts[user_id]
|
||||||
|
)
|
||||||
|
time_delta = time_now_s - time_start
|
||||||
|
if message_count - time_delta * msg_rate_hz > 0:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
del self.message_counts[user_id]
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
"""Contains the URL paths to prefix various aspects of the server with. """
|
"""Contains the URL paths to prefix various aspects of the server with. """
|
||||||
|
|
||||||
CLIENT_PREFIX = "/matrix/client/api/v1"
|
CLIENT_PREFIX = "/_matrix/client/api/v1"
|
||||||
FEDERATION_PREFIX = "/matrix/federation/v1"
|
FEDERATION_PREFIX = "/_matrix/federation/v1"
|
||||||
WEB_CLIENT_PREFIX = "/matrix/client"
|
WEB_CLIENT_PREFIX = "/_matrix/client"
|
||||||
CONTENT_REPO_PREFIX = "/matrix/content"
|
CONTENT_REPO_PREFIX = "/_matrix/content"
|
||||||
|
SERVER_KEY_PREFIX = "/_matrix/key/v1"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -12,4 +12,3 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,55 +14,44 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from synapse.storage import read_schema
|
from synapse.storage import prepare_database
|
||||||
|
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.enterprise import adbapi
|
from twisted.enterprise import adbapi
|
||||||
from twisted.python.log import PythonLoggingObserver
|
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
from twisted.web.static import File
|
from twisted.web.static import File
|
||||||
from twisted.web.server import Site
|
from twisted.web.server import Site
|
||||||
from synapse.http.server import JsonResource, RootRedirect, ContentRepoResource
|
from synapse.http.server import JsonResource, RootRedirect
|
||||||
from synapse.http.client import TwistedHttpClient
|
from synapse.http.content_repository import ContentRepoResource
|
||||||
|
from synapse.http.server_key_resource import LocalKey
|
||||||
|
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
||||||
from synapse.api.urls import (
|
from synapse.api.urls import (
|
||||||
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
|
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
|
||||||
|
SERVER_KEY_PREFIX,
|
||||||
)
|
)
|
||||||
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
|
from synapse.crypto import context_factory
|
||||||
|
from synapse.util.logcontext import LoggingContext
|
||||||
|
|
||||||
from daemonize import Daemonize
|
from daemonize import Daemonize
|
||||||
import twisted.manhole.telnet
|
import twisted.manhole.telnet
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
|
||||||
import sqlite3
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
import sqlite3
|
||||||
|
import syweb
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
SCHEMAS = [
|
|
||||||
"transactions",
|
|
||||||
"pdu",
|
|
||||||
"users",
|
|
||||||
"profiles",
|
|
||||||
"presence",
|
|
||||||
"im",
|
|
||||||
"room_aliases",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Remember to update this number every time an incompatible change is made to
|
|
||||||
# database schema files, so the users will be informed on server restarts.
|
|
||||||
SCHEMA_VERSION = 1
|
|
||||||
|
|
||||||
|
|
||||||
class SynapseHomeServer(HomeServer):
|
class SynapseHomeServer(HomeServer):
|
||||||
|
|
||||||
def build_http_client(self):
|
def build_http_client(self):
|
||||||
return TwistedHttpClient()
|
return MatrixFederationHttpClient(self)
|
||||||
|
|
||||||
def build_resource_for_client(self):
|
def build_resource_for_client(self):
|
||||||
return JsonResource()
|
return JsonResource()
|
||||||
@@ -71,50 +60,25 @@ class SynapseHomeServer(HomeServer):
|
|||||||
return JsonResource()
|
return JsonResource()
|
||||||
|
|
||||||
def build_resource_for_web_client(self):
|
def build_resource_for_web_client(self):
|
||||||
return File("webclient") # TODO configurable?
|
syweb_path = os.path.dirname(syweb.__file__)
|
||||||
|
webclient_path = os.path.join(syweb_path, "webclient")
|
||||||
|
return File(webclient_path) # TODO configurable?
|
||||||
|
|
||||||
def build_resource_for_content_repo(self):
|
def build_resource_for_content_repo(self):
|
||||||
return ContentRepoResource(self, self.upload_dir, self.auth)
|
return ContentRepoResource(
|
||||||
|
self, self.upload_dir, self.auth, self.content_addr
|
||||||
|
)
|
||||||
|
|
||||||
|
def build_resource_for_server_key(self):
|
||||||
|
return LocalKey(self)
|
||||||
|
|
||||||
def build_db_pool(self):
|
def build_db_pool(self):
|
||||||
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
return adbapi.ConnectionPool(
|
||||||
don't have to worry about overwriting existing content.
|
"sqlite3", self.get_db_name(),
|
||||||
"""
|
check_same_thread=False,
|
||||||
logging.info("Preparing database: %s...", self.db_name)
|
cp_min=1,
|
||||||
|
cp_max=1
|
||||||
with sqlite3.connect(self.db_name) as db_conn:
|
)
|
||||||
c = db_conn.cursor()
|
|
||||||
c.execute("PRAGMA user_version")
|
|
||||||
row = c.fetchone()
|
|
||||||
|
|
||||||
if row and row[0]:
|
|
||||||
user_version = row[0]
|
|
||||||
|
|
||||||
if user_version < SCHEMA_VERSION:
|
|
||||||
# TODO(paul): add some kind of intelligent fixup here
|
|
||||||
raise ValueError("Cannot use this database as the " +
|
|
||||||
"schema version (%d) does not match (%d)" %
|
|
||||||
(user_version, SCHEMA_VERSION)
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
for sql_loc in SCHEMAS:
|
|
||||||
sql_script = read_schema(sql_loc)
|
|
||||||
|
|
||||||
c.executescript(sql_script)
|
|
||||||
db_conn.commit()
|
|
||||||
|
|
||||||
c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
|
|
||||||
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
logging.info("Database prepared in %s.", self.db_name)
|
|
||||||
|
|
||||||
pool = adbapi.ConnectionPool(
|
|
||||||
'sqlite3', self.db_name, check_same_thread=False,
|
|
||||||
cp_min=1, cp_max=1)
|
|
||||||
|
|
||||||
return pool
|
|
||||||
|
|
||||||
def create_resource_tree(self, web_client, redirect_root_to_web_client):
|
def create_resource_tree(self, web_client, redirect_root_to_web_client):
|
||||||
"""Create the resource tree for this Home Server.
|
"""Create the resource tree for this Home Server.
|
||||||
@@ -133,7 +97,8 @@ class SynapseHomeServer(HomeServer):
|
|||||||
desired_tree = [
|
desired_tree = [
|
||||||
(CLIENT_PREFIX, self.get_resource_for_client()),
|
(CLIENT_PREFIX, self.get_resource_for_client()),
|
||||||
(FEDERATION_PREFIX, self.get_resource_for_federation()),
|
(FEDERATION_PREFIX, self.get_resource_for_federation()),
|
||||||
(CONTENT_REPO_PREFIX, self.get_resource_for_content_repo())
|
(CONTENT_REPO_PREFIX, self.get_resource_for_content_repo()),
|
||||||
|
(SERVER_KEY_PREFIX, self.get_resource_for_server_key()),
|
||||||
]
|
]
|
||||||
if web_client:
|
if web_client:
|
||||||
logger.info("Adding the web client.")
|
logger.info("Adding the web client.")
|
||||||
@@ -151,7 +116,7 @@ class SynapseHomeServer(HomeServer):
|
|||||||
# extra resources to existing nodes. See self._resource_id for the key.
|
# extra resources to existing nodes. See self._resource_id for the key.
|
||||||
resource_mappings = {}
|
resource_mappings = {}
|
||||||
for (full_path, resource) in desired_tree:
|
for (full_path, resource) in desired_tree:
|
||||||
logging.info("Attaching %s to path %s", resource, full_path)
|
logger.info("Attaching %s to path %s", resource, full_path)
|
||||||
last_resource = self.root_resource
|
last_resource = self.root_resource
|
||||||
for path_seg in full_path.split('/')[1:-1]:
|
for path_seg in full_path.split('/')[1:-1]:
|
||||||
if not path_seg in last_resource.listNames():
|
if not path_seg in last_resource.listNames():
|
||||||
@@ -206,116 +171,82 @@ class SynapseHomeServer(HomeServer):
|
|||||||
"""
|
"""
|
||||||
return "%s-%s" % (resource, path_seg)
|
return "%s-%s" % (resource, path_seg)
|
||||||
|
|
||||||
def start_listening(self, port):
|
def start_listening(self, secure_port, unsecure_port):
|
||||||
reactor.listenTCP(port, Site(self.root_resource))
|
if secure_port is not None:
|
||||||
logger.info("Synapse now listening on port %d", port)
|
reactor.listenSSL(
|
||||||
|
secure_port, Site(self.root_resource), self.tls_context_factory
|
||||||
|
)
|
||||||
def setup_logging(verbosity=0, filename=None, config_path=None):
|
logger.info("Synapse now listening on port %d", secure_port)
|
||||||
""" Sets up logging with verbosity levels.
|
if unsecure_port is not None:
|
||||||
|
reactor.listenTCP(
|
||||||
Args:
|
unsecure_port, Site(self.root_resource)
|
||||||
verbosity: The verbosity level.
|
)
|
||||||
filename: Log to the given file rather than to the console.
|
logger.info("Synapse now listening on port %d", unsecure_port)
|
||||||
config_path: Path to a python logging config file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if config_path is None:
|
|
||||||
log_format = (
|
|
||||||
'%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
|
|
||||||
)
|
|
||||||
|
|
||||||
level = logging.INFO
|
|
||||||
if verbosity:
|
|
||||||
level = logging.DEBUG
|
|
||||||
|
|
||||||
# FIXME: we need a logging.WARN for a -q quiet option
|
|
||||||
|
|
||||||
logging.basicConfig(level=level, filename=filename, format=log_format)
|
|
||||||
else:
|
|
||||||
logging.config.fileConfig(config_path)
|
|
||||||
|
|
||||||
observer = PythonLoggingObserver()
|
|
||||||
observer.start()
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
def setup():
|
||||||
parser = argparse.ArgumentParser()
|
config = HomeServerConfig.load_config(
|
||||||
parser.add_argument("-p", "--port", dest="port", type=int, default=8080,
|
"Synapse Homeserver",
|
||||||
help="The port to listen on.")
|
sys.argv[1:],
|
||||||
parser.add_argument("-d", "--database", dest="db", default="homeserver.db",
|
generate_section="Homeserver"
|
||||||
help="The database name.")
|
|
||||||
parser.add_argument("-H", "--host", dest="host", default="localhost",
|
|
||||||
help="The hostname of the server.")
|
|
||||||
parser.add_argument('-v', '--verbose', dest="verbose", action='count',
|
|
||||||
help="The verbosity level.")
|
|
||||||
parser.add_argument('-f', '--log-file', dest="log_file", default=None,
|
|
||||||
help="File to log to.")
|
|
||||||
parser.add_argument('--log-config', dest="log_config", default=None,
|
|
||||||
help="Python logging config")
|
|
||||||
parser.add_argument('-D', '--daemonize', action='store_true',
|
|
||||||
default=False, help="Daemonize the home server")
|
|
||||||
parser.add_argument('--pid-file', dest="pid", help="When running as a "
|
|
||||||
"daemon, the file to store the pid in",
|
|
||||||
default="hs.pid")
|
|
||||||
parser.add_argument("-W", "--webclient", dest="webclient", default=True,
|
|
||||||
action="store_false", help="Don't host a web client.")
|
|
||||||
parser.add_argument("--manhole", dest="manhole", type=int, default=None,
|
|
||||||
help="Turn on the twisted telnet manhole service.")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
verbosity = int(args.verbose) if args.verbose else None
|
|
||||||
|
|
||||||
# Because if/when we daemonize we change to root dir.
|
|
||||||
db_name = os.path.abspath(args.db)
|
|
||||||
log_file = args.log_file
|
|
||||||
if log_file:
|
|
||||||
log_file = os.path.abspath(log_file)
|
|
||||||
|
|
||||||
setup_logging(
|
|
||||||
verbosity=verbosity,
|
|
||||||
filename=log_file,
|
|
||||||
config_path=args.log_config,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Server hostname: %s", args.host)
|
config.setup_logging()
|
||||||
|
|
||||||
if re.search(":[0-9]+$", args.host):
|
logger.info("Server hostname: %s", config.server_name)
|
||||||
domain_with_port = args.host
|
|
||||||
|
if re.search(":[0-9]+$", config.server_name):
|
||||||
|
domain_with_port = config.server_name
|
||||||
else:
|
else:
|
||||||
domain_with_port = "%s:%s" % (args.host, args.port)
|
domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
|
||||||
|
|
||||||
|
tls_context_factory = context_factory.ServerContextFactory(config)
|
||||||
|
|
||||||
hs = SynapseHomeServer(
|
hs = SynapseHomeServer(
|
||||||
args.host,
|
config.server_name,
|
||||||
domain_with_port=domain_with_port,
|
domain_with_port=domain_with_port,
|
||||||
upload_dir=os.path.abspath("uploads"),
|
upload_dir=os.path.abspath("uploads"),
|
||||||
db_name=db_name,
|
db_name=config.database_path,
|
||||||
|
tls_context_factory=tls_context_factory,
|
||||||
|
config=config,
|
||||||
|
content_addr=config.content_addr,
|
||||||
)
|
)
|
||||||
|
|
||||||
hs.register_servlets()
|
hs.register_servlets()
|
||||||
|
|
||||||
hs.create_resource_tree(
|
hs.create_resource_tree(
|
||||||
web_client=args.webclient,
|
web_client=config.webclient,
|
||||||
redirect_root_to_web_client=True)
|
redirect_root_to_web_client=True,
|
||||||
hs.start_listening(args.port)
|
)
|
||||||
|
|
||||||
|
db_name = hs.get_db_name()
|
||||||
|
|
||||||
|
logger.info("Preparing database: %s...", db_name)
|
||||||
|
|
||||||
|
with sqlite3.connect(db_name) as db_conn:
|
||||||
|
prepare_database(db_conn)
|
||||||
|
|
||||||
|
logger.info("Database prepared in %s.", db_name)
|
||||||
|
|
||||||
hs.get_db_pool()
|
hs.get_db_pool()
|
||||||
|
|
||||||
if args.manhole:
|
if config.manhole:
|
||||||
f = twisted.manhole.telnet.ShellFactory()
|
f = twisted.manhole.telnet.ShellFactory()
|
||||||
f.username = "matrix"
|
f.username = "matrix"
|
||||||
f.password = "rabbithole"
|
f.password = "rabbithole"
|
||||||
f.namespace['hs'] = hs
|
f.namespace['hs'] = hs
|
||||||
reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
|
reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
|
||||||
|
|
||||||
if args.daemonize:
|
bind_port = config.bind_port
|
||||||
|
if config.no_tls:
|
||||||
|
bind_port = None
|
||||||
|
hs.start_listening(bind_port, config.unsecure_port)
|
||||||
|
|
||||||
|
if config.daemonize:
|
||||||
|
print config.pid_file
|
||||||
daemon = Daemonize(
|
daemon = Daemonize(
|
||||||
app="synapse-homeserver",
|
app="synapse-homeserver",
|
||||||
pid=args.pid,
|
pid=config.pid_file,
|
||||||
action=run,
|
action=run,
|
||||||
auto_close_fds=False,
|
auto_close_fds=False,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
@@ -324,8 +255,18 @@ def setup():
|
|||||||
|
|
||||||
daemon.start()
|
daemon.start()
|
||||||
else:
|
else:
|
||||||
run()
|
reactor.run()
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
with LoggingContext("run"):
|
||||||
|
reactor.run()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with LoggingContext("main"):
|
||||||
|
setup()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
setup()
|
main()
|
||||||
|
|||||||
70
synapse/app/synctl.py
Executable file
70
synapse/app/synctl.py
Executable file
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import signal
|
||||||
|
|
||||||
|
SYNAPSE = ["python", "-m", "synapse.app.homeserver"]
|
||||||
|
|
||||||
|
CONFIGFILE = "homeserver.yaml"
|
||||||
|
PIDFILE = "homeserver.pid"
|
||||||
|
|
||||||
|
GREEN = "\x1b[1;32m"
|
||||||
|
NORMAL = "\x1b[m"
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
if not os.path.exists(CONFIGFILE):
|
||||||
|
sys.stderr.write(
|
||||||
|
"No config file found\n"
|
||||||
|
"To generate a config file, run '%s -c %s --generate-config"
|
||||||
|
" --server-name=<server name>'\n" % (
|
||||||
|
" ".join(SYNAPSE), CONFIGFILE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
print "Starting ...",
|
||||||
|
args = SYNAPSE
|
||||||
|
args.extend(["--daemonize", "-c", CONFIGFILE, "--pid-file", PIDFILE])
|
||||||
|
subprocess.check_call(args)
|
||||||
|
print GREEN + "started" + NORMAL
|
||||||
|
|
||||||
|
|
||||||
|
def stop():
|
||||||
|
if os.path.exists(PIDFILE):
|
||||||
|
pid = int(open(PIDFILE).read())
|
||||||
|
os.kill(pid, signal.SIGTERM)
|
||||||
|
print GREEN + "stopped" + NORMAL
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
action = sys.argv[1] if sys.argv[1:] else "usage"
|
||||||
|
if action == "start":
|
||||||
|
start()
|
||||||
|
elif action == "stop":
|
||||||
|
stop()
|
||||||
|
elif action == "restart":
|
||||||
|
stop()
|
||||||
|
start()
|
||||||
|
else:
|
||||||
|
sys.stderr.write("Usage: %s [start|stop|restart]\n" % (sys.argv[0],))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -12,4 +12,3 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
143
synapse/config/_base.py
Normal file
143
synapse/config/_base.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def abspath(file_path):
|
||||||
|
return os.path.abspath(file_path) if file_path else file_path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_file(cls, file_path, config_name):
|
||||||
|
if file_path is None:
|
||||||
|
raise ConfigError(
|
||||||
|
"Missing config for %s."
|
||||||
|
" You must specify a path for the config file. You can "
|
||||||
|
"do this with the -c or --config-path option. "
|
||||||
|
"Adding --generate-config along with --server-name "
|
||||||
|
"<server name> will generate a config file at the given path."
|
||||||
|
% (config_name,)
|
||||||
|
)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise ConfigError(
|
||||||
|
"File % config for %s doesn't exist."
|
||||||
|
" Try running again with --generate-config"
|
||||||
|
% (config_name,)
|
||||||
|
)
|
||||||
|
return cls.abspath(file_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_file(cls, file_path, config_name):
|
||||||
|
cls.check_file(file_path, config_name)
|
||||||
|
with open(file_path) as file_stream:
|
||||||
|
return file_stream.read()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_config_file(file_path):
|
||||||
|
with open(file_path) as file_stream:
|
||||||
|
return yaml.load(file_stream)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_config(cls, args, config_dir_path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_config(cls, description, argv, generate_section=None):
|
||||||
|
config_parser = argparse.ArgumentParser(add_help=False)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"-c", "--config-path",
|
||||||
|
metavar="CONFIG_FILE",
|
||||||
|
help="Specify config file"
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"--generate-config",
|
||||||
|
action="store_true",
|
||||||
|
help="Generate config file"
|
||||||
|
)
|
||||||
|
config_args, remaining_args = config_parser.parse_known_args(argv)
|
||||||
|
|
||||||
|
if config_args.generate_config:
|
||||||
|
if not config_args.config_path:
|
||||||
|
config_parser.error(
|
||||||
|
"Must specify where to generate the config file"
|
||||||
|
)
|
||||||
|
config_dir_path = os.path.dirname(config_args.config_path)
|
||||||
|
if os.path.exists(config_args.config_path):
|
||||||
|
defaults = cls.read_config_file(config_args.config_path)
|
||||||
|
else:
|
||||||
|
defaults = {}
|
||||||
|
else:
|
||||||
|
if config_args.config_path:
|
||||||
|
defaults = cls.read_config_file(config_args.config_path)
|
||||||
|
else:
|
||||||
|
defaults = {}
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
parents=[config_parser],
|
||||||
|
description=description,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
cls.add_arguments(parser)
|
||||||
|
parser.set_defaults(**defaults)
|
||||||
|
|
||||||
|
args = parser.parse_args(remaining_args)
|
||||||
|
|
||||||
|
if config_args.generate_config:
|
||||||
|
config_dir_path = os.path.dirname(config_args.config_path)
|
||||||
|
config_dir_path = os.path.abspath(config_dir_path)
|
||||||
|
if not os.path.exists(config_dir_path):
|
||||||
|
os.makedirs(config_dir_path)
|
||||||
|
cls.generate_config(args, config_dir_path)
|
||||||
|
config = {}
|
||||||
|
for key, value in vars(args).items():
|
||||||
|
if (key not in set(["config_path", "generate_config"])
|
||||||
|
and value is not None):
|
||||||
|
config[key] = value
|
||||||
|
with open(config_args.config_path, "w") as config_file:
|
||||||
|
# TODO(paul) it would be lovely if we wrote out vim- and emacs-
|
||||||
|
# style mode markers into the file, to hint to people that
|
||||||
|
# this is a YAML file.
|
||||||
|
yaml.dump(config, config_file, default_flow_style=False)
|
||||||
|
print (
|
||||||
|
"A config file has been generated in %s for server name"
|
||||||
|
" '%s' with corresponding SSL keys and self-signed"
|
||||||
|
" certificates. Please review this file and customise it to"
|
||||||
|
" your needs."
|
||||||
|
) % (
|
||||||
|
config_args.config_path, config['server_name']
|
||||||
|
)
|
||||||
|
print (
|
||||||
|
"If this server name is incorrect, you will need to regenerate"
|
||||||
|
" the SSL certificates"
|
||||||
|
)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
return cls(args)
|
||||||
51
synapse/config/captcha.py
Normal file
51
synapse/config/captcha.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class CaptchaConfig(Config):
|
||||||
|
|
||||||
|
def __init__(self, args):
|
||||||
|
super(CaptchaConfig, self).__init__(args)
|
||||||
|
self.recaptcha_private_key = args.recaptcha_private_key
|
||||||
|
self.enable_registration_captcha = args.enable_registration_captcha
|
||||||
|
self.captcha_ip_origin_is_x_forwarded = (
|
||||||
|
args.captcha_ip_origin_is_x_forwarded
|
||||||
|
)
|
||||||
|
self.captcha_bypass_secret = args.captcha_bypass_secret
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(CaptchaConfig, cls).add_arguments(parser)
|
||||||
|
group = parser.add_argument_group("recaptcha")
|
||||||
|
group.add_argument(
|
||||||
|
"--recaptcha-private-key", type=str, default="YOUR_PRIVATE_KEY",
|
||||||
|
help="The matching private key for the web client's public key."
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--enable-registration-captcha", type=bool, default=False,
|
||||||
|
help="Enables ReCaptcha checks when registering, preventing signup"
|
||||||
|
+ " unless a captcha is answered. Requires a valid ReCaptcha "
|
||||||
|
+ "public/private key."
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--captcha_ip_origin_is_x_forwarded", type=bool, default=False,
|
||||||
|
help="When checking captchas, use the X-Forwarded-For (XFF) header"
|
||||||
|
+ " as the client IP and not the actual client IP."
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--captcha_bypass_secret", type=str,
|
||||||
|
help="A secret key used to bypass the captcha test entirely."
|
||||||
|
)
|
||||||
37
synapse/config/database.py
Normal file
37
synapse/config/database.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(DatabaseConfig, self).__init__(args)
|
||||||
|
self.database_path = self.abspath(args.database_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(DatabaseConfig, cls).add_arguments(parser)
|
||||||
|
db_group = parser.add_argument_group("database")
|
||||||
|
db_group.add_argument(
|
||||||
|
"-d", "--database-path", default="homeserver.db",
|
||||||
|
help="The database name."
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_config(cls, args, config_dir_path):
|
||||||
|
super(DatabaseConfig, cls).generate_config(args, config_dir_path)
|
||||||
|
args.database_path = os.path.abspath(args.database_path)
|
||||||
42
synapse/config/email.py
Normal file
42
synapse/config/email.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class EmailConfig(Config):
|
||||||
|
|
||||||
|
def __init__(self, args):
|
||||||
|
super(EmailConfig, self).__init__(args)
|
||||||
|
self.email_from_address = args.email_from_address
|
||||||
|
self.email_smtp_server = args.email_smtp_server
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(EmailConfig, cls).add_arguments(parser)
|
||||||
|
email_group = parser.add_argument_group("email")
|
||||||
|
email_group.add_argument(
|
||||||
|
"--email-from-address",
|
||||||
|
default="FROM@EXAMPLE.COM",
|
||||||
|
help="The address to send emails from (e.g. for password resets)."
|
||||||
|
)
|
||||||
|
email_group.add_argument(
|
||||||
|
"--email-smtp-server",
|
||||||
|
default="",
|
||||||
|
help=(
|
||||||
|
"The SMTP server to send emails from (e.g. for password"
|
||||||
|
" resets)."
|
||||||
|
)
|
||||||
|
)
|
||||||
35
synapse/config/homeserver.py
Normal file
35
synapse/config/homeserver.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .tls import TlsConfig
|
||||||
|
from .server import ServerConfig
|
||||||
|
from .logger import LoggingConfig
|
||||||
|
from .database import DatabaseConfig
|
||||||
|
from .ratelimiting import RatelimitConfig
|
||||||
|
from .repository import ContentRepositoryConfig
|
||||||
|
from .captcha import CaptchaConfig
|
||||||
|
from .email import EmailConfig
|
||||||
|
from .voip import VoipConfig
|
||||||
|
|
||||||
|
|
||||||
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||||
|
EmailConfig, VoipConfig):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
|
||||||
76
synapse/config/logger.py
Normal file
76
synapse/config/logger.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
from synapse.util.logcontext import LoggingContextFilter
|
||||||
|
from twisted.python.log import PythonLoggingObserver
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(LoggingConfig, self).__init__(args)
|
||||||
|
self.verbosity = int(args.verbose) if args.verbose else None
|
||||||
|
self.log_config = self.abspath(args.log_config)
|
||||||
|
self.log_file = self.abspath(args.log_file)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(LoggingConfig, cls).add_arguments(parser)
|
||||||
|
logging_group = parser.add_argument_group("logging")
|
||||||
|
logging_group.add_argument(
|
||||||
|
'-v', '--verbose', dest="verbose", action='count',
|
||||||
|
help="The verbosity level."
|
||||||
|
)
|
||||||
|
logging_group.add_argument(
|
||||||
|
'-f', '--log-file', dest="log_file", default=None,
|
||||||
|
help="File to log to."
|
||||||
|
)
|
||||||
|
logging_group.add_argument(
|
||||||
|
'--log-config', dest="log_config", default=None,
|
||||||
|
help="Python logging config file"
|
||||||
|
)
|
||||||
|
|
||||||
|
def setup_logging(self):
|
||||||
|
log_format = (
|
||||||
|
"%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s"
|
||||||
|
" - %(message)s"
|
||||||
|
)
|
||||||
|
if self.log_config is None:
|
||||||
|
|
||||||
|
level = logging.INFO
|
||||||
|
if self.verbosity:
|
||||||
|
level = logging.DEBUG
|
||||||
|
|
||||||
|
# FIXME: we need a logging.WARN for a -q quiet option
|
||||||
|
logger = logging.getLogger('')
|
||||||
|
logger.setLevel(level)
|
||||||
|
formatter = logging.Formatter(log_format)
|
||||||
|
if self.log_file:
|
||||||
|
handler = logging.FileHandler(self.log_file)
|
||||||
|
else:
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
handler.addFilter(LoggingContextFilter(request=""))
|
||||||
|
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.info("Test")
|
||||||
|
else:
|
||||||
|
logging.config.fileConfig(self.log_config)
|
||||||
|
|
||||||
|
observer = PythonLoggingObserver()
|
||||||
|
observer.start()
|
||||||
36
synapse/config/ratelimiting.py
Normal file
36
synapse/config/ratelimiting.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class RatelimitConfig(Config):
|
||||||
|
|
||||||
|
def __init__(self, args):
|
||||||
|
super(RatelimitConfig, self).__init__(args)
|
||||||
|
self.rc_messages_per_second = args.rc_messages_per_second
|
||||||
|
self.rc_message_burst_count = args.rc_message_burst_count
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(RatelimitConfig, cls).add_arguments(parser)
|
||||||
|
rc_group = parser.add_argument_group("ratelimiting")
|
||||||
|
rc_group.add_argument(
|
||||||
|
"--rc-messages-per-second", type=float, default=0.2,
|
||||||
|
help="number of messages a client can send per second"
|
||||||
|
)
|
||||||
|
rc_group.add_argument(
|
||||||
|
"--rc-message-burst-count", type=float, default=10,
|
||||||
|
help="number of message a client can send before being throttled"
|
||||||
|
)
|
||||||
39
synapse/config/repository.py
Normal file
39
synapse/config/repository.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 matrix.org
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class ContentRepositoryConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(ContentRepositoryConfig, self).__init__(args)
|
||||||
|
self.max_upload_size = self.parse_size(args.max_upload_size)
|
||||||
|
|
||||||
|
def parse_size(self, string):
|
||||||
|
sizes = {"K": 1024, "M": 1024 * 1024}
|
||||||
|
size = 1
|
||||||
|
suffix = string[-1]
|
||||||
|
if suffix in sizes:
|
||||||
|
string = string[:-1]
|
||||||
|
size = sizes[suffix]
|
||||||
|
return int(string) * size
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(ContentRepositoryConfig, cls).add_arguments(parser)
|
||||||
|
db_group = parser.add_argument_group("content_repository")
|
||||||
|
db_group.add_argument(
|
||||||
|
"--max-upload-size", default="1M"
|
||||||
|
)
|
||||||
118
synapse/config/server.py
Normal file
118
synapse/config/server.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from ._base import Config, ConfigError
|
||||||
|
import syutil.crypto.signing_key
|
||||||
|
|
||||||
|
|
||||||
|
class ServerConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(ServerConfig, self).__init__(args)
|
||||||
|
self.server_name = args.server_name
|
||||||
|
self.signing_key = self.read_signing_key(args.signing_key_path)
|
||||||
|
self.bind_port = args.bind_port
|
||||||
|
self.bind_host = args.bind_host
|
||||||
|
self.unsecure_port = args.unsecure_port
|
||||||
|
self.daemonize = args.daemonize
|
||||||
|
self.pid_file = self.abspath(args.pid_file)
|
||||||
|
self.webclient = True
|
||||||
|
self.manhole = args.manhole
|
||||||
|
self.no_tls = args.no_tls
|
||||||
|
|
||||||
|
if not args.content_addr:
|
||||||
|
host = args.server_name
|
||||||
|
if ':' not in host:
|
||||||
|
host = "%s:%d" % (host, args.unsecure_port)
|
||||||
|
else:
|
||||||
|
host = host.split(':')[0]
|
||||||
|
host = "%s:%d" % (host, args.unsecure_port)
|
||||||
|
args.content_addr = "http://%s" % (host,)
|
||||||
|
|
||||||
|
self.content_addr = args.content_addr
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(ServerConfig, cls).add_arguments(parser)
|
||||||
|
server_group = parser.add_argument_group("server")
|
||||||
|
server_group.add_argument("-H", "--server-name", default="localhost",
|
||||||
|
help="The name of the server")
|
||||||
|
server_group.add_argument("--signing-key-path",
|
||||||
|
help="The signing key to sign messages with")
|
||||||
|
server_group.add_argument("-p", "--bind-port", metavar="PORT",
|
||||||
|
type=int, help="https port to listen on",
|
||||||
|
default=8448)
|
||||||
|
server_group.add_argument("--unsecure-port", metavar="PORT",
|
||||||
|
type=int, help="http port to listen on",
|
||||||
|
default=8008)
|
||||||
|
server_group.add_argument("--bind-host", default="",
|
||||||
|
help="Local interface to listen on")
|
||||||
|
server_group.add_argument("-D", "--daemonize", action='store_true',
|
||||||
|
help="Daemonize the home server")
|
||||||
|
server_group.add_argument('--pid-file', default="homeserver.pid",
|
||||||
|
help="When running as a daemon, the file to"
|
||||||
|
" store the pid in")
|
||||||
|
server_group.add_argument("--manhole", metavar="PORT", dest="manhole",
|
||||||
|
type=int,
|
||||||
|
help="Turn on the twisted telnet manhole"
|
||||||
|
" service on the given port.")
|
||||||
|
server_group.add_argument("--content-addr", default=None,
|
||||||
|
help="The host and scheme to use for the "
|
||||||
|
"content repository")
|
||||||
|
server_group.add_argument("--no-tls", action='store_true',
|
||||||
|
help="Don't bind to the https port.")
|
||||||
|
|
||||||
|
def read_signing_key(self, signing_key_path):
|
||||||
|
signing_keys = self.read_file(signing_key_path, "signing_key")
|
||||||
|
try:
|
||||||
|
return syutil.crypto.signing_key.read_signing_keys(
|
||||||
|
signing_keys.splitlines(True)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
raise ConfigError(
|
||||||
|
"Error reading signing_key."
|
||||||
|
" Try running again with --generate-config"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_config(cls, args, config_dir_path):
|
||||||
|
super(ServerConfig, cls).generate_config(args, config_dir_path)
|
||||||
|
base_key_name = os.path.join(config_dir_path, args.server_name)
|
||||||
|
|
||||||
|
args.pid_file = os.path.abspath(args.pid_file)
|
||||||
|
|
||||||
|
if not args.signing_key_path:
|
||||||
|
args.signing_key_path = base_key_name + ".signing.key"
|
||||||
|
|
||||||
|
if not os.path.exists(args.signing_key_path):
|
||||||
|
with open(args.signing_key_path, "w") as signing_key_file:
|
||||||
|
syutil.crypto.signing_key.write_signing_keys(
|
||||||
|
signing_key_file,
|
||||||
|
(syutil.crypto.signing_key.generate_singing_key("auto"),),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
signing_keys = cls.read_file(args.signing_key_path, "signing_key")
|
||||||
|
if len(signing_keys.split("\n")[0].split()) == 1:
|
||||||
|
# handle keys in the old format.
|
||||||
|
key = syutil.crypto.signing_key.decode_signing_key_base64(
|
||||||
|
syutil.crypto.signing_key.NACL_ED25519,
|
||||||
|
"auto",
|
||||||
|
signing_keys.split("\n")[0]
|
||||||
|
)
|
||||||
|
with open(args.signing_key_path, "w") as signing_key_file:
|
||||||
|
syutil.crypto.signing_key.write_signing_keys(
|
||||||
|
signing_key_file,
|
||||||
|
(key,),
|
||||||
|
)
|
||||||
130
synapse/config/tls.py
Normal file
130
synapse/config/tls.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
from OpenSSL import crypto
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
GENERATE_DH_PARAMS = False
|
||||||
|
|
||||||
|
|
||||||
|
class TlsConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(TlsConfig, self).__init__(args)
|
||||||
|
self.tls_certificate = self.read_tls_certificate(
|
||||||
|
args.tls_certificate_path
|
||||||
|
)
|
||||||
|
self.tls_private_key = self.read_tls_private_key(
|
||||||
|
args.tls_private_key_path
|
||||||
|
)
|
||||||
|
self.tls_dh_params_path = self.check_file(
|
||||||
|
args.tls_dh_params_path, "tls_dh_params"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(TlsConfig, cls).add_arguments(parser)
|
||||||
|
tls_group = parser.add_argument_group("tls")
|
||||||
|
tls_group.add_argument("--tls-certificate-path",
|
||||||
|
help="PEM encoded X509 certificate for TLS")
|
||||||
|
tls_group.add_argument("--tls-private-key-path",
|
||||||
|
help="PEM encoded private key for TLS")
|
||||||
|
tls_group.add_argument("--tls-dh-params-path",
|
||||||
|
help="PEM dh parameters for ephemeral keys")
|
||||||
|
|
||||||
|
def read_tls_certificate(self, cert_path):
|
||||||
|
cert_pem = self.read_file(cert_path, "tls_certificate")
|
||||||
|
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
|
||||||
|
|
||||||
|
def read_tls_private_key(self, private_key_path):
|
||||||
|
private_key_pem = self.read_file(private_key_path, "tls_private_key")
|
||||||
|
return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_config(cls, args, config_dir_path):
|
||||||
|
super(TlsConfig, cls).generate_config(args, config_dir_path)
|
||||||
|
base_key_name = os.path.join(config_dir_path, args.server_name)
|
||||||
|
|
||||||
|
if args.tls_certificate_path is None:
|
||||||
|
args.tls_certificate_path = base_key_name + ".tls.crt"
|
||||||
|
|
||||||
|
if args.tls_private_key_path is None:
|
||||||
|
args.tls_private_key_path = base_key_name + ".tls.key"
|
||||||
|
|
||||||
|
if args.tls_dh_params_path is None:
|
||||||
|
args.tls_dh_params_path = base_key_name + ".tls.dh"
|
||||||
|
|
||||||
|
if not os.path.exists(args.tls_private_key_path):
|
||||||
|
with open(args.tls_private_key_path, "w") as private_key_file:
|
||||||
|
tls_private_key = crypto.PKey()
|
||||||
|
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
|
||||||
|
private_key_pem = crypto.dump_privatekey(
|
||||||
|
crypto.FILETYPE_PEM, tls_private_key
|
||||||
|
)
|
||||||
|
private_key_file.write(private_key_pem)
|
||||||
|
else:
|
||||||
|
with open(args.tls_private_key_path) as private_key_file:
|
||||||
|
private_key_pem = private_key_file.read()
|
||||||
|
tls_private_key = crypto.load_privatekey(
|
||||||
|
crypto.FILETYPE_PEM, private_key_pem
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(args.tls_certificate_path):
|
||||||
|
with open(args.tls_certificate_path, "w") as certifcate_file:
|
||||||
|
cert = crypto.X509()
|
||||||
|
subject = cert.get_subject()
|
||||||
|
subject.CN = args.server_name
|
||||||
|
|
||||||
|
cert.set_serial_number(1000)
|
||||||
|
cert.gmtime_adj_notBefore(0)
|
||||||
|
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
|
||||||
|
cert.set_issuer(cert.get_subject())
|
||||||
|
cert.set_pubkey(tls_private_key)
|
||||||
|
|
||||||
|
cert.sign(tls_private_key, 'sha256')
|
||||||
|
|
||||||
|
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||||
|
|
||||||
|
certifcate_file.write(cert_pem)
|
||||||
|
|
||||||
|
if not os.path.exists(args.tls_dh_params_path):
|
||||||
|
if GENERATE_DH_PARAMS:
|
||||||
|
subprocess.check_call([
|
||||||
|
"openssl", "dhparam",
|
||||||
|
"-outform", "PEM",
|
||||||
|
"-out", args.tls_dh_params_path,
|
||||||
|
"2048"
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
with open(args.tls_dh_params_path, "w") as dh_params_file:
|
||||||
|
dh_params_file.write(
|
||||||
|
"2048-bit DH parameters taken from rfc3526\n"
|
||||||
|
"-----BEGIN DH PARAMETERS-----\n"
|
||||||
|
"MIIBCAKCAQEA///////////JD9qiIWjC"
|
||||||
|
"NMTGYouA3BzRKQJOCIpnzHQCC76mOxOb\n"
|
||||||
|
"IlFKCHmONATd75UZs806QxswKwpt8l8U"
|
||||||
|
"N0/hNW1tUcJF5IW1dmJefsb0TELppjft\n"
|
||||||
|
"awv/XLb0Brft7jhr+1qJn6WunyQRfEsf"
|
||||||
|
"5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT\n"
|
||||||
|
"mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVS"
|
||||||
|
"u57VKQdwlpZtZww1Tkq8mATxdGwIyhgh\n"
|
||||||
|
"fDKQXkYuNs474553LBgOhgObJ4Oi7Aei"
|
||||||
|
"j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq\n"
|
||||||
|
"5RXSJhiY+gUQFXKOWoqsqmj/////////"
|
||||||
|
"/wIBAg==\n"
|
||||||
|
"-----END DH PARAMETERS-----\n"
|
||||||
|
)
|
||||||
44
synapse/config/voip.py
Normal file
44
synapse/config/voip.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class VoipConfig(Config):
|
||||||
|
|
||||||
|
def __init__(self, args):
|
||||||
|
super(VoipConfig, self).__init__(args)
|
||||||
|
self.turn_uris = args.turn_uris
|
||||||
|
self.turn_shared_secret = args.turn_shared_secret
|
||||||
|
self.turn_user_lifetime = args.turn_user_lifetime
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(VoipConfig, cls).add_arguments(parser)
|
||||||
|
group = parser.add_argument_group("voip")
|
||||||
|
group.add_argument(
|
||||||
|
"--turn-uris", type=str, default=None,
|
||||||
|
help="The public URIs of the TURN server to give to clients"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--turn-shared-secret", type=str, default=None,
|
||||||
|
help=(
|
||||||
|
"The shared secret used to compute passwords for the TURN"
|
||||||
|
" server"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--turn-user-lifetime", type=int, default=(1000 * 60 * 60),
|
||||||
|
help="How long generated TURN credentials last, in ms"
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -12,4 +12,3 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 matrix.org
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import ConfigParser as configparser
|
|
||||||
import argparse
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from OpenSSL import crypto
|
|
||||||
import nacl.signing
|
|
||||||
from syutil.base64util import encode_base64
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(description, argv):
|
|
||||||
config_parser = argparse.ArgumentParser(add_help=False)
|
|
||||||
config_parser.add_argument("-c", "--config-path", metavar="CONFIG_FILE",
|
|
||||||
help="Specify config file")
|
|
||||||
config_args, remaining_args = config_parser.parse_known_args(argv)
|
|
||||||
if config_args.config_path:
|
|
||||||
config = configparser.SafeConfigParser()
|
|
||||||
config.read([config_args.config_path])
|
|
||||||
defaults = dict(config.items("KeyServer"))
|
|
||||||
else:
|
|
||||||
defaults = {}
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
parents=[config_parser],
|
|
||||||
description=description,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
parser.set_defaults(**defaults)
|
|
||||||
parser.add_argument("--server-name", default=socket.getfqdn(),
|
|
||||||
help="The name of the server")
|
|
||||||
parser.add_argument("--signing-key-path",
|
|
||||||
help="The signing key to sign responses with")
|
|
||||||
parser.add_argument("--tls-certificate-path",
|
|
||||||
help="PEM encoded X509 certificate for TLS")
|
|
||||||
parser.add_argument("--tls-private-key-path",
|
|
||||||
help="PEM encoded private key for TLS")
|
|
||||||
parser.add_argument("--tls-dh-params-path",
|
|
||||||
help="PEM encoded dh parameters for ephemeral keys")
|
|
||||||
parser.add_argument("--bind-port", type=int,
|
|
||||||
help="TCP port to listen on")
|
|
||||||
parser.add_argument("--bind-host", default="",
|
|
||||||
help="Local interface to listen on")
|
|
||||||
|
|
||||||
args = parser.parse_args(remaining_args)
|
|
||||||
|
|
||||||
server_config = vars(args)
|
|
||||||
del server_config["config_path"]
|
|
||||||
return server_config
|
|
||||||
|
|
||||||
|
|
||||||
def generate_config(argv):
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-c", "--config-path", help="Specify config file",
|
|
||||||
metavar="CONFIG_FILE", required=True)
|
|
||||||
parser.add_argument("--server-name", default=socket.getfqdn(),
|
|
||||||
help="The name of the server")
|
|
||||||
parser.add_argument("--signing-key-path",
|
|
||||||
help="The signing key to sign responses with")
|
|
||||||
parser.add_argument("--tls-certificate-path",
|
|
||||||
help="PEM encoded X509 certificate for TLS")
|
|
||||||
parser.add_argument("--tls-private-key-path",
|
|
||||||
help="PEM encoded private key for TLS")
|
|
||||||
parser.add_argument("--tls-dh-params-path",
|
|
||||||
help="PEM encoded dh parameters for ephemeral keys")
|
|
||||||
parser.add_argument("--bind-port", type=int, required=True,
|
|
||||||
help="TCP port to listen on")
|
|
||||||
parser.add_argument("--bind-host", default="",
|
|
||||||
help="Local interface to listen on")
|
|
||||||
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
|
|
||||||
dir_name = os.path.dirname(args.config_path)
|
|
||||||
base_key_name = os.path.join(dir_name, args.server_name)
|
|
||||||
|
|
||||||
if args.signing_key_path is None:
|
|
||||||
args.signing_key_path = base_key_name + ".signing.key"
|
|
||||||
|
|
||||||
if args.tls_certificate_path is None:
|
|
||||||
args.tls_certificate_path = base_key_name + ".tls.crt"
|
|
||||||
|
|
||||||
if args.tls_private_key_path is None:
|
|
||||||
args.tls_private_key_path = base_key_name + ".tls.key"
|
|
||||||
|
|
||||||
if args.tls_dh_params_path is None:
|
|
||||||
args.tls_dh_params_path = base_key_name + ".tls.dh"
|
|
||||||
|
|
||||||
if not os.path.exists(args.signing_key_path):
|
|
||||||
with open(args.signing_key_path, "w") as signing_key_file:
|
|
||||||
key = nacl.signing.SigningKey.generate()
|
|
||||||
signing_key_file.write(encode_base64(key.encode()))
|
|
||||||
|
|
||||||
if not os.path.exists(args.tls_private_key_path):
|
|
||||||
with open(args.tls_private_key_path, "w") as private_key_file:
|
|
||||||
tls_private_key = crypto.PKey()
|
|
||||||
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
|
|
||||||
private_key_pem = crypto.dump_privatekey(
|
|
||||||
crypto.FILETYPE_PEM, tls_private_key
|
|
||||||
)
|
|
||||||
private_key_file.write(private_key_pem)
|
|
||||||
else:
|
|
||||||
with open(args.tls_private_key_path) as private_key_file:
|
|
||||||
private_key_pem = private_key_file.read()
|
|
||||||
tls_private_key = crypto.load_privatekey(
|
|
||||||
crypto.FILETYPE_PEM, private_key_pem
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.exists(args.tls_certificate_path):
|
|
||||||
with open(args.tls_certificate_path, "w") as certifcate_file:
|
|
||||||
cert = crypto.X509()
|
|
||||||
subject = cert.get_subject()
|
|
||||||
subject.CN = args.server_name
|
|
||||||
|
|
||||||
cert.set_serial_number(1000)
|
|
||||||
cert.gmtime_adj_notBefore(0)
|
|
||||||
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
|
|
||||||
cert.set_issuer(cert.get_subject())
|
|
||||||
cert.set_pubkey(tls_private_key)
|
|
||||||
|
|
||||||
cert.sign(tls_private_key, 'sha256')
|
|
||||||
|
|
||||||
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
|
||||||
|
|
||||||
certifcate_file.write(cert_pem)
|
|
||||||
|
|
||||||
if not os.path.exists(args.tls_dh_params_path):
|
|
||||||
subprocess.check_call([
|
|
||||||
"openssl", "dhparam",
|
|
||||||
"-outform", "PEM",
|
|
||||||
"-out", args.tls_dh_params_path,
|
|
||||||
"2048"
|
|
||||||
])
|
|
||||||
|
|
||||||
config = configparser.SafeConfigParser()
|
|
||||||
config.add_section("KeyServer")
|
|
||||||
for key, value in vars(args).items():
|
|
||||||
if key != "config_path":
|
|
||||||
config.set("KeyServer", key, str(value))
|
|
||||||
|
|
||||||
with open(args.config_path, "w") as config_file:
|
|
||||||
config.write(config_file)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
generate_config(sys.argv[1:])
|
|
||||||
46
synapse/crypto/context_factory.py
Normal file
46
synapse/crypto/context_factory.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import ssl
|
||||||
|
from OpenSSL import SSL
|
||||||
|
from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerContextFactory(ssl.ContextFactory):
|
||||||
|
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
|
||||||
|
connections and to make connections to remote servers."""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self._context = SSL.Context(SSL.SSLv23_METHOD)
|
||||||
|
self.configure_context(self._context, config)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configure_context(context, config):
|
||||||
|
try:
|
||||||
|
_ecCurve = _OpenSSLECCurve(_defaultCurveName)
|
||||||
|
_ecCurve.addECKeyToContext(context)
|
||||||
|
except:
|
||||||
|
logger.exception("Failed to enable eliptic curve for TLS")
|
||||||
|
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||||
|
context.use_certificate(config.tls_certificate)
|
||||||
|
context.use_privatekey(config.tls_private_key)
|
||||||
|
context.load_tmp_dh(config.tls_dh_params_path)
|
||||||
|
context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
|
||||||
|
|
||||||
|
def getContext(self):
|
||||||
|
return self._context
|
||||||
108
synapse/crypto/event_signing.py
Normal file
108
synapse/crypto/event_signing.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from synapse.api.events.utils import prune_event
|
||||||
|
from syutil.jsonutil import encode_canonical_json
|
||||||
|
from syutil.base64util import encode_base64, decode_base64
|
||||||
|
from syutil.crypto.jsonsign import sign_json
|
||||||
|
from synapse.api.errors import SynapseError, Codes
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def check_event_content_hash(event, hash_algorithm=hashlib.sha256):
|
||||||
|
"""Check whether the hash for this PDU matches the contents"""
|
||||||
|
computed_hash = _compute_content_hash(event, hash_algorithm)
|
||||||
|
logger.debug("Expecting hash: %s", encode_base64(computed_hash.digest()))
|
||||||
|
if computed_hash.name not in event.hashes:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Algorithm %s not in hashes %s" % (
|
||||||
|
computed_hash.name, list(event.hashes),
|
||||||
|
),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
message_hash_base64 = event.hashes[computed_hash.name]
|
||||||
|
try:
|
||||||
|
message_hash_bytes = decode_base64(message_hash_base64)
|
||||||
|
except:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Invalid base64: %s" % (message_hash_base64,),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
return message_hash_bytes == computed_hash.digest()
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_content_hash(event, hash_algorithm):
|
||||||
|
event_json = event.get_pdu_json()
|
||||||
|
event_json.pop("age_ts", None)
|
||||||
|
event_json.pop("unsigned", None)
|
||||||
|
event_json.pop("signatures", None)
|
||||||
|
event_json.pop("hashes", None)
|
||||||
|
event_json.pop("outlier", None)
|
||||||
|
event_json.pop("destinations", None)
|
||||||
|
event_json_bytes = encode_canonical_json(event_json)
|
||||||
|
return hash_algorithm(event_json_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_event_reference_hash(event, hash_algorithm=hashlib.sha256):
|
||||||
|
tmp_event = prune_event(event)
|
||||||
|
event_json = tmp_event.get_pdu_json()
|
||||||
|
event_json.pop("signatures", None)
|
||||||
|
event_json.pop("age_ts", None)
|
||||||
|
event_json.pop("unsigned", None)
|
||||||
|
event_json_bytes = encode_canonical_json(event_json)
|
||||||
|
hashed = hash_algorithm(event_json_bytes)
|
||||||
|
return (hashed.name, hashed.digest())
|
||||||
|
|
||||||
|
|
||||||
|
def compute_event_signature(event, signature_name, signing_key):
|
||||||
|
tmp_event = prune_event(event)
|
||||||
|
redact_json = tmp_event.get_pdu_json()
|
||||||
|
redact_json.pop("age_ts", None)
|
||||||
|
redact_json.pop("unsigned", None)
|
||||||
|
logger.debug("Signing event: %s", redact_json)
|
||||||
|
redact_json = sign_json(redact_json, signature_name, signing_key)
|
||||||
|
return redact_json["signatures"]
|
||||||
|
|
||||||
|
|
||||||
|
def add_hashes_and_signatures(event, signature_name, signing_key,
|
||||||
|
hash_algorithm=hashlib.sha256):
|
||||||
|
if hasattr(event, "old_state_events"):
|
||||||
|
state_json_bytes = encode_canonical_json(
|
||||||
|
[e.event_id for e in event.old_state_events.values()]
|
||||||
|
)
|
||||||
|
hashed = hash_algorithm(state_json_bytes)
|
||||||
|
event.state_hash = {
|
||||||
|
hashed.name: encode_base64(hashed.digest())
|
||||||
|
}
|
||||||
|
|
||||||
|
hashed = _compute_content_hash(event, hash_algorithm=hash_algorithm)
|
||||||
|
|
||||||
|
if not hasattr(event, "hashes"):
|
||||||
|
event.hashes = {}
|
||||||
|
event.hashes[hashed.name] = encode_base64(hashed.digest())
|
||||||
|
|
||||||
|
event.signatures = compute_event_signature(
|
||||||
|
event,
|
||||||
|
signature_name=signature_name,
|
||||||
|
signing_key=signing_key,
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -15,9 +15,10 @@
|
|||||||
|
|
||||||
|
|
||||||
from twisted.web.http import HTTPClient
|
from twisted.web.http import HTTPClient
|
||||||
|
from twisted.internet.protocol import Factory
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
from twisted.internet.protocol import ClientFactory
|
from synapse.http.endpoint import matrix_federation_endpoint
|
||||||
from twisted.names.srvconnect import SRVConnector
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -30,19 +31,24 @@ def fetch_server_key(server_name, ssl_context_factory):
|
|||||||
"""Fetch the keys for a remote server."""
|
"""Fetch the keys for a remote server."""
|
||||||
|
|
||||||
factory = SynapseKeyClientFactory()
|
factory = SynapseKeyClientFactory()
|
||||||
|
endpoint = matrix_federation_endpoint(
|
||||||
|
reactor, server_name, ssl_context_factory, timeout=30
|
||||||
|
)
|
||||||
|
|
||||||
SRVConnector(
|
for i in range(5):
|
||||||
reactor, "matrix", server_name, factory,
|
try:
|
||||||
protocol="tcp", connectFuncName="connectSSL", defaultPort=443,
|
with PreserveLoggingContext():
|
||||||
connectFuncKwArgs=dict(contextFactory=ssl_context_factory)).connect()
|
protocol = yield endpoint.connect(factory)
|
||||||
|
server_response, server_certificate = yield protocol.remote_key
|
||||||
server_key, server_certificate = yield factory.remote_key
|
defer.returnValue((server_response, server_certificate))
|
||||||
|
return
|
||||||
defer.returnValue((server_key, server_certificate))
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
raise IOError("Cannot get key for %s" % server_name)
|
||||||
|
|
||||||
|
|
||||||
class SynapseKeyClientError(Exception):
|
class SynapseKeyClientError(Exception):
|
||||||
"""The key wasn't retireved from the remote server."""
|
"""The key wasn't retrieved from the remote server."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -51,69 +57,46 @@ class SynapseKeyClientProtocol(HTTPClient):
|
|||||||
the server and extracts the X.509 certificate for the remote peer from the
|
the server and extracts the X.509 certificate for the remote peer from the
|
||||||
SSL connection."""
|
SSL connection."""
|
||||||
|
|
||||||
|
timeout = 30
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.remote_key = defer.Deferred()
|
||||||
|
|
||||||
def connectionMade(self):
|
def connectionMade(self):
|
||||||
logger.debug("Connected to %s", self.transport.getHost())
|
logger.debug("Connected to %s", self.transport.getHost())
|
||||||
self.sendCommand(b"GET", b"/key")
|
self.sendCommand(b"GET", b"/_matrix/key/v1/")
|
||||||
self.endHeaders()
|
self.endHeaders()
|
||||||
self.timer = reactor.callLater(
|
self.timer = reactor.callLater(
|
||||||
self.factory.timeout_seconds,
|
self.timeout,
|
||||||
self.on_timeout
|
self.on_timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
def handleStatus(self, version, status, message):
|
def handleStatus(self, version, status, message):
|
||||||
if status != b"200":
|
if status != b"200":
|
||||||
logger.info("Non-200 response from %s: %s %s",
|
#logger.info("Non-200 response from %s: %s %s",
|
||||||
self.transport.getHost(), status, message)
|
# self.transport.getHost(), status, message)
|
||||||
self.transport.abortConnection()
|
self.transport.abortConnection()
|
||||||
|
|
||||||
def handleResponse(self, response_body_bytes):
|
def handleResponse(self, response_body_bytes):
|
||||||
try:
|
try:
|
||||||
json_response = json.loads(response_body_bytes)
|
json_response = json.loads(response_body_bytes)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.info("Invalid JSON response from %s",
|
#logger.info("Invalid JSON response from %s",
|
||||||
self.transport.getHost())
|
# self.transport.getHost())
|
||||||
self.transport.abortConnection()
|
self.transport.abortConnection()
|
||||||
return
|
return
|
||||||
|
|
||||||
certificate = self.transport.getPeerCertificate()
|
certificate = self.transport.getPeerCertificate()
|
||||||
self.factory.on_remote_key((json_response, certificate))
|
self.remote_key.callback((json_response, certificate))
|
||||||
self.transport.abortConnection()
|
self.transport.abortConnection()
|
||||||
self.timer.cancel()
|
self.timer.cancel()
|
||||||
|
|
||||||
def on_timeout(self):
|
def on_timeout(self):
|
||||||
logger.debug("Timeout waiting for response from %s",
|
logger.debug("Timeout waiting for response from %s",
|
||||||
self.transport.getHost())
|
self.transport.getHost())
|
||||||
|
self.remote_key.errback(IOError("Timeout waiting for response"))
|
||||||
self.transport.abortConnection()
|
self.transport.abortConnection()
|
||||||
|
|
||||||
|
|
||||||
class SynapseKeyClientFactory(ClientFactory):
|
class SynapseKeyClientFactory(Factory):
|
||||||
protocol = SynapseKeyClientProtocol
|
protocol = SynapseKeyClientProtocol
|
||||||
max_retries = 5
|
|
||||||
timeout_seconds = 30
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.succeeded = False
|
|
||||||
self.retries = 0
|
|
||||||
self.remote_key = defer.Deferred()
|
|
||||||
|
|
||||||
def on_remote_key(self, key):
|
|
||||||
self.succeeded = True
|
|
||||||
self.remote_key.callback(key)
|
|
||||||
|
|
||||||
def retry_connection(self, connector):
|
|
||||||
self.retries += 1
|
|
||||||
if self.retries < self.max_retries:
|
|
||||||
connector.connector = None
|
|
||||||
connector.connect()
|
|
||||||
else:
|
|
||||||
self.remote_key.errback(
|
|
||||||
SynapseKeyClientError("Max retries exceeded"))
|
|
||||||
|
|
||||||
def clientConnectionFailed(self, connector, reason):
|
|
||||||
logger.info("Connection failed %s", reason)
|
|
||||||
self.retry_connection(connector)
|
|
||||||
|
|
||||||
def clientConnectionLost(self, connector, reason):
|
|
||||||
logger.info("Connection lost %s", reason)
|
|
||||||
if not self.succeeded:
|
|
||||||
self.retry_connection(connector)
|
|
||||||
|
|||||||
155
synapse/crypto/keyring.py
Normal file
155
synapse/crypto/keyring.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from synapse.crypto.keyclient import fetch_server_key
|
||||||
|
from twisted.internet import defer
|
||||||
|
from syutil.crypto.jsonsign import verify_signed_json, signature_ids
|
||||||
|
from syutil.crypto.signing_key import (
|
||||||
|
is_signing_algorithm_supported, decode_verify_key_bytes
|
||||||
|
)
|
||||||
|
from syutil.base64util import decode_base64, encode_base64
|
||||||
|
from synapse.api.errors import SynapseError, Codes
|
||||||
|
|
||||||
|
from OpenSSL import crypto
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Keyring(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
self.hs = hs
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def verify_json_for_server(self, server_name, json_object):
|
||||||
|
logger.debug("Verifying for %s", server_name)
|
||||||
|
key_ids = signature_ids(json_object, server_name)
|
||||||
|
if not key_ids:
|
||||||
|
raise SynapseError(
|
||||||
|
400,
|
||||||
|
"Not signed with a supported algorithm",
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
verify_key = yield self.get_server_verify_key(server_name, key_ids)
|
||||||
|
except IOError:
|
||||||
|
raise SynapseError(
|
||||||
|
502,
|
||||||
|
"Error downloading keys for %s" % (server_name,),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
raise SynapseError(
|
||||||
|
401,
|
||||||
|
"No key for %s with id %s" % (server_name, key_ids),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
verify_signed_json(json_object, server_name, verify_key)
|
||||||
|
except:
|
||||||
|
raise SynapseError(
|
||||||
|
401,
|
||||||
|
"Invalid signature for server %s with key %s:%s" % (
|
||||||
|
server_name, verify_key.alg, verify_key.version
|
||||||
|
),
|
||||||
|
Codes.UNAUTHORIZED,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_server_verify_key(self, server_name, key_ids):
|
||||||
|
"""Finds a verification key for the server with one of the key ids.
|
||||||
|
Args:
|
||||||
|
server_name (str): The name of the server to fetch a key for.
|
||||||
|
keys_ids (list of str): The key_ids to check for.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check the datastore to see if we have one cached.
|
||||||
|
cached = yield self.store.get_server_verify_keys(server_name, key_ids)
|
||||||
|
|
||||||
|
if cached:
|
||||||
|
defer.returnValue(cached[0])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try to fetch the key from the remote server.
|
||||||
|
# TODO(markjh): Ratelimit requests to a given server.
|
||||||
|
|
||||||
|
(response, tls_certificate) = yield fetch_server_key(
|
||||||
|
server_name, self.hs.tls_context_factory
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the response.
|
||||||
|
|
||||||
|
x509_certificate_bytes = crypto.dump_certificate(
|
||||||
|
crypto.FILETYPE_ASN1, tls_certificate
|
||||||
|
)
|
||||||
|
|
||||||
|
if ("signatures" not in response
|
||||||
|
or server_name not in response["signatures"]):
|
||||||
|
raise ValueError("Key response not signed by remote server")
|
||||||
|
|
||||||
|
if "tls_certificate" not in response:
|
||||||
|
raise ValueError("Key response missing TLS certificate")
|
||||||
|
|
||||||
|
tls_certificate_b64 = response["tls_certificate"]
|
||||||
|
|
||||||
|
if encode_base64(x509_certificate_bytes) != tls_certificate_b64:
|
||||||
|
raise ValueError("TLS certificate doesn't match")
|
||||||
|
|
||||||
|
verify_keys = {}
|
||||||
|
for key_id, key_base64 in response["verify_keys"].items():
|
||||||
|
if is_signing_algorithm_supported(key_id):
|
||||||
|
key_bytes = decode_base64(key_base64)
|
||||||
|
verify_key = decode_verify_key_bytes(key_id, key_bytes)
|
||||||
|
verify_keys[key_id] = verify_key
|
||||||
|
|
||||||
|
for key_id in response["signatures"][server_name]:
|
||||||
|
if key_id not in response["verify_keys"]:
|
||||||
|
raise ValueError(
|
||||||
|
"Key response must include verification keys for all"
|
||||||
|
" signatures"
|
||||||
|
)
|
||||||
|
if key_id in verify_keys:
|
||||||
|
verify_signed_json(
|
||||||
|
response,
|
||||||
|
server_name,
|
||||||
|
verify_keys[key_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cache the result in the datastore.
|
||||||
|
|
||||||
|
time_now_ms = self.clock.time_msec()
|
||||||
|
|
||||||
|
yield self.store.store_server_certificate(
|
||||||
|
server_name,
|
||||||
|
server_name,
|
||||||
|
time_now_ms,
|
||||||
|
tls_certificate,
|
||||||
|
)
|
||||||
|
|
||||||
|
for key_id, key in verify_keys.items():
|
||||||
|
yield self.store.store_server_verify_key(
|
||||||
|
server_name, server_name, time_now_ms, key
|
||||||
|
)
|
||||||
|
|
||||||
|
for key_id in key_ids:
|
||||||
|
if key_id in verify_keys:
|
||||||
|
defer.returnValue(verify_keys[key_id])
|
||||||
|
return
|
||||||
|
|
||||||
|
raise ValueError("No verification key found for given key ids")
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 matrix.org
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
from twisted.internet import reactor, ssl
|
|
||||||
from twisted.web import server
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
from twisted.python.log import PythonLoggingObserver
|
|
||||||
|
|
||||||
from synapse.crypto.resource.key import LocalKey
|
|
||||||
from synapse.crypto.config import load_config
|
|
||||||
|
|
||||||
from syutil.base64util import decode_base64
|
|
||||||
|
|
||||||
from OpenSSL import crypto, SSL
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import nacl.signing
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class KeyServerSSLContextFactory(ssl.ContextFactory):
|
|
||||||
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
|
|
||||||
connections and to make connections to remote servers."""
|
|
||||||
|
|
||||||
def __init__(self, key_server):
|
|
||||||
self._context = SSL.Context(SSL.SSLv23_METHOD)
|
|
||||||
self.configure_context(self._context, key_server)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def configure_context(context, key_server):
|
|
||||||
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
|
||||||
context.use_certificate(key_server.tls_certificate)
|
|
||||||
context.use_privatekey(key_server.tls_private_key)
|
|
||||||
context.load_tmp_dh(key_server.tls_dh_params_path)
|
|
||||||
context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
|
|
||||||
|
|
||||||
def getContext(self):
|
|
||||||
return self._context
|
|
||||||
|
|
||||||
|
|
||||||
class KeyServer(object):
|
|
||||||
"""An HTTPS server serving LocalKey and RemoteKey resources."""
|
|
||||||
|
|
||||||
def __init__(self, server_name, tls_certificate_path, tls_private_key_path,
|
|
||||||
tls_dh_params_path, signing_key_path, bind_host, bind_port):
|
|
||||||
self.server_name = server_name
|
|
||||||
self.tls_certificate = self.read_tls_certificate(tls_certificate_path)
|
|
||||||
self.tls_private_key = self.read_tls_private_key(tls_private_key_path)
|
|
||||||
self.tls_dh_params_path = tls_dh_params_path
|
|
||||||
self.signing_key = self.read_signing_key(signing_key_path)
|
|
||||||
self.bind_host = bind_host
|
|
||||||
self.bind_port = int(bind_port)
|
|
||||||
self.ssl_context_factory = KeyServerSSLContextFactory(self)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def read_tls_certificate(cert_path):
|
|
||||||
with open(cert_path) as cert_file:
|
|
||||||
cert_pem = cert_file.read()
|
|
||||||
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def read_tls_private_key(private_key_path):
|
|
||||||
with open(private_key_path) as private_key_file:
|
|
||||||
private_key_pem = private_key_file.read()
|
|
||||||
return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def read_signing_key(signing_key_path):
|
|
||||||
with open(signing_key_path) as signing_key_file:
|
|
||||||
signing_key_b64 = signing_key_file.read()
|
|
||||||
signing_key_bytes = decode_base64(signing_key_b64)
|
|
||||||
return nacl.signing.SigningKey(signing_key_bytes)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
root = Resource()
|
|
||||||
root.putChild("key", LocalKey(self))
|
|
||||||
site = server.Site(root)
|
|
||||||
reactor.listenSSL(
|
|
||||||
self.bind_port,
|
|
||||||
site,
|
|
||||||
self.ssl_context_factory,
|
|
||||||
interface=self.bind_host
|
|
||||||
)
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
|
||||||
observer = PythonLoggingObserver()
|
|
||||||
observer.start()
|
|
||||||
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
key_server = KeyServer(**load_config(__doc__, sys.argv[1:]))
|
|
||||||
key_server.run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 matrix.org
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
|
|
||||||
from twisted.web.resource import Resource
|
|
||||||
from twisted.web.server import NOT_DONE_YET
|
|
||||||
from twisted.internet import defer
|
|
||||||
from synapse.http.server import respond_with_json_bytes
|
|
||||||
from synapse.crypto.keyclient import fetch_server_key
|
|
||||||
from syutil.crypto.jsonsign import sign_json, verify_signed_json
|
|
||||||
from syutil.base64util import encode_base64, decode_base64
|
|
||||||
from syutil.jsonutil import encode_canonical_json
|
|
||||||
from OpenSSL import crypto
|
|
||||||
from nacl.signing import VerifyKey
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class LocalKey(Resource):
|
|
||||||
"""HTTP resource containing encoding the TLS X.509 certificate and NACL
|
|
||||||
signature verification keys for this server::
|
|
||||||
|
|
||||||
GET /key HTTP/1.1
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"server_name": "this.server.example.com"
|
|
||||||
"signature_verify_key": # base64 encoded NACL verification key.
|
|
||||||
"tls_certificate": # base64 ASN.1 DER encoded X.509 tls cert.
|
|
||||||
"signatures": {
|
|
||||||
"this.server.example.com": # NACL signature for this server.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, key_server):
|
|
||||||
self.key_server = key_server
|
|
||||||
self.response_body = encode_canonical_json(
|
|
||||||
self.response_json_object(key_server)
|
|
||||||
)
|
|
||||||
Resource.__init__(self)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def response_json_object(key_server):
|
|
||||||
verify_key_bytes = key_server.signing_key.verify_key.encode()
|
|
||||||
x509_certificate_bytes = crypto.dump_certificate(
|
|
||||||
crypto.FILETYPE_ASN1,
|
|
||||||
key_server.tls_certificate
|
|
||||||
)
|
|
||||||
json_object = {
|
|
||||||
u"server_name": key_server.server_name,
|
|
||||||
u"signature_verify_key": encode_base64(verify_key_bytes),
|
|
||||||
u"tls_certificate": encode_base64(x509_certificate_bytes)
|
|
||||||
}
|
|
||||||
signed_json = sign_json(
|
|
||||||
json_object,
|
|
||||||
key_server.server_name,
|
|
||||||
key_server.signing_key
|
|
||||||
)
|
|
||||||
return signed_json
|
|
||||||
|
|
||||||
def getChild(self, name, request):
|
|
||||||
logger.info("getChild %s %s", name, request)
|
|
||||||
if name == '':
|
|
||||||
return self
|
|
||||||
else:
|
|
||||||
return RemoteKey(name, self.key_server)
|
|
||||||
|
|
||||||
def render_GET(self, request):
|
|
||||||
return respond_with_json_bytes(request, 200, self.response_body)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoteKey(Resource):
|
|
||||||
"""HTTP resource for retreiving the TLS certificate and NACL signature
|
|
||||||
verification keys for a another server. Checks that the reported X.509 TLS
|
|
||||||
certificate matches the one used in the HTTPS connection. Checks that the
|
|
||||||
NACL signature for the remote server is valid. Returns JSON signed by both
|
|
||||||
the remote server and by this server.
|
|
||||||
|
|
||||||
GET /key/remote.server.example.com HTTP/1.1
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: application/json
|
|
||||||
{
|
|
||||||
"server_name": "remote.server.example.com"
|
|
||||||
"signature_verify_key": # base64 encoded NACL verification key.
|
|
||||||
"tls_certificate": # base64 ASN.1 DER encoded X.509 tls cert.
|
|
||||||
"signatures": {
|
|
||||||
"remote.server.example.com": # NACL signature for remote server.
|
|
||||||
"this.server.example.com": # NACL signature for this server.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
isLeaf = True
|
|
||||||
|
|
||||||
def __init__(self, server_name, key_server):
|
|
||||||
self.server_name = server_name
|
|
||||||
self.key_server = key_server
|
|
||||||
Resource.__init__(self)
|
|
||||||
|
|
||||||
def render_GET(self, request):
|
|
||||||
self._async_render_GET(request)
|
|
||||||
return NOT_DONE_YET
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _async_render_GET(self, request):
|
|
||||||
try:
|
|
||||||
server_keys, certificate = yield fetch_server_key(
|
|
||||||
self.server_name,
|
|
||||||
self.key_server.ssl_context_factory
|
|
||||||
)
|
|
||||||
|
|
||||||
resp_server_name = server_keys[u"server_name"]
|
|
||||||
verify_key_b64 = server_keys[u"signature_verify_key"]
|
|
||||||
tls_certificate_b64 = server_keys[u"tls_certificate"]
|
|
||||||
verify_key = VerifyKey(decode_base64(verify_key_b64))
|
|
||||||
|
|
||||||
if resp_server_name != self.server_name:
|
|
||||||
raise ValueError("Wrong server name '%s' != '%s'" %
|
|
||||||
(resp_server_name, self.server_name))
|
|
||||||
|
|
||||||
x509_certificate_bytes = crypto.dump_certificate(
|
|
||||||
crypto.FILETYPE_ASN1,
|
|
||||||
certificate
|
|
||||||
)
|
|
||||||
|
|
||||||
if encode_base64(x509_certificate_bytes) != tls_certificate_b64:
|
|
||||||
raise ValueError("TLS certificate doesn't match")
|
|
||||||
|
|
||||||
verify_signed_json(server_keys, self.server_name, verify_key)
|
|
||||||
|
|
||||||
signed_json = sign_json(
|
|
||||||
server_keys,
|
|
||||||
self.key_server.server_name,
|
|
||||||
self.key_server.signing_key
|
|
||||||
)
|
|
||||||
|
|
||||||
json_bytes = encode_canonical_json(signed_json)
|
|
||||||
respond_with_json_bytes(request, 200, json_bytes)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
json_bytes = encode_canonical_json({
|
|
||||||
u"error": {u"code": 502, u"message": e.message}
|
|
||||||
})
|
|
||||||
respond_with_json_bytes(request, 502, json_bytes)
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -22,6 +22,7 @@ from .transport import TransportLayer
|
|||||||
|
|
||||||
def initialize_http_replication(homeserver):
|
def initialize_http_replication(homeserver):
|
||||||
transport = TransportLayer(
|
transport = TransportLayer(
|
||||||
|
homeserver,
|
||||||
homeserver.hostname,
|
homeserver.hostname,
|
||||||
server=homeserver.get_resource_for_federation(),
|
server=homeserver.get_resource_for_federation(),
|
||||||
client=homeserver.get_http_client()
|
client=homeserver.get_http_client()
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 matrix.org
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from .units import Pdu
|
|
||||||
|
|
||||||
import copy
|
|
||||||
|
|
||||||
|
|
||||||
def decode_event_id(event_id, server_name):
|
|
||||||
parts = event_id.split("@")
|
|
||||||
if len(parts) < 2:
|
|
||||||
return (event_id, server_name)
|
|
||||||
else:
|
|
||||||
return (parts[0], "".join(parts[1:]))
|
|
||||||
|
|
||||||
|
|
||||||
def encode_event_id(pdu_id, origin):
|
|
||||||
return "%s@%s" % (pdu_id, origin)
|
|
||||||
|
|
||||||
|
|
||||||
class PduCodec(object):
|
|
||||||
|
|
||||||
def __init__(self, hs):
|
|
||||||
self.server_name = hs.hostname
|
|
||||||
self.event_factory = hs.get_event_factory()
|
|
||||||
self.clock = hs.get_clock()
|
|
||||||
|
|
||||||
def event_from_pdu(self, pdu):
|
|
||||||
kwargs = {}
|
|
||||||
|
|
||||||
kwargs["event_id"] = encode_event_id(pdu.pdu_id, pdu.origin)
|
|
||||||
kwargs["room_id"] = pdu.context
|
|
||||||
kwargs["etype"] = pdu.pdu_type
|
|
||||||
kwargs["prev_events"] = [
|
|
||||||
encode_event_id(p[0], p[1]) for p in pdu.prev_pdus
|
|
||||||
]
|
|
||||||
|
|
||||||
if hasattr(pdu, "prev_state_id") and hasattr(pdu, "prev_state_origin"):
|
|
||||||
kwargs["prev_state"] = encode_event_id(
|
|
||||||
pdu.prev_state_id, pdu.prev_state_origin
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs.update({
|
|
||||||
k: v
|
|
||||||
for k, v in pdu.get_full_dict().items()
|
|
||||||
if k not in [
|
|
||||||
"pdu_id",
|
|
||||||
"context",
|
|
||||||
"pdu_type",
|
|
||||||
"prev_pdus",
|
|
||||||
"prev_state_id",
|
|
||||||
"prev_state_origin",
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
return self.event_factory.create_event(**kwargs)
|
|
||||||
|
|
||||||
def pdu_from_event(self, event):
|
|
||||||
d = event.get_full_dict()
|
|
||||||
|
|
||||||
d["pdu_id"], d["origin"] = decode_event_id(
|
|
||||||
event.event_id, self.server_name
|
|
||||||
)
|
|
||||||
d["context"] = event.room_id
|
|
||||||
d["pdu_type"] = event.type
|
|
||||||
|
|
||||||
if hasattr(event, "prev_events"):
|
|
||||||
d["prev_pdus"] = [
|
|
||||||
decode_event_id(e, self.server_name)
|
|
||||||
for e in event.prev_events
|
|
||||||
]
|
|
||||||
|
|
||||||
if hasattr(event, "prev_state"):
|
|
||||||
d["prev_state_id"], d["prev_state_origin"] = (
|
|
||||||
decode_event_id(event.prev_state, self.server_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
if hasattr(event, "state_key"):
|
|
||||||
d["is_state"] = True
|
|
||||||
|
|
||||||
kwargs = copy.deepcopy(event.unrecognized_keys)
|
|
||||||
kwargs.update({
|
|
||||||
k: v for k, v in d.items()
|
|
||||||
if k not in ["event_id", "room_id", "type", "prev_events"]
|
|
||||||
})
|
|
||||||
|
|
||||||
if "ts" not in kwargs:
|
|
||||||
kwargs["ts"] = int(self.clock.time_msec())
|
|
||||||
|
|
||||||
return Pdu(**kwargs)
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,8 +21,6 @@ These actions are mostly only used by the :py:mod:`.replication` module.
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from .units import Pdu
|
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -32,76 +30,6 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PduActions(object):
|
|
||||||
""" Defines persistence actions that relate to handling PDUs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, datastore):
|
|
||||||
self.store = datastore
|
|
||||||
|
|
||||||
@log_function
|
|
||||||
def mark_as_processed(self, pdu):
|
|
||||||
""" Persist the fact that we have fully processed the given `Pdu`
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred
|
|
||||||
"""
|
|
||||||
return self.store.mark_pdu_as_processed(pdu.pdu_id, pdu.origin)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
@log_function
|
|
||||||
def after_transaction(self, transaction_id, destination, origin):
|
|
||||||
""" Returns all `Pdu`s that we sent to the given remote home server
|
|
||||||
after a given transaction id.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred: Results in a list of `Pdu`s
|
|
||||||
"""
|
|
||||||
results = yield self.store.get_pdus_after_transaction(
|
|
||||||
transaction_id,
|
|
||||||
destination
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue([Pdu.from_pdu_tuple(p) for p in results])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
@log_function
|
|
||||||
def get_all_pdus_from_context(self, context):
|
|
||||||
results = yield self.store.get_all_pdus_from_context(context)
|
|
||||||
defer.returnValue([Pdu.from_pdu_tuple(p) for p in results])
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
@log_function
|
|
||||||
def backfill(self, context, pdu_list, limit):
|
|
||||||
""" For a given list of PDU id and origins return the proceeding
|
|
||||||
`limit` `Pdu`s in the given `context`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred: Results in a list of `Pdu`s.
|
|
||||||
"""
|
|
||||||
results = yield self.store.get_backfill(
|
|
||||||
context, pdu_list, limit
|
|
||||||
)
|
|
||||||
|
|
||||||
defer.returnValue([Pdu.from_pdu_tuple(p) for p in results])
|
|
||||||
|
|
||||||
@log_function
|
|
||||||
def is_new(self, pdu):
|
|
||||||
""" When we receive a `Pdu` from a remote home server, we want to
|
|
||||||
figure out whether it is `new`, i.e. it is not some historic PDU that
|
|
||||||
we haven't seen simply because we haven't backfilled back that far.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deferred: Results in a `bool`
|
|
||||||
"""
|
|
||||||
return self.store.is_pdu_new(
|
|
||||||
pdu_id=pdu.pdu_id,
|
|
||||||
origin=pdu.origin,
|
|
||||||
context=pdu.context,
|
|
||||||
depth=pdu.depth
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionActions(object):
|
class TransactionActions(object):
|
||||||
""" Defines persistence actions that relate to handling Transactions.
|
""" Defines persistence actions that relate to handling Transactions.
|
||||||
"""
|
"""
|
||||||
@@ -157,8 +85,7 @@ class TransactionActions(object):
|
|||||||
transaction.prev_ids = yield self.store.prep_send_transaction(
|
transaction.prev_ids = yield self.store.prep_send_transaction(
|
||||||
transaction.transaction_id,
|
transaction.transaction_id,
|
||||||
transaction.destination,
|
transaction.destination,
|
||||||
transaction.ts,
|
transaction.origin_server_ts,
|
||||||
[(p["pdu_id"], p["origin"]) for p in transaction.pdus]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -19,11 +19,12 @@ a given transport.
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from .units import Transaction, Pdu, Edu
|
from .units import Transaction, Edu
|
||||||
|
|
||||||
from .persistence import PduActions, TransactionActions
|
from .persistence import TransactionActions
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
from synapse.util.logcontext import PreserveLoggingContext
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ class ReplicationLayer(object):
|
|||||||
self.transport_layer.register_request_handler(self)
|
self.transport_layer.register_request_handler(self)
|
||||||
|
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.pdu_actions = PduActions(self.store)
|
# self.pdu_actions = PduActions(self.store)
|
||||||
self.transaction_actions = TransactionActions(self.store)
|
self.transaction_actions = TransactionActions(self.store)
|
||||||
|
|
||||||
self._transaction_queue = _TransactionQueue(
|
self._transaction_queue = _TransactionQueue(
|
||||||
@@ -72,6 +73,8 @@ class ReplicationLayer(object):
|
|||||||
|
|
||||||
self._clock = hs.get_clock()
|
self._clock = hs.get_clock()
|
||||||
|
|
||||||
|
self.event_factory = hs.get_event_factory()
|
||||||
|
|
||||||
def set_handler(self, handler):
|
def set_handler(self, handler):
|
||||||
"""Sets the handler that the replication layer will use to communicate
|
"""Sets the handler that the replication layer will use to communicate
|
||||||
receipt of new PDUs from other home servers. The required methods are
|
receipt of new PDUs from other home servers. The required methods are
|
||||||
@@ -81,7 +84,7 @@ class ReplicationLayer(object):
|
|||||||
|
|
||||||
def register_edu_handler(self, edu_type, handler):
|
def register_edu_handler(self, edu_type, handler):
|
||||||
if edu_type in self.edu_handlers:
|
if edu_type in self.edu_handlers:
|
||||||
raise KeyError("Already have an EDU handler for %s" % (edu_type))
|
raise KeyError("Already have an EDU handler for %s" % (edu_type,))
|
||||||
|
|
||||||
self.edu_handlers[edu_type] = handler
|
self.edu_handlers[edu_type] = handler
|
||||||
|
|
||||||
@@ -102,24 +105,17 @@ class ReplicationLayer(object):
|
|||||||
object to encode as JSON.
|
object to encode as JSON.
|
||||||
"""
|
"""
|
||||||
if query_type in self.query_handlers:
|
if query_type in self.query_handlers:
|
||||||
raise KeyError("Already have a Query handler for %s" % (query_type))
|
raise KeyError(
|
||||||
|
"Already have a Query handler for %s" % (query_type,)
|
||||||
|
)
|
||||||
|
|
||||||
self.query_handlers[query_type] = handler
|
self.query_handlers[query_type] = handler
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
@log_function
|
@log_function
|
||||||
def send_pdu(self, pdu):
|
def send_pdu(self, pdu):
|
||||||
"""Informs the replication layer about a new PDU generated within the
|
"""Informs the replication layer about a new PDU generated within the
|
||||||
home server that should be transmitted to others.
|
home server that should be transmitted to others.
|
||||||
|
|
||||||
This will fill out various attributes on the PDU object, e.g. the
|
|
||||||
`prev_pdus` key.
|
|
||||||
|
|
||||||
*Note:* The home server should always call `send_pdu` even if it knows
|
|
||||||
that it does not need to be replicated to other home servers. This is
|
|
||||||
in case e.g. someone else joins via a remote home server and then
|
|
||||||
backfills.
|
|
||||||
|
|
||||||
TODO: Figure out when we should actually resolve the deferred.
|
TODO: Figure out when we should actually resolve the deferred.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -132,18 +128,15 @@ class ReplicationLayer(object):
|
|||||||
order = self._order
|
order = self._order
|
||||||
self._order += 1
|
self._order += 1
|
||||||
|
|
||||||
logger.debug("[%s] Persisting PDU", pdu.pdu_id)
|
logger.debug("[%s] transaction_layer.enqueue_pdu... ", pdu.event_id)
|
||||||
|
|
||||||
# Save *before* trying to send
|
|
||||||
yield self.store.persist_event(pdu=pdu)
|
|
||||||
|
|
||||||
logger.debug("[%s] Persisted PDU", pdu.pdu_id)
|
|
||||||
logger.debug("[%s] transaction_layer.enqueue_pdu... ", pdu.pdu_id)
|
|
||||||
|
|
||||||
# TODO, add errback, etc.
|
# TODO, add errback, etc.
|
||||||
self._transaction_queue.enqueue_pdu(pdu, order)
|
self._transaction_queue.enqueue_pdu(pdu, order)
|
||||||
|
|
||||||
logger.debug("[%s] transaction_layer.enqueue_pdu... done", pdu.pdu_id)
|
logger.debug(
|
||||||
|
"[%s] transaction_layer.enqueue_pdu... done",
|
||||||
|
pdu.event_id
|
||||||
|
)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def send_edu(self, destination, edu_type, content):
|
def send_edu(self, destination, edu_type, content):
|
||||||
@@ -159,7 +152,13 @@ class ReplicationLayer(object):
|
|||||||
return defer.succeed(None)
|
return defer.succeed(None)
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def make_query(self, destination, query_type, args):
|
def send_failure(self, failure, destination):
|
||||||
|
self._transaction_queue.enqueue_failure(failure, destination)
|
||||||
|
return defer.succeed(None)
|
||||||
|
|
||||||
|
@log_function
|
||||||
|
def make_query(self, destination, query_type, args,
|
||||||
|
retry_on_dns_fail=True):
|
||||||
"""Sends a federation Query to a remote homeserver of the given type
|
"""Sends a federation Query to a remote homeserver of the given type
|
||||||
and arguments.
|
and arguments.
|
||||||
|
|
||||||
@@ -174,11 +173,13 @@ class ReplicationLayer(object):
|
|||||||
a Deferred which will eventually yield a JSON object from the
|
a Deferred which will eventually yield a JSON object from the
|
||||||
response
|
response
|
||||||
"""
|
"""
|
||||||
return self.transport_layer.make_query(destination, query_type, args)
|
return self.transport_layer.make_query(
|
||||||
|
destination, query_type, args, retry_on_dns_fail=retry_on_dns_fail
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def backfill(self, dest, context, limit):
|
def backfill(self, dest, context, limit, extremities):
|
||||||
"""Requests some more historic PDUs for the given context from the
|
"""Requests some more historic PDUs for the given context from the
|
||||||
given destination server.
|
given destination server.
|
||||||
|
|
||||||
@@ -186,12 +187,12 @@ class ReplicationLayer(object):
|
|||||||
dest (str): The remote home server to ask.
|
dest (str): The remote home server to ask.
|
||||||
context (str): The context to backfill.
|
context (str): The context to backfill.
|
||||||
limit (int): The maximum number of PDUs to return.
|
limit (int): The maximum number of PDUs to return.
|
||||||
|
extremities (list): List of PDU id and origins of the first pdus
|
||||||
|
we have seen from the context
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred: Results in the received PDUs.
|
Deferred: Results in the received PDUs.
|
||||||
"""
|
"""
|
||||||
extremities = yield self.store.get_oldest_pdus_in_context(context)
|
|
||||||
|
|
||||||
logger.debug("backfill extrem=%s", extremities)
|
logger.debug("backfill extrem=%s", extremities)
|
||||||
|
|
||||||
# If there are no extremeties then we've (probably) reached the start.
|
# If there are no extremeties then we've (probably) reached the start.
|
||||||
@@ -205,15 +206,18 @@ class ReplicationLayer(object):
|
|||||||
|
|
||||||
transaction = Transaction(**transaction_data)
|
transaction = Transaction(**transaction_data)
|
||||||
|
|
||||||
pdus = [Pdu(outlier=False, **p) for p in transaction.pdus]
|
pdus = [
|
||||||
|
self.event_from_pdu_json(p, outlier=False)
|
||||||
|
for p in transaction.pdus
|
||||||
|
]
|
||||||
for pdu in pdus:
|
for pdu in pdus:
|
||||||
yield self._handle_new_pdu(pdu, backfilled=True)
|
yield self._handle_new_pdu(dest, pdu, backfilled=True)
|
||||||
|
|
||||||
defer.returnValue(pdus)
|
defer.returnValue(pdus)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_pdu(self, destination, pdu_origin, pdu_id, outlier=False):
|
def get_pdu(self, destination, event_id, outlier=False):
|
||||||
"""Requests the PDU with given origin and ID from the remote home
|
"""Requests the PDU with given origin and ID from the remote home
|
||||||
server.
|
server.
|
||||||
|
|
||||||
@@ -222,7 +226,7 @@ class ReplicationLayer(object):
|
|||||||
Args:
|
Args:
|
||||||
destination (str): Which home server to query
|
destination (str): Which home server to query
|
||||||
pdu_origin (str): The home server that originally sent the pdu.
|
pdu_origin (str): The home server that originally sent the pdu.
|
||||||
pdu_id (str)
|
event_id (str)
|
||||||
outlier (bool): Indicates whether the PDU is an `outlier`, i.e. if
|
outlier (bool): Indicates whether the PDU is an `outlier`, i.e. if
|
||||||
it's from an arbitary point in the context as opposed to part
|
it's from an arbitary point in the context as opposed to part
|
||||||
of the current block of PDUs. Defaults to `False`
|
of the current block of PDUs. Defaults to `False`
|
||||||
@@ -231,23 +235,27 @@ class ReplicationLayer(object):
|
|||||||
Deferred: Results in the requested PDU.
|
Deferred: Results in the requested PDU.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
transaction_data = yield self.transport_layer.get_pdu(
|
transaction_data = yield self.transport_layer.get_event(
|
||||||
destination, pdu_origin, pdu_id)
|
destination, event_id
|
||||||
|
)
|
||||||
|
|
||||||
transaction = Transaction(**transaction_data)
|
transaction = Transaction(**transaction_data)
|
||||||
|
|
||||||
pdu_list = [Pdu(outlier=outlier, **p) for p in transaction.pdus]
|
pdu_list = [
|
||||||
|
self.event_from_pdu_json(p, outlier=outlier)
|
||||||
|
for p in transaction.pdus
|
||||||
|
]
|
||||||
|
|
||||||
pdu = None
|
pdu = None
|
||||||
if pdu_list:
|
if pdu_list:
|
||||||
pdu = pdu_list[0]
|
pdu = pdu_list[0]
|
||||||
yield self._handle_new_pdu(pdu)
|
yield self._handle_new_pdu(destination, pdu)
|
||||||
|
|
||||||
defer.returnValue(pdu)
|
defer.returnValue(pdu)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def get_state_for_context(self, destination, context):
|
def get_state_for_context(self, destination, context, event_id=None):
|
||||||
"""Requests all of the `current` state PDUs for a given context from
|
"""Requests all of the `current` state PDUs for a given context from
|
||||||
a remote home server.
|
a remote home server.
|
||||||
|
|
||||||
@@ -260,29 +268,41 @@ class ReplicationLayer(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
transaction_data = yield self.transport_layer.get_context_state(
|
transaction_data = yield self.transport_layer.get_context_state(
|
||||||
destination, context)
|
destination,
|
||||||
|
context,
|
||||||
|
event_id=event_id,
|
||||||
|
)
|
||||||
|
|
||||||
transaction = Transaction(**transaction_data)
|
transaction = Transaction(**transaction_data)
|
||||||
|
pdus = [
|
||||||
pdus = [Pdu(outlier=True, **p) for p in transaction.pdus]
|
self.event_from_pdu_json(p, outlier=True)
|
||||||
for pdu in pdus:
|
for p in transaction.pdus
|
||||||
yield self._handle_new_pdu(pdu)
|
]
|
||||||
|
|
||||||
defer.returnValue(pdus)
|
defer.returnValue(pdus)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_context_pdus_request(self, context):
|
def get_event_auth(self, destination, context, event_id):
|
||||||
pdus = yield self.pdu_actions.get_all_pdus_from_context(
|
res = yield self.transport_layer.get_event_auth(
|
||||||
context
|
destination, context, event_id,
|
||||||
)
|
)
|
||||||
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
|
||||||
|
auth_chain = [
|
||||||
|
self.event_from_pdu_json(p, outlier=True)
|
||||||
|
for p in res["auth_chain"]
|
||||||
|
]
|
||||||
|
|
||||||
|
auth_chain.sort(key=lambda e: e.depth)
|
||||||
|
|
||||||
|
defer.returnValue(auth_chain)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_backfill_request(self, context, versions, limit):
|
def on_backfill_request(self, origin, context, versions, limit):
|
||||||
|
pdus = yield self.handler.on_backfill_request(
|
||||||
pdus = yield self.pdu_actions.backfill(context, versions, limit)
|
origin, context, versions, limit
|
||||||
|
)
|
||||||
|
|
||||||
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
||||||
|
|
||||||
@@ -291,6 +311,19 @@ class ReplicationLayer(object):
|
|||||||
def on_incoming_transaction(self, transaction_data):
|
def on_incoming_transaction(self, transaction_data):
|
||||||
transaction = Transaction(**transaction_data)
|
transaction = Transaction(**transaction_data)
|
||||||
|
|
||||||
|
for p in transaction.pdus:
|
||||||
|
if "unsigned" in p:
|
||||||
|
unsigned = p["unsigned"]
|
||||||
|
if "age" in unsigned:
|
||||||
|
p["age"] = unsigned["age"]
|
||||||
|
if "age" in p:
|
||||||
|
p["age_ts"] = int(self._clock.time_msec()) - int(p["age"])
|
||||||
|
del p["age"]
|
||||||
|
|
||||||
|
pdu_list = [
|
||||||
|
self.event_from_pdu_json(p) for p in transaction.pdus
|
||||||
|
]
|
||||||
|
|
||||||
logger.debug("[%s] Got transaction", transaction.transaction_id)
|
logger.debug("[%s] Got transaction", transaction.transaction_id)
|
||||||
|
|
||||||
response = yield self.transaction_actions.have_responded(transaction)
|
response = yield self.transaction_actions.have_responded(transaction)
|
||||||
@@ -303,17 +336,20 @@ class ReplicationLayer(object):
|
|||||||
|
|
||||||
logger.debug("[%s] Transacition is new", transaction.transaction_id)
|
logger.debug("[%s] Transacition is new", transaction.transaction_id)
|
||||||
|
|
||||||
pdu_list = [Pdu(**p) for p in transaction.pdus]
|
with PreserveLoggingContext():
|
||||||
|
dl = []
|
||||||
|
for pdu in pdu_list:
|
||||||
|
dl.append(self._handle_new_pdu(transaction.origin, pdu))
|
||||||
|
|
||||||
dl = []
|
if hasattr(transaction, "edus"):
|
||||||
for pdu in pdu_list:
|
for edu in [Edu(**x) for x in transaction.edus]:
|
||||||
dl.append(self._handle_new_pdu(pdu))
|
self.received_edu(
|
||||||
|
transaction.origin,
|
||||||
|
edu.edu_type,
|
||||||
|
edu.content
|
||||||
|
)
|
||||||
|
|
||||||
if hasattr(transaction, "edus"):
|
results = yield defer.DeferredList(dl)
|
||||||
for edu in [Edu(**x) for x in transaction.edus]:
|
|
||||||
self.received_edu(edu.origin, edu.edu_type, edu.content)
|
|
||||||
|
|
||||||
results = yield defer.DeferredList(dl)
|
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
for r in results:
|
for r in results:
|
||||||
@@ -339,20 +375,22 @@ class ReplicationLayer(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_context_state_request(self, context):
|
def on_context_state_request(self, origin, context, event_id):
|
||||||
results = yield self.store.get_current_state_for_context(
|
if event_id:
|
||||||
context
|
pdus = yield self.handler.get_state_for_pdu(
|
||||||
)
|
origin,
|
||||||
|
context,
|
||||||
|
event_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Specify an event")
|
||||||
|
|
||||||
logger.debug("Context returning %d results", len(results))
|
|
||||||
|
|
||||||
pdus = [Pdu.from_pdu_tuple(p) for p in results]
|
|
||||||
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_pdu_request(self, pdu_origin, pdu_id):
|
def on_pdu_request(self, origin, event_id):
|
||||||
pdu = yield self._get_persisted_pdu(pdu_id, pdu_origin)
|
pdu = yield self._get_persisted_pdu(origin, event_id)
|
||||||
|
|
||||||
if pdu:
|
if pdu:
|
||||||
defer.returnValue(
|
defer.returnValue(
|
||||||
@@ -364,20 +402,7 @@ class ReplicationLayer(object):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def on_pull_request(self, origin, versions):
|
def on_pull_request(self, origin, versions):
|
||||||
transaction_id = max([int(v) for v in versions])
|
raise NotImplementedError("Pull transacions not implemented")
|
||||||
|
|
||||||
response = yield self.pdu_actions.after_transaction(
|
|
||||||
transaction_id,
|
|
||||||
origin,
|
|
||||||
self.server_name
|
|
||||||
)
|
|
||||||
|
|
||||||
if not response:
|
|
||||||
response = []
|
|
||||||
|
|
||||||
defer.returnValue(
|
|
||||||
(200, self._transaction_from_pdus(response).get_dict())
|
|
||||||
)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_query_request(self, query_type, args):
|
def on_query_request(self, query_type, args):
|
||||||
@@ -385,90 +410,266 @@ class ReplicationLayer(object):
|
|||||||
response = yield self.query_handlers[query_type](args)
|
response = yield self.query_handlers[query_type](args)
|
||||||
defer.returnValue((200, response))
|
defer.returnValue((200, response))
|
||||||
else:
|
else:
|
||||||
defer.returnValue((404, "No handler for Query type '%s'"
|
defer.returnValue(
|
||||||
% (query_type)
|
(404, "No handler for Query type '%s'" % (query_type, ))
|
||||||
))
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
def on_make_join_request(self, context, user_id):
|
||||||
|
pdu = yield self.handler.on_make_join_request(context, user_id)
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
defer.returnValue({
|
||||||
|
"event": pdu.get_pdu_json(time_now),
|
||||||
|
})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_invite_request(self, origin, content):
|
||||||
|
pdu = self.event_from_pdu_json(content)
|
||||||
|
ret_pdu = yield self.handler.on_invite_request(origin, pdu)
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
defer.returnValue(
|
||||||
|
(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"event": ret_pdu.get_pdu_json(time_now),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_send_join_request(self, origin, content):
|
||||||
|
pdu = self.event_from_pdu_json(content)
|
||||||
|
res_pdus = yield self.handler.on_send_join_request(origin, pdu)
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
defer.returnValue((200, {
|
||||||
|
"state": [p.get_pdu_json(time_now) for p in res_pdus["state"]],
|
||||||
|
"auth_chain": [
|
||||||
|
p.get_pdu_json(time_now) for p in res_pdus["auth_chain"]
|
||||||
|
],
|
||||||
|
}))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_event_auth(self, origin, context, event_id):
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
auth_pdus = yield self.handler.on_event_auth(event_id)
|
||||||
|
defer.returnValue(
|
||||||
|
(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"auth_chain": [
|
||||||
|
a.get_pdu_json(time_now) for a in auth_pdus
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def make_join(self, destination, context, user_id):
|
||||||
|
ret = yield self.transport_layer.make_join(
|
||||||
|
destination=destination,
|
||||||
|
context=context,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
pdu_dict = ret["event"]
|
||||||
|
|
||||||
|
logger.debug("Got response to make_join: %s", pdu_dict)
|
||||||
|
|
||||||
|
defer.returnValue(self.event_from_pdu_json(pdu_dict))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def send_join(self, destination, pdu):
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
_, content = yield self.transport_layer.send_join(
|
||||||
|
destination,
|
||||||
|
pdu.room_id,
|
||||||
|
pdu.event_id,
|
||||||
|
pdu.get_pdu_json(time_now),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug("Got content: %s", content)
|
||||||
|
|
||||||
|
state = [
|
||||||
|
self.event_from_pdu_json(p, outlier=True)
|
||||||
|
for p in content.get("state", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
# FIXME: We probably want to do something with the auth_chain given
|
||||||
|
# to us
|
||||||
|
|
||||||
|
auth_chain = [
|
||||||
|
self.event_from_pdu_json(p, outlier=True)
|
||||||
|
for p in content.get("auth_chain", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
auth_chain.sort(key=lambda e: e.depth)
|
||||||
|
|
||||||
|
defer.returnValue({
|
||||||
|
"state": state,
|
||||||
|
"auth_chain": auth_chain,
|
||||||
|
})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def send_invite(self, destination, context, event_id, pdu):
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
code, content = yield self.transport_layer.send_invite(
|
||||||
|
destination=destination,
|
||||||
|
context=context,
|
||||||
|
event_id=event_id,
|
||||||
|
content=pdu.get_pdu_json(time_now),
|
||||||
|
)
|
||||||
|
|
||||||
|
pdu_dict = content["event"]
|
||||||
|
|
||||||
|
logger.debug("Got response to send_invite: %s", pdu_dict)
|
||||||
|
|
||||||
|
defer.returnValue(self.event_from_pdu_json(pdu_dict))
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def _get_persisted_pdu(self, pdu_id, pdu_origin):
|
def _get_persisted_pdu(self, origin, event_id, do_auth=True):
|
||||||
""" Get a PDU from the database with given origin and id.
|
""" Get a PDU from the database with given origin and id.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deferred: Results in a `Pdu`.
|
Deferred: Results in a `Pdu`.
|
||||||
"""
|
"""
|
||||||
pdu_tuple = yield self.store.get_pdu(pdu_id, pdu_origin)
|
return self.handler.get_persisted_pdu(
|
||||||
|
origin, event_id, do_auth=do_auth
|
||||||
defer.returnValue(Pdu.from_pdu_tuple(pdu_tuple))
|
)
|
||||||
|
|
||||||
def _transaction_from_pdus(self, pdu_list):
|
def _transaction_from_pdus(self, pdu_list):
|
||||||
"""Returns a new Transaction containing the given PDUs suitable for
|
"""Returns a new Transaction containing the given PDUs suitable for
|
||||||
transmission.
|
transmission.
|
||||||
"""
|
"""
|
||||||
|
time_now = self._clock.time_msec()
|
||||||
|
pdus = [p.get_pdu_json(time_now) for p in pdu_list]
|
||||||
return Transaction(
|
return Transaction(
|
||||||
pdus=[p.get_dict() for p in pdu_list],
|
|
||||||
origin=self.server_name,
|
origin=self.server_name,
|
||||||
ts=int(self._clock.time_msec()),
|
pdus=pdus,
|
||||||
|
origin_server_ts=int(time_now),
|
||||||
destination=None,
|
destination=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def _handle_new_pdu(self, pdu, backfilled=False):
|
def _handle_new_pdu(self, origin, pdu, backfilled=False):
|
||||||
# We reprocess pdus when we have seen them only as outliers
|
# We reprocess pdus when we have seen them only as outliers
|
||||||
existing = yield self._get_persisted_pdu(pdu.pdu_id, pdu.origin)
|
existing = yield self._get_persisted_pdu(
|
||||||
|
origin, pdu.event_id, do_auth=False
|
||||||
|
)
|
||||||
|
|
||||||
if existing and (not existing.outlier or pdu.outlier):
|
if existing and (not existing.outlier or pdu.outlier):
|
||||||
logger.debug("Already seen pdu %s %s", pdu.pdu_id, pdu.origin)
|
logger.debug("Already seen pdu %s", pdu.event_id)
|
||||||
defer.returnValue({})
|
defer.returnValue({})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
state = None
|
||||||
|
|
||||||
|
# We need to make sure we have all the auth events.
|
||||||
|
# for e_id, _ in pdu.auth_events:
|
||||||
|
# exists = yield self._get_persisted_pdu(
|
||||||
|
# origin,
|
||||||
|
# e_id,
|
||||||
|
# do_auth=False
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# if not exists:
|
||||||
|
# try:
|
||||||
|
# logger.debug(
|
||||||
|
# "_handle_new_pdu fetch missing auth event %s from %s",
|
||||||
|
# e_id,
|
||||||
|
# origin,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# yield self.get_pdu(
|
||||||
|
# origin,
|
||||||
|
# event_id=e_id,
|
||||||
|
# outlier=True,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# logger.debug("Processed pdu %s", e_id)
|
||||||
|
# except:
|
||||||
|
# logger.warn(
|
||||||
|
# "Failed to get auth event %s from %s",
|
||||||
|
# e_id,
|
||||||
|
# origin
|
||||||
|
# )
|
||||||
|
|
||||||
# Get missing pdus if necessary.
|
# Get missing pdus if necessary.
|
||||||
is_new = yield self.pdu_actions.is_new(pdu)
|
if not pdu.outlier:
|
||||||
if is_new and not pdu.outlier:
|
|
||||||
# We only backfill backwards to the min depth.
|
# We only backfill backwards to the min depth.
|
||||||
min_depth = yield self.store.get_min_depth_for_context(pdu.context)
|
min_depth = yield self.handler.get_min_depth_for_context(
|
||||||
|
pdu.room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"_handle_new_pdu min_depth for %s: %d",
|
||||||
|
pdu.room_id, min_depth
|
||||||
|
)
|
||||||
|
|
||||||
if min_depth and pdu.depth > min_depth:
|
if min_depth and pdu.depth > min_depth:
|
||||||
for pdu_id, origin in pdu.prev_pdus:
|
for event_id, hashes in pdu.prev_events:
|
||||||
exists = yield self._get_persisted_pdu(pdu_id, origin)
|
exists = yield self._get_persisted_pdu(
|
||||||
|
origin,
|
||||||
|
event_id,
|
||||||
|
do_auth=False
|
||||||
|
)
|
||||||
|
|
||||||
if not exists:
|
if not exists:
|
||||||
logger.debug("Requesting pdu %s %s", pdu_id, origin)
|
logger.debug(
|
||||||
|
"_handle_new_pdu requesting pdu %s",
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self.get_pdu(
|
yield self.get_pdu(
|
||||||
pdu.origin,
|
origin,
|
||||||
pdu_id=pdu_id,
|
event_id=event_id,
|
||||||
pdu_origin=origin
|
|
||||||
)
|
)
|
||||||
logger.debug("Processed pdu %s %s", pdu_id, origin)
|
logger.debug("Processed pdu %s", event_id)
|
||||||
except:
|
except:
|
||||||
# TODO(erikj): Do some more intelligent retries.
|
# TODO(erikj): Do some more intelligent retries.
|
||||||
logger.exception("Failed to get PDU")
|
logger.exception("Failed to get PDU")
|
||||||
|
else:
|
||||||
# Persist the Pdu, but don't mark it as processed yet.
|
# We need to get the state at this event, since we have reached
|
||||||
yield self.store.persist_event(pdu=pdu)
|
# a backward extremity edge.
|
||||||
|
logger.debug(
|
||||||
|
"_handle_new_pdu getting state for %s",
|
||||||
|
pdu.room_id
|
||||||
|
)
|
||||||
|
state = yield self.get_state_for_context(
|
||||||
|
origin, pdu.room_id, pdu.event_id,
|
||||||
|
)
|
||||||
|
|
||||||
if not backfilled:
|
if not backfilled:
|
||||||
ret = yield self.handler.on_receive_pdu(pdu, backfilled=backfilled)
|
ret = yield self.handler.on_receive_pdu(
|
||||||
|
origin,
|
||||||
|
pdu,
|
||||||
|
backfilled=backfilled,
|
||||||
|
state=state,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
ret = None
|
ret = None
|
||||||
|
|
||||||
yield self.pdu_actions.mark_as_processed(pdu)
|
# yield self.pdu_actions.mark_as_processed(pdu)
|
||||||
|
|
||||||
defer.returnValue(ret)
|
defer.returnValue(ret)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "<ReplicationLayer(%s)>" % self.server_name
|
return "<ReplicationLayer(%s)>" % self.server_name
|
||||||
|
|
||||||
|
def event_from_pdu_json(self, pdu_json, outlier=False):
|
||||||
class ReplicationHandler(object):
|
#TODO: Check we have all the PDU keys here
|
||||||
"""This defines the methods that the :py:class:`.ReplicationLayer` will
|
pdu_json.setdefault("hashes", {})
|
||||||
use to communicate with the rest of the home server.
|
pdu_json.setdefault("signatures", {})
|
||||||
"""
|
sender = pdu_json.pop("sender", None)
|
||||||
def on_receive_pdu(self, pdu):
|
if sender is not None:
|
||||||
raise NotImplementedError("on_receive_pdu")
|
pdu_json["user_id"] = sender
|
||||||
|
state_hash = pdu_json.get("unsigned", {}).pop("state_hash", None)
|
||||||
|
if state_hash is not None:
|
||||||
|
pdu_json["state_hash"] = state_hash
|
||||||
|
return self.event_factory.create_event(
|
||||||
|
pdu_json["type"], outlier=outlier, **pdu_json
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _TransactionQueue(object):
|
class _TransactionQueue(object):
|
||||||
@@ -479,7 +680,6 @@ class _TransactionQueue(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs, transaction_actions, transport_layer):
|
def __init__(self, hs, transaction_actions, transport_layer):
|
||||||
|
|
||||||
self.server_name = hs.hostname
|
self.server_name = hs.hostname
|
||||||
self.transaction_actions = transaction_actions
|
self.transaction_actions = transaction_actions
|
||||||
self.transport_layer = transport_layer
|
self.transport_layer = transport_layer
|
||||||
@@ -497,6 +697,9 @@ class _TransactionQueue(object):
|
|||||||
# destination -> list of tuple(edu, deferred)
|
# destination -> list of tuple(edu, deferred)
|
||||||
self.pending_edus_by_dest = {}
|
self.pending_edus_by_dest = {}
|
||||||
|
|
||||||
|
# destination -> list of tuple(failure, deferred)
|
||||||
|
self.pending_failures_by_dest = {}
|
||||||
|
|
||||||
# HACK to get unique tx id
|
# HACK to get unique tx id
|
||||||
self._next_txn_id = int(self._clock.time_msec())
|
self._next_txn_id = int(self._clock.time_msec())
|
||||||
|
|
||||||
@@ -525,7 +728,8 @@ class _TransactionQueue(object):
|
|||||||
(pdu, deferred, order)
|
(pdu, deferred, order)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attempt_new_transaction(destination)
|
with PreserveLoggingContext():
|
||||||
|
self._attempt_new_transaction(destination)
|
||||||
|
|
||||||
deferreds.append(deferred)
|
deferreds.append(deferred)
|
||||||
|
|
||||||
@@ -545,10 +749,24 @@ class _TransactionQueue(object):
|
|||||||
deferred.errback(failure)
|
deferred.errback(failure)
|
||||||
else:
|
else:
|
||||||
logger.exception("Failed to send edu", failure)
|
logger.exception("Failed to send edu", failure)
|
||||||
self._attempt_new_transaction(destination).addErrback(eb)
|
|
||||||
|
with PreserveLoggingContext():
|
||||||
|
self._attempt_new_transaction(destination).addErrback(eb)
|
||||||
|
|
||||||
return deferred
|
return deferred
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def enqueue_failure(self, failure, destination):
|
||||||
|
deferred = defer.Deferred()
|
||||||
|
|
||||||
|
self.pending_failures_by_dest.setdefault(
|
||||||
|
destination, []
|
||||||
|
).append(
|
||||||
|
(failure, deferred)
|
||||||
|
)
|
||||||
|
|
||||||
|
yield deferred
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def _attempt_new_transaction(self, destination):
|
def _attempt_new_transaction(self, destination):
|
||||||
@@ -558,8 +776,9 @@ class _TransactionQueue(object):
|
|||||||
# list of (pending_pdu, deferred, order)
|
# list of (pending_pdu, deferred, order)
|
||||||
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
pending_pdus = self.pending_pdus_by_dest.pop(destination, [])
|
||||||
pending_edus = self.pending_edus_by_dest.pop(destination, [])
|
pending_edus = self.pending_edus_by_dest.pop(destination, [])
|
||||||
|
pending_failures = self.pending_failures_by_dest.pop(destination, [])
|
||||||
|
|
||||||
if not pending_pdus and not pending_edus:
|
if not pending_pdus and not pending_edus and not pending_failures:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug("TX [%s] Attempting new transaction", destination)
|
logger.debug("TX [%s] Attempting new transaction", destination)
|
||||||
@@ -569,7 +788,11 @@ class _TransactionQueue(object):
|
|||||||
|
|
||||||
pdus = [x[0] for x in pending_pdus]
|
pdus = [x[0] for x in pending_pdus]
|
||||||
edus = [x[0] for x in pending_edus]
|
edus = [x[0] for x in pending_edus]
|
||||||
deferreds = [x[1] for x in pending_pdus + pending_edus]
|
failures = [x[0].get_dict() for x in pending_failures]
|
||||||
|
deferreds = [
|
||||||
|
x[1]
|
||||||
|
for x in pending_pdus + pending_edus + pending_failures
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.pending_transactions[destination] = 1
|
self.pending_transactions[destination] = 1
|
||||||
@@ -577,12 +800,13 @@ class _TransactionQueue(object):
|
|||||||
logger.debug("TX [%s] Persisting transaction...", destination)
|
logger.debug("TX [%s] Persisting transaction...", destination)
|
||||||
|
|
||||||
transaction = Transaction.create_new(
|
transaction = Transaction.create_new(
|
||||||
ts=self._clock.time_msec(),
|
origin_server_ts=int(self._clock.time_msec()),
|
||||||
transaction_id=self._next_txn_id,
|
transaction_id=str(self._next_txn_id),
|
||||||
origin=self.server_name,
|
origin=self.server_name,
|
||||||
destination=destination,
|
destination=destination,
|
||||||
pdus=pdus,
|
pdus=pdus,
|
||||||
edus=edus,
|
edus=edus,
|
||||||
|
pdu_failures=failures,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._next_txn_id += 1
|
self._next_txn_id += 1
|
||||||
@@ -593,8 +817,22 @@ class _TransactionQueue(object):
|
|||||||
logger.debug("TX [%s] Sending transaction...", destination)
|
logger.debug("TX [%s] Sending transaction...", destination)
|
||||||
|
|
||||||
# Actually send the transaction
|
# Actually send the transaction
|
||||||
|
|
||||||
|
# FIXME (erikj): This is a bit of a hack to make the Pdu age
|
||||||
|
# keys work
|
||||||
|
def json_data_cb():
|
||||||
|
data = transaction.get_dict()
|
||||||
|
now = int(self._clock.time_msec())
|
||||||
|
if "pdus" in data:
|
||||||
|
for p in data["pdus"]:
|
||||||
|
if "age_ts" in p:
|
||||||
|
unsigned = p.setdefault("unsigned", {})
|
||||||
|
unsigned["age"] = now - int(p["age_ts"])
|
||||||
|
del p["age_ts"]
|
||||||
|
return data
|
||||||
|
|
||||||
code, response = yield self.transport_layer.send_transaction(
|
code, response = yield self.transport_layer.send_transaction(
|
||||||
transaction
|
transaction, json_data_cb
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("TX [%s] Sent transaction", destination)
|
logger.debug("TX [%s] Sent transaction", destination)
|
||||||
@@ -628,7 +866,6 @@ class _TransactionQueue(object):
|
|||||||
|
|
||||||
for deferred in deferreds:
|
for deferred in deferreds:
|
||||||
deferred.errback(e)
|
deferred.errback(e)
|
||||||
yield deferred
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# We want to be *very* sure we delete this after we stop processing
|
# We want to be *very* sure we delete this after we stop processing
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user