mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-11 01:40:27 +00:00
Compare commits
904 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b63691f6e2 | ||
|
|
13fad06239 | ||
|
|
71ef8f0636 | ||
|
|
20cf0b7aeb | ||
|
|
ac2a177070 | ||
|
|
188de756be | ||
|
|
baf472f83f | ||
|
|
86d3180666 | ||
|
|
864de6a7a4 | ||
|
|
ea6bec96d3 | ||
|
|
f618f99ece | ||
|
|
0985bfb775 | ||
|
|
9de9661baa | ||
|
|
6f3f631fd1 | ||
|
|
40342af459 | ||
|
|
8e8bbb00f5 | ||
|
|
d5aa965522 | ||
|
|
7d709542ca | ||
|
|
b4b492824e | ||
|
|
0f192579ac | ||
|
|
beae9acfcc | ||
|
|
0d278f5da8 | ||
|
|
b1ee6fd7ed | ||
|
|
d6bcffa929 | ||
|
|
c5a25f610a | ||
|
|
194e1e9151 | ||
|
|
c2f2e26ec5 | ||
|
|
6d4617960d | ||
|
|
70137409ed | ||
|
|
ed241ba032 | ||
|
|
2a44558fbd | ||
|
|
51b81b472d | ||
|
|
4f6acf114c | ||
|
|
4841b6d4ba | ||
|
|
fc121f9785 | ||
|
|
332b2869ef | ||
|
|
f4e64ac253 | ||
|
|
da87990bd6 | ||
|
|
cf1feee21d | ||
|
|
6603e39e6a | ||
|
|
f3bb3943c9 | ||
|
|
7bd604e3be | ||
|
|
d56e389a95 | ||
|
|
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 | ||
|
|
5356044b77 | ||
|
|
71e6a94af7 | ||
|
|
5662be894e | ||
|
|
a065becea5 | ||
|
|
82c5820767 | ||
|
|
f5cf7ac25b | ||
|
|
456017e0ae | ||
|
|
be2a9a8d1a | ||
|
|
79bd6e77b8 | ||
|
|
da19fd0d1a | ||
|
|
07890b43ca | ||
|
|
f4667f86af | ||
|
|
13b560971e | ||
|
|
9aed791fc3 | ||
|
|
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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -18,9 +18,13 @@ 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/environment-protractor.js
|
||||||
|
|
||||||
uploads
|
uploads
|
||||||
|
|||||||
222
CHANGES.rst
222
CHANGES.rst
@@ -1,3 +1,225 @@
|
|||||||
|
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)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|||||||
205
README.rst
205
README.rst
@@ -1,65 +1,94 @@
|
|||||||
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
|
- Chatrooms are distributed and do not exist on any single server. Rooms
|
||||||
can be found using names like ``#matrix:matrix.org`` or
|
can be found using aliases like ``#matrix:matrix.org`` or
|
||||||
``#test:localhost:8080`` or they can be ephemeral.
|
``#test:localhost:8008`` or they can be ephemeral.
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
|
WARNING
|
||||||
|
=======
|
||||||
|
|
||||||
|
**Synapse is currently in a state of rapid development, and not all features
|
||||||
|
are yet functional. Critically, some security features are still in
|
||||||
|
development, which means Synapse can *not* be considered secure or reliable at
|
||||||
|
this point.** For instance:
|
||||||
|
|
||||||
|
- **SSL Certificates used by server-server federation are not yet validated.**
|
||||||
|
- **Room permissions are not yet enforced on traffic received via federation.**
|
||||||
|
- **Homeservers do not yet cryptographically sign their events to avoid
|
||||||
|
tampering**
|
||||||
|
- Default configuration provides open signup to the service from the internet
|
||||||
|
|
||||||
|
Despite this, we believe Synapse is more than useful as a way for experimenting
|
||||||
|
and exploring Synapse, and the missing features will land shortly. **Until
|
||||||
|
then, please do *NOT* use Synapse for any remotely important or secure
|
||||||
|
communication.**
|
||||||
|
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
===========
|
||||||
|
|
||||||
|
System requirements:
|
||||||
|
- POSIX-compliant system (tested on Linux & OSX)
|
||||||
|
- Python 2.7
|
||||||
|
|
||||||
To get up and running:
|
To get up and running:
|
||||||
|
|
||||||
- To simply play with an **existing** homeserver you can
|
- To simply play with an **existing** homeserver you can
|
||||||
just go straight to http://matrix.org/alpha.
|
just go straight to http://matrix.org/alpha.
|
||||||
|
|
||||||
- To run your own **private** homeserver on localhost:8080, install synapse
|
- To run your own **private** homeserver on localhost:8008, generate a basic
|
||||||
with ``python setup.py develop --user`` and then run one with
|
config file: ``./synctl start`` will give you instructions on how to do this.
|
||||||
``python synapse/app/homeserver.py`` - you will find a webclient running
|
For this purpose, you can use 'localhost' or your hostname as a server name.
|
||||||
at http://localhost:8080 (use a recent Chrome, Safari or Firefox for now,
|
Once you've done so, running ``./synctl start`` again will start your private
|
||||||
please...)
|
home sserver. You will find a webclient running at http://localhost:8008.
|
||||||
|
Please use a recent Chrome or Firefox for now (or Safari if you don't need
|
||||||
- To make the homeserver **public** and let it exchange messages with
|
VoIP support).
|
||||||
other homeservers and participate in the overall Matrix federation, open
|
|
||||||
up port 8080 and run ``python synapse/app/homeserver.py --host
|
- To run a **public** homeserver and let it exchange messages with other
|
||||||
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
|
homeservers and participate in the global Matrix federation, you must expose
|
||||||
say hi! :)
|
port 8448 to the internet and edit homeserver.yaml to specify server_name
|
||||||
|
(the public DNS entry for this server) and then run ``synctl start``. If you
|
||||||
|
changed the server_name, you may need to move the old database
|
||||||
|
(homeserver.db) out of the way first. Then come join ``#matrix:matrix.org``
|
||||||
|
and say hi! :)
|
||||||
|
|
||||||
For more detailed setup instructions, please see further down this document.
|
For more detailed setup instructions, please see further down this document.
|
||||||
|
|
||||||
[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[1] 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[2]
|
||||||
- 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,38 +103,37 @@ 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
|
We'd like to invite you to take a look at the Matrix spec, try to run a
|
||||||
homeserver, and join the existing Matrix chatrooms already out there, experiment
|
homeserver, and join the existing Matrix chatrooms already out there,
|
||||||
with the APIs and the demo clients, and let us know your thoughts at
|
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.
|
https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
|
||||||
|
|
||||||
Thanks for trying Matrix!
|
Thanks for trying Matrix!
|
||||||
|
|
||||||
[2] Cryptographic signing of messages isn't turned on yet
|
[1] Cryptographic signing of messages isn't turned on yet
|
||||||
|
|
||||||
[3] End-to-end encryption is currently in development
|
[2] End-to-end encryption is currently in development
|
||||||
|
|
||||||
|
|
||||||
Homeserver Installation
|
Homeserver Installation
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
First, the dependencies need to be installed. Start by installing
|
First, the dependencies need to be installed. Start by installing
|
||||||
'python2.7-dev' and the various tools of the compiler toolchain.
|
'python2.7-dev' and the various tools of the compiler toolchain.
|
||||||
N.B. synapse requires python 2.x where x >= 7
|
|
||||||
|
|
||||||
Installing prerequisites on ubuntu::
|
Installing prerequisites on Ubuntu::
|
||||||
|
|
||||||
$ sudo apt-get install build-essential python2.7-dev libffi-dev
|
$ sudo apt-get install build-essential python2.7-dev libffi-dev
|
||||||
|
|
||||||
Installing prerequisites on Mac OS X::
|
Installing prerequisites on Mac OS X::
|
||||||
|
|
||||||
$ xcode-select --install
|
$ xcode-select --install
|
||||||
|
|
||||||
@@ -113,22 +141,25 @@ 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
|
You'll need a version of setuptools new enough to know about git, so you
|
||||||
may need to also run:
|
may need to also run::
|
||||||
|
|
||||||
$ sudo apt-get install python-pip
|
$ sudo apt-get install python-pip
|
||||||
$ sudo pip install --upgrade setuptools
|
$ sudo pip install --upgrade setuptools
|
||||||
|
|
||||||
If you don't have access to github, then you may need to install ``syutil``
|
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
|
manually by checking it out and running ``python setup.py develop --user`` on
|
||||||
too.
|
it too.
|
||||||
|
|
||||||
If you get errors about ``sodium.h`` being missing, you may also need to
|
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
|
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
|
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
|
installing it. Installing PyNaCl using pip may also work (remember to remove
|
||||||
other versions installed by setuputils in, for example, ~/.local/lib).
|
any other versions installed by setuputils in, for example, ~/.local/lib).
|
||||||
|
|
||||||
|
On OSX, if you encounter ``clang: error: unknown argument: '-mno-fused-madd'``
|
||||||
|
you will need to ``export CFLAGS=-Qunused-arguments``.
|
||||||
|
|
||||||
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
|
||||||
@@ -151,7 +182,7 @@ Upgrading an existing homeserver
|
|||||||
|
|
||||||
Before upgrading an existing homeserver to a new version, please refer to
|
Before upgrading an existing homeserver to a new version, please refer to
|
||||||
UPGRADE.rst for any additional instructions.
|
UPGRADE.rst for any additional instructions.
|
||||||
|
|
||||||
|
|
||||||
Setting up Federation
|
Setting up Federation
|
||||||
=====================
|
=====================
|
||||||
@@ -161,19 +192,28 @@ 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::
|
--host parameter::
|
||||||
|
|
||||||
$ python synapse/app/homeserver.py --host machine.my.domain.name
|
$ python synapse/app/homeserver.py \
|
||||||
|
--server-name machine.my.domain.name \
|
||||||
|
--config-path homeserver.config \
|
||||||
|
--generate-config
|
||||||
|
$ python synapse/app/homeserver.py --config-path homeserver.config
|
||||||
|
|
||||||
|
Alternatively, you can run synapse via synctl - running ``synctl start`` to
|
||||||
|
generate a homeserver.yaml config file, where you can then edit server-name to
|
||||||
|
specify machine.my.domain.name, and then set the actual server running again
|
||||||
|
with synctl start.
|
||||||
|
|
||||||
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
|
||||||
@@ -186,7 +226,13 @@ record would then look something like::
|
|||||||
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 synapse/app/homeserver.py \
|
||||||
|
--server-name YOURDOMAIN \
|
||||||
|
--bind-port 8448 \
|
||||||
|
--config-path homeserver.config \
|
||||||
|
--generate-config
|
||||||
|
$ python synapse/app/homeserver.py --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.
|
||||||
@@ -208,7 +254,7 @@ http://localhost:8080. Simply run::
|
|||||||
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 +274,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 +290,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,7 +301,8 @@ 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?!
|
||||||
|
|||||||
70
UPGRADE.rst
70
UPGRADE.rst
@@ -1,3 +1,73 @@
|
|||||||
|
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.
|
||||||
|
|||||||
@@ -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,17 +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
|
||||||
|
|
||||||
|
python -m synapse.app.homeserver \
|
||||||
|
--config-path "demo/etc/$port.config" \
|
||||||
|
-vv \
|
||||||
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Starting webclient on port 8000..."
|
echo "Starting webclient on port 8000..."
|
||||||
|
|||||||
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)
|
||||||
@@ -2,6 +2,20 @@
|
|||||||
Matrix Client-Server API
|
Matrix Client-Server API
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
|
||||||
|
.. WARNING::
|
||||||
|
This specification is old. Please see /docs/specification.rst instead.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The following specification outlines how a client can send and receive data from
|
The following specification outlines how a client can send and receive data from
|
||||||
a home server.
|
a home server.
|
||||||
|
|
||||||
@@ -306,11 +320,11 @@ POST requests MUST be submitted as application/json.
|
|||||||
All paths MUST be namespaced by the version of the API being used. This should
|
All paths MUST be namespaced by the version of the API being used. This should
|
||||||
be:
|
be:
|
||||||
|
|
||||||
/matrix/client/api/v1
|
/_matrix/client/api/v1
|
||||||
|
|
||||||
All REST paths in this section MUST be prefixed with this. E.g.
|
All REST paths in this section MUST be prefixed with this. E.g.
|
||||||
REST Path: /rooms/$room_id
|
REST Path: /rooms/$room_id
|
||||||
Absolute Path: /matrix/client/api/v1/rooms/$room_id
|
Absolute Path: /_matrix/client/api/v1/rooms/$room_id
|
||||||
|
|
||||||
Registration
|
Registration
|
||||||
============
|
============
|
||||||
@@ -1007,26 +1021,15 @@ for users from other servers entirely.
|
|||||||
Presence
|
Presence
|
||||||
========
|
========
|
||||||
|
|
||||||
In the following messages, the presence state is an integer enumeration of the
|
In the following messages, the presence state is a presence string as described in
|
||||||
following states:
|
the main specification document.
|
||||||
0 : OFFLINE
|
|
||||||
1 : BUSY
|
|
||||||
2 : ONLINE
|
|
||||||
3 : FREE_TO_CHAT
|
|
||||||
|
|
||||||
Aside from OFFLINE, the protocol doesn't assign any special meaning to these
|
|
||||||
states; they are provided as an approximate signal for users to give to other
|
|
||||||
users and for clients to present them in some way that may be useful. Clients
|
|
||||||
could have different behaviours for different states of the user's presence, for
|
|
||||||
example to decide how much prominence or sound to use for incoming event
|
|
||||||
notifications.
|
|
||||||
|
|
||||||
Getting/Setting your own presence state
|
Getting/Setting your own presence state
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
REST Path: /presence/$user_id/status
|
REST Path: /presence/$user_id/status
|
||||||
Valid methods: GET/PUT
|
Valid methods: GET/PUT
|
||||||
Required keys:
|
Required keys:
|
||||||
state : [0|1|2|3] - The user's new presence state
|
presence : <string> - The user's new presence state
|
||||||
Optional keys:
|
Optional keys:
|
||||||
status_msg : text string provided by the user to explain their status
|
status_msg : text string provided by the user to explain their status
|
||||||
|
|
||||||
@@ -1039,7 +1042,7 @@ Fetching your presence list
|
|||||||
following keys:
|
following keys:
|
||||||
{
|
{
|
||||||
"user_id" : string giving the observed user's ID
|
"user_id" : string giving the observed user's ID
|
||||||
"state" : int giving their status
|
"presence" : int giving their status
|
||||||
"status_msg" : optional text string
|
"status_msg" : optional text string
|
||||||
"displayname" : optional text string from the user's profile
|
"displayname" : optional text string from the user's profile
|
||||||
"avatar_url" : optional text string from the user's profile
|
"avatar_url" : optional text string from the user's profile
|
||||||
@@ -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/**
|
|
||||||
@@ -1,103 +1,3 @@
|
|||||||
========
|
|
||||||
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
|
API Efficiency
|
||||||
==============
|
==============
|
||||||
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
docs/client-server/web/README
Normal file
5
docs/client-server/web/README
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
To get this running:
|
||||||
|
ln -s ../swagger_matrix
|
||||||
|
python -m SimpleHTTPServer
|
||||||
|
|
||||||
|
Go to http://localhost:8000/swagger.html
|
||||||
38
docs/client-server/web/files/backbone-min.js
vendored
Normal file
38
docs/client-server/web/files/backbone-min.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Backbone.js 0.9.2
|
||||||
|
|
||||||
|
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||||
|
// Backbone may be freely distributed under the MIT license.
|
||||||
|
// For all details and documentation:
|
||||||
|
// http://backbonejs.org
|
||||||
|
(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
|
||||||
|
{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
|
||||||
|
z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
|
||||||
|
{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
|
||||||
|
b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
|
||||||
|
b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
|
||||||
|
a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
|
||||||
|
h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
|
||||||
|
return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
|
||||||
|
{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
|
||||||
|
!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
|
||||||
|
this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
|
||||||
|
l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
|
||||||
|
a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
|
||||||
|
shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
|
||||||
|
this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
|
||||||
|
e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
|
||||||
|
{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
|
||||||
|
arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
|
||||||
|
C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
|
||||||
|
this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
|
||||||
|
""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
|
||||||
|
!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
|
||||||
|
this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
|
||||||
|
stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
|
||||||
|
function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
|
||||||
|
this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
|
||||||
|
f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
|
||||||
|
for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
|
||||||
|
!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
|
||||||
|
e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
|
||||||
|
b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);
|
||||||
16
docs/client-server/web/files/css
Normal file
16
docs/client-server/web/files/css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Droid Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Droid Sans'), local('DroidSans'), url(http://fonts.gstatic.com/s/droidsans/v5/s-BiyweUPV0v-yRb-cjciPk_vArhqVIZ0nv9q090hN8.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Droid Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Droid Sans Bold'), local('DroidSans-Bold'), url(http://fonts.gstatic.com/s/droidsans/v5/EFpQQyG9GqCrobXxL-KRMYWiMMZ7xLd792ULpGE4W_Y.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||||
|
}
|
||||||
2278
docs/client-server/web/files/handlebars-1.0.0.js
Normal file
2278
docs/client-server/web/files/handlebars-1.0.0.js
Normal file
File diff suppressed because it is too large
Load Diff
1
docs/client-server/web/files/highlight.7.3.pack.js
Normal file
1
docs/client-server/web/files/highlight.7.3.pack.js
Normal file
File diff suppressed because one or more lines are too long
2
docs/client-server/web/files/jquery-1.8.0.min.js
vendored
Normal file
2
docs/client-server/web/files/jquery-1.8.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
docs/client-server/web/files/jquery.ba-bbq.min.js
vendored
Normal file
18
docs/client-server/web/files/jquery.ba-bbq.min.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010
|
||||||
|
* http://benalman.com/projects/jquery-bbq-plugin/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010 "Cowboy" Ben Alman
|
||||||
|
* Dual licensed under the MIT and GPL licenses.
|
||||||
|
* http://benalman.com/about/license/
|
||||||
|
*/
|
||||||
|
(function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M<N?O[P]||(R[M+1]&&isNaN(R[M+1])?{}:[]):J}}else{if($.isArray(H[P])){H[P].push(J)}else{if(H[P]!==i){H[P]=[H[P],J]}else{H[P]=J}}}}else{if(P){H[P]=F?i:""}}});return H};function z(H,F,G){if(F===i||typeof F==="boolean"){G=F;F=a[H?D:A]()}else{F=E(F)?F.replace(H?w:x,""):F}return l(F,G)}l[A]=B(z,0);l[D]=v=B(z,1);$[y]||($[y]=function(F){return $.extend(C,F)})({a:k,base:k,iframe:t,img:t,input:t,form:"action",link:k,script:t});j=$[y];function s(I,G,H,F){if(!E(H)&&typeof H!=="object"){F=H;H=G;G=i}return this.each(function(){var L=$(this),J=G||j()[(this.nodeName||"").toLowerCase()]||"",K=J&&L.attr(J)||"";L.attr(J,a[I](K,H,F))})}$.fn[A]=B(s,A);$.fn[D]=B(s,D);b.pushState=q=function(I,F){if(E(I)&&/^#/.test(I)&&F===i){F=2}var H=I!==i,G=c(p[g][k],H?I:{},H?F:2);p[g][k]=G+(/#/.test(G)?"":"#")};b.getState=u=function(F,G){return F===i||typeof F==="boolean"?v(F):v(G)[F]};b.removeState=function(F){var G={};if(F!==i){G=u();$.each($.isArray(F)?F:arguments,function(I,H){delete G[H]})}q(G,2)};e[d]=$.extend(e[d],{add:function(F){var H;function G(J){var I=J[D]=c();J.getState=function(K,L){return K===i||typeof K==="boolean"?l(I,K):l(I,L)[K]};H.apply(this,arguments)}if($.isFunction(F)){H=F;return G}else{H=F.handler;F.handler=G}}})})(jQuery,this);
|
||||||
|
/*
|
||||||
|
* jQuery hashchange event - v1.2 - 2/11/2010
|
||||||
|
* http://benalman.com/projects/jquery-hashchange-plugin/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010 "Cowboy" Ben Alman
|
||||||
|
* Dual licensed under the MIT and GPL licenses.
|
||||||
|
* http://benalman.com/about/license/
|
||||||
|
*/
|
||||||
|
(function($,i,b){var j,k=$.event.special,c="location",d="hashchange",l="href",f=$.browser,g=document.documentMode,h=f.msie&&(g===b||g<8),e="on"+d in i&&!h;function a(m){m=m||i[c][l];return m.replace(/^[^#]*#?(.*)$/,"$1")}$[d+"Delay"]=100;k[d]=$.extend(k[d],{setup:function(){if(e){return false}$(j.start)},teardown:function(){if(e){return false}$(j.stop)}});j=(function(){var m={},r,n,o,q;function p(){o=q=function(s){return s};if(h){n=$('<iframe src="javascript:0"/>').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this);
|
||||||
1
docs/client-server/web/files/jquery.slideto.min.js
vendored
Normal file
1
docs/client-server/web/files/jquery.slideto.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
(function(b){b.fn.slideto=function(a){a=b.extend({slide_duration:"slow",highlight_duration:3E3,highlight:true,highlight_color:"#FFFF99"},a);return this.each(function(){obj=b(this);b("body").animate({scrollTop:obj.offset().top},a.slide_duration,function(){a.highlight&&b.ui.version&&obj.effect("highlight",{color:a.highlight_color},a.highlight_duration)})})}})(jQuery);
|
||||||
8
docs/client-server/web/files/jquery.wiggle.min.js
vendored
Normal file
8
docs/client-server/web/files/jquery.wiggle.min.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
jQuery Wiggle
|
||||||
|
Author: WonderGroup, Jordan Thomas
|
||||||
|
URL: http://labs.wondergroup.com/demos/mini-ui/index.html
|
||||||
|
License: MIT (http://en.wikipedia.org/wiki/MIT_License)
|
||||||
|
*/
|
||||||
|
jQuery.fn.wiggle=function(o){var d={speed:50,wiggles:3,travel:5,callback:null};var o=jQuery.extend(d,o);return this.each(function(){var cache=this;var wrap=jQuery(this).wrap('<div class="wiggle-wrap"></div>').css("position","relative");var calls=0;for(i=1;i<=o.wiggles;i++){jQuery(this).animate({left:"-="+o.travel},o.speed).animate({left:"+="+o.travel*2},o.speed*2).animate({left:"-="+o.travel},o.speed,function(){calls++;if(jQuery(cache).parent().hasClass('wiggle-wrap')){jQuery(cache).parent().replaceWith(cache);}
|
||||||
|
if(calls==o.wiggles&&jQuery.isFunction(o.callback)){o.callback();}});}});};
|
||||||
125
docs/client-server/web/files/reset.css
Normal file
125
docs/client-server/web/files/reset.css
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 */
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
applet,
|
||||||
|
object,
|
||||||
|
iframe,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
a,
|
||||||
|
abbr,
|
||||||
|
acronym,
|
||||||
|
address,
|
||||||
|
big,
|
||||||
|
cite,
|
||||||
|
code,
|
||||||
|
del,
|
||||||
|
dfn,
|
||||||
|
em,
|
||||||
|
img,
|
||||||
|
ins,
|
||||||
|
kbd,
|
||||||
|
q,
|
||||||
|
s,
|
||||||
|
samp,
|
||||||
|
small,
|
||||||
|
strike,
|
||||||
|
strong,
|
||||||
|
sub,
|
||||||
|
sup,
|
||||||
|
tt,
|
||||||
|
var,
|
||||||
|
b,
|
||||||
|
u,
|
||||||
|
i,
|
||||||
|
center,
|
||||||
|
dl,
|
||||||
|
dt,
|
||||||
|
dd,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
fieldset,
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
|
table,
|
||||||
|
caption,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
thead,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
canvas,
|
||||||
|
details,
|
||||||
|
embed,
|
||||||
|
figure,
|
||||||
|
figcaption,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
output,
|
||||||
|
ruby,
|
||||||
|
section,
|
||||||
|
summary,
|
||||||
|
time,
|
||||||
|
mark,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote,
|
||||||
|
q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before,
|
||||||
|
blockquote:after,
|
||||||
|
q:before,
|
||||||
|
q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
1221
docs/client-server/web/files/screen.css
Normal file
1221
docs/client-server/web/files/screen.css
Normal file
File diff suppressed because it is too large
Load Diff
2765
docs/client-server/web/files/shred.bundle.js
Normal file
2765
docs/client-server/web/files/shred.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
211
docs/client-server/web/files/swagger-oauth.js
Normal file
211
docs/client-server/web/files/swagger-oauth.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
var appName;
|
||||||
|
var popupMask;
|
||||||
|
var popupDialog;
|
||||||
|
var clientId;
|
||||||
|
var realm;
|
||||||
|
|
||||||
|
function handleLogin() {
|
||||||
|
var scopes = [];
|
||||||
|
|
||||||
|
if(window.swaggerUi.api.authSchemes
|
||||||
|
&& window.swaggerUi.api.authSchemes.oauth2
|
||||||
|
&& window.swaggerUi.api.authSchemes.oauth2.scopes) {
|
||||||
|
scopes = window.swaggerUi.api.authSchemes.oauth2.scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(window.swaggerUi.api
|
||||||
|
&& window.swaggerUi.api.info) {
|
||||||
|
appName = window.swaggerUi.api.info.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(popupDialog.length > 0)
|
||||||
|
popupDialog = popupDialog.last();
|
||||||
|
else {
|
||||||
|
popupDialog = $(
|
||||||
|
[
|
||||||
|
'<div class="api-popup-dialog">',
|
||||||
|
'<div class="api-popup-title">Select OAuth2.0 Scopes</div>',
|
||||||
|
'<div class="api-popup-content">',
|
||||||
|
'<p>Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.',
|
||||||
|
'<a href="#">Learn how to use</a>',
|
||||||
|
'</p>',
|
||||||
|
'<p><strong>' + appName + '</strong> API requires the following scopes. Select which ones you want to grant to Swagger UI.</p>',
|
||||||
|
'<ul class="api-popup-scopes">',
|
||||||
|
'</ul>',
|
||||||
|
'<p class="error-msg"></p>',
|
||||||
|
'<div class="api-popup-actions"><button class="api-popup-authbtn api-button green" type="button">Authorize</button><button class="api-popup-cancel api-button gray" type="button">Cancel</button></div>',
|
||||||
|
'</div>',
|
||||||
|
'</div>'].join(''));
|
||||||
|
$(document.body).append(popupDialog);
|
||||||
|
|
||||||
|
popup = popupDialog.find('ul.api-popup-scopes').empty();
|
||||||
|
for (i = 0; i < scopes.length; i ++) {
|
||||||
|
scope = scopes[i];
|
||||||
|
str = '<li><input type="checkbox" id="scope_' + i + '" scope="' + scope.scope + '"/>' + '<label for="scope_' + i + '">' + scope.scope;
|
||||||
|
if (scope.description) {
|
||||||
|
str += '<br/><span class="api-scope-desc">' + scope.description + '</span>';
|
||||||
|
}
|
||||||
|
str += '</label></li>';
|
||||||
|
popup.append(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var $win = $(window),
|
||||||
|
dw = $win.width(),
|
||||||
|
dh = $win.height(),
|
||||||
|
st = $win.scrollTop(),
|
||||||
|
dlgWd = popupDialog.outerWidth(),
|
||||||
|
dlgHt = popupDialog.outerHeight(),
|
||||||
|
top = (dh -dlgHt)/2 + st,
|
||||||
|
left = (dw - dlgWd)/2;
|
||||||
|
|
||||||
|
popupDialog.css({
|
||||||
|
top: (top < 0? 0 : top) + 'px',
|
||||||
|
left: (left < 0? 0 : left) + 'px'
|
||||||
|
});
|
||||||
|
|
||||||
|
popupDialog.find('button.api-popup-cancel').click(function() {
|
||||||
|
popupMask.hide();
|
||||||
|
popupDialog.hide();
|
||||||
|
});
|
||||||
|
popupDialog.find('button.api-popup-authbtn').click(function() {
|
||||||
|
popupMask.hide();
|
||||||
|
popupDialog.hide();
|
||||||
|
|
||||||
|
var authSchemes = window.swaggerUi.api.authSchemes;
|
||||||
|
var host = window.location;
|
||||||
|
var redirectUrl = host.protocol + '//' + host.host + "/o2c.html";
|
||||||
|
var url = null;
|
||||||
|
|
||||||
|
var p = window.swaggerUi.api.authSchemes;
|
||||||
|
for (var key in p) {
|
||||||
|
if (p.hasOwnProperty(key)) {
|
||||||
|
var o = p[key].grantTypes;
|
||||||
|
for(var t in o) {
|
||||||
|
if(o.hasOwnProperty(t) && t === 'implicit') {
|
||||||
|
var dets = o[t];
|
||||||
|
url = dets.loginEndpoint.url + "?response_type=token";
|
||||||
|
window.swaggerUi.tokenName = dets.tokenName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var scopes = []
|
||||||
|
var o = $('.api-popup-scopes').find('input:checked');
|
||||||
|
|
||||||
|
for(k =0; k < o.length; k++) {
|
||||||
|
scopes.push($(o[k]).attr("scope"));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.enabledScopes=scopes;
|
||||||
|
|
||||||
|
url += '&redirect_uri=' + encodeURIComponent(redirectUrl);
|
||||||
|
url += '&realm=' + encodeURIComponent(realm);
|
||||||
|
url += '&client_id=' + encodeURIComponent(clientId);
|
||||||
|
url += '&scope=' + encodeURIComponent(scopes);
|
||||||
|
|
||||||
|
window.open(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
popupMask.show();
|
||||||
|
popupDialog.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
for(key in window.authorizations.authz){
|
||||||
|
window.authorizations.remove(key)
|
||||||
|
}
|
||||||
|
window.enabledScopes = null;
|
||||||
|
$('.api-ic.ic-on').addClass('ic-off');
|
||||||
|
$('.api-ic.ic-on').removeClass('ic-on');
|
||||||
|
|
||||||
|
// set the info box
|
||||||
|
$('.api-ic.ic-warning').addClass('ic-error');
|
||||||
|
$('.api-ic.ic-warning').removeClass('ic-warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
function initOAuth(opts) {
|
||||||
|
var o = (opts||{});
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
appName = (o.appName||errors.push("missing appName"));
|
||||||
|
popupMask = (o.popupMask||$('#api-common-mask'));
|
||||||
|
popupDialog = (o.popupDialog||$('.api-popup-dialog'));
|
||||||
|
clientId = (o.clientId||errors.push("missing client id"));
|
||||||
|
realm = (o.realm||errors.push("missing realm"));
|
||||||
|
|
||||||
|
if(errors.length > 0){
|
||||||
|
log("auth unable initialize oauth: " + errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('pre code').each(function(i, e) {hljs.highlightBlock(e)});
|
||||||
|
$('.api-ic').click(function(s) {
|
||||||
|
if($(s.target).hasClass('ic-off'))
|
||||||
|
handleLogin();
|
||||||
|
else {
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOAuthComplete(token) {
|
||||||
|
if(token) {
|
||||||
|
if(token.error) {
|
||||||
|
var checkbox = $('input[type=checkbox],.secured')
|
||||||
|
checkbox.each(function(pos){
|
||||||
|
checkbox[pos].checked = false;
|
||||||
|
});
|
||||||
|
alert(token.error);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var b = token[window.swaggerUi.tokenName];
|
||||||
|
if(b){
|
||||||
|
// if all roles are satisfied
|
||||||
|
var o = null;
|
||||||
|
$.each($('.auth #api_information_panel'), function(k, v) {
|
||||||
|
var children = v;
|
||||||
|
if(children && children.childNodes) {
|
||||||
|
var requiredScopes = [];
|
||||||
|
$.each((children.childNodes), function (k1, v1){
|
||||||
|
var inner = v1.innerHTML;
|
||||||
|
if(inner)
|
||||||
|
requiredScopes.push(inner);
|
||||||
|
});
|
||||||
|
var diff = [];
|
||||||
|
for(var i=0; i < requiredScopes.length; i++) {
|
||||||
|
var s = requiredScopes[i];
|
||||||
|
if(window.enabledScopes && window.enabledScopes.indexOf(s) == -1) {
|
||||||
|
diff.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(diff.length > 0){
|
||||||
|
o = v.parentNode;
|
||||||
|
$(o.parentNode).find('.api-ic.ic-on').addClass('ic-off');
|
||||||
|
$(o.parentNode).find('.api-ic.ic-on').removeClass('ic-on');
|
||||||
|
|
||||||
|
// sorry, not all scopes are satisfied
|
||||||
|
$(o).find('.api-ic').addClass('ic-warning');
|
||||||
|
$(o).find('.api-ic').removeClass('ic-error');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
o = v.parentNode;
|
||||||
|
$(o.parentNode).find('.api-ic.ic-off').addClass('ic-on');
|
||||||
|
$(o.parentNode).find('.api-ic.ic-off').removeClass('ic-off');
|
||||||
|
|
||||||
|
// all scopes are satisfied
|
||||||
|
$(o).find('.api-ic').addClass('ic-info');
|
||||||
|
$(o).find('.api-ic').removeClass('ic-warning');
|
||||||
|
$(o).find('.api-ic').removeClass('ic-error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.authorizations.add("oauth2", new ApiKeyAuthorization("Authorization", "Bearer " + b, "header"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2315
docs/client-server/web/files/swagger-ui.js
Normal file
2315
docs/client-server/web/files/swagger-ui.js
Normal file
File diff suppressed because it is too large
Load Diff
1604
docs/client-server/web/files/swagger.js
Normal file
1604
docs/client-server/web/files/swagger.js
Normal file
File diff suppressed because it is too large
Load Diff
32
docs/client-server/web/files/underscore-min.js
vendored
Normal file
32
docs/client-server/web/files/underscore-min.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Underscore.js 1.3.3
|
||||||
|
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||||
|
// Underscore is freely distributable under the MIT license.
|
||||||
|
// Portions of Underscore are inspired or borrowed from Prototype,
|
||||||
|
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
||||||
|
// For all details and documentation:
|
||||||
|
// http://documentcloud.github.com/underscore
|
||||||
|
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
|
||||||
|
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
|
||||||
|
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
|
||||||
|
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
|
||||||
|
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
|
||||||
|
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
|
||||||
|
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
|
||||||
|
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
|
||||||
|
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
|
||||||
|
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
|
||||||
|
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
|
||||||
|
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
|
||||||
|
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
|
||||||
|
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
|
||||||
|
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
|
||||||
|
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
|
||||||
|
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
|
||||||
|
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
|
||||||
|
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
|
||||||
|
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
|
||||||
|
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
|
||||||
|
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
|
||||||
|
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
|
||||||
|
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
|
||||||
|
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
|
||||||
78
docs/client-server/web/swagger.html
Normal file
78
docs/client-server/web/swagger.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
||||||
|
<title>Matrix Client-Server API Documentation</title>
|
||||||
|
<link href="./files/css" rel="stylesheet" type="text/css">
|
||||||
|
<link href="./files/reset.css" media="screen" rel="stylesheet" type="text/css">
|
||||||
|
<link href="./files/screen.css" media="screen" rel="stylesheet" type="text/css">
|
||||||
|
<link href="./files/reset.css" media="print" rel="stylesheet" type="text/css">
|
||||||
|
<link href="./files/screen.css" media="print" rel="stylesheet" type="text/css">
|
||||||
|
<script type="text/javascript" src="./files/shred.bundle.js"></script>
|
||||||
|
<script src="./files/jquery-1.8.0.min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/jquery.slideto.min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/jquery.wiggle.min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/jquery.ba-bbq.min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/handlebars-1.0.0.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/underscore-min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/backbone-min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/swagger.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/swagger-ui.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/highlight.7.3.pack.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<!-- enabling this will enable oauth2 implicit scope support -->
|
||||||
|
<script src="./files/swagger-oauth.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function () {
|
||||||
|
window.swaggerUi = new SwaggerUi({
|
||||||
|
url: "http://localhost:8000/swagger_matrix/api-docs",
|
||||||
|
dom_id: "swagger-ui-container",
|
||||||
|
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
|
||||||
|
onComplete: function(swaggerApi, swaggerUi){
|
||||||
|
log("Loaded SwaggerUI");
|
||||||
|
|
||||||
|
if(typeof initOAuth == "function") {
|
||||||
|
initOAuth({
|
||||||
|
clientId: "your-client-id",
|
||||||
|
realm: "your-realms",
|
||||||
|
appName: "your-app-name"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('pre code').each(function(i, e) {
|
||||||
|
hljs.highlightBlock(e)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onFailure: function(data) {
|
||||||
|
log("Unable to Load SwaggerUI");
|
||||||
|
},
|
||||||
|
docExpansion: "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#input_apiKey').change(function() {
|
||||||
|
var key = $('#input_apiKey')[0].value;
|
||||||
|
log("key: " + key);
|
||||||
|
if(key && key.trim() != "") {
|
||||||
|
log("added key " + key);
|
||||||
|
window.authorizations.add("key", new ApiKeyAuthorization("access_token", key, "query"));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.swaggerUi.load();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="swagger-section">
|
||||||
|
<div id="header">
|
||||||
|
<div class="swagger-ui-wrap">
|
||||||
|
<a id="logo" href="http://swagger.wordnik.com/">swagger</a>
|
||||||
|
<form id="api_selector">
|
||||||
|
<div class="input"><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"></div>
|
||||||
|
<div class="input"><input placeholder="access_token" id="input_apiKey" name="apiKey" type="text"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message-bar" class="swagger-ui-wrap message-fail">Can't read from server. It may not have the appropriate access-control-origin settings.</div>
|
||||||
|
<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
|
||||||
|
|
||||||
|
|
||||||
|
</body></html>
|
||||||
@@ -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,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.
|
|
||||||
|
|
||||||
|
|
||||||
151
docs/server-server/signing.rst
Normal file
151
docs/server-server/signing.rst
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
Signing JSON
|
||||||
|
============
|
||||||
|
|
||||||
|
JSON is signed by encoding the JSON object without ``signatures`` or ``meta``
|
||||||
|
keys using a canonical encoding. The JSON bytes are then signed using the
|
||||||
|
signature algorithm and the signature encoded using base64 with the padding
|
||||||
|
stripped. The resulting base64 signature is added to an object under the
|
||||||
|
*signing key identifier* which is added to the ``signatures`` object under the
|
||||||
|
name of the server signing it which is added back to the original JSON object
|
||||||
|
along with the ``meta`` object.
|
||||||
|
|
||||||
|
The *signing key identifier* is the concatenation of the *signing algorithm*
|
||||||
|
and a *key version*. The *signing algorithm* identifies the algorithm used to
|
||||||
|
sign the JSON. The currently support value for *signing algorithm* is
|
||||||
|
``ed25519`` as implemented by NACL (http://nacl.cr.yp.to/). The *key version*
|
||||||
|
is used to distinguish between different signing keys used by the same entity.
|
||||||
|
|
||||||
|
The ``meta`` object and the ``signatures`` object are not covered by the
|
||||||
|
signature. Therefore intermediate servers can add metadata such as time stamps
|
||||||
|
and additional signatures.
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "example.org",
|
||||||
|
"signing_keys": {
|
||||||
|
"ed25519:1": "XSl0kuyvrXNj6A+7/tkrB9sxSbRi08Of5uRhxOqZtEQ"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"retrieved_ts_ms": 922834800000
|
||||||
|
},
|
||||||
|
"signatures": {
|
||||||
|
"example.org": {
|
||||||
|
"ed25519:1": "s76RUgajp8w172am0zQb/iPTHsRnb4SkrzGoeCOSFfcBY2V/1c8QfrmdXHpvnc2jK5BD1WiJIxiMW95fMjK7Bw"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
def sign_json(json_object, signing_key, signing_name):
|
||||||
|
signatures = json_object.pop("signatures", {})
|
||||||
|
meta = json_object.pop("meta", None)
|
||||||
|
|
||||||
|
signed = signing_key.sign(encode_canonical_json(json_object))
|
||||||
|
signature_base64 = encode_base64(signed.signature)
|
||||||
|
|
||||||
|
key_id = "%s:%s" % (signing_key.alg, signing_key.version)
|
||||||
|
signatures.setdefault(sigature_name, {})[key_id] = signature_base64
|
||||||
|
|
||||||
|
json_object["signatures"] = signatures
|
||||||
|
if meta is not None:
|
||||||
|
json_object["meta"] = meta
|
||||||
|
|
||||||
|
return json_object
|
||||||
|
|
||||||
|
Checking for a Signature
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
To check if an entity has signed a JSON object a server does the following
|
||||||
|
|
||||||
|
1. Checks if the ``signatures`` object contains an entry with the name of the
|
||||||
|
entity. If the entry is missing then the check fails.
|
||||||
|
2. Removes any *signing key identifiers* from the entry with algorithms it
|
||||||
|
doesn't understand. If there are no *signing key identifiers* left then the
|
||||||
|
check fails.
|
||||||
|
3. Looks up *verification keys* for the remaining *signing key identifiers*
|
||||||
|
either from a local cache or by consulting a trusted key server. If it
|
||||||
|
cannot find a *verification key* then the check fails.
|
||||||
|
4. Decodes the base64 encoded signature bytes. If base64 decoding fails then
|
||||||
|
the check fails.
|
||||||
|
5. Checks the signature bytes using the *verification key*. If this fails then
|
||||||
|
the check fails. Otherwise the check succeeds.
|
||||||
|
|
||||||
|
Canonical JSON
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The canonical JSON encoding for a value is the shortest UTF-8 JSON encoding
|
||||||
|
with dictionary keys lexicographically sorted by unicode codepoint. Numbers in
|
||||||
|
the JSON value must be integers in the range [-(2**53)+1, (2**53)-1].
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
def canonical_json(value):
|
||||||
|
return json.dumps(
|
||||||
|
value,
|
||||||
|
ensure_ascii=False,
|
||||||
|
separators=(',',':'),
|
||||||
|
sort_keys=True,
|
||||||
|
).encode("UTF-8")
|
||||||
|
|
||||||
|
Grammar
|
||||||
|
+++++++
|
||||||
|
|
||||||
|
Adapted from the grammar in http://tools.ietf.org/html/rfc7159 removing
|
||||||
|
insignificant whitespace, fractions, exponents and redundant character escapes
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
value = false / null / true / object / array / number / string
|
||||||
|
false = %x66.61.6c.73.65
|
||||||
|
null = %x6e.75.6c.6c
|
||||||
|
true = %x74.72.75.65
|
||||||
|
object = %x7B [ member *( %x2C member ) ] %7D
|
||||||
|
member = string %x3A value
|
||||||
|
array = %x5B [ value *( %x2C value ) ] %5B
|
||||||
|
number = [ %x2D ] int
|
||||||
|
int = %x30 / ( %x31-39 *digit )
|
||||||
|
digit = %x30-39
|
||||||
|
string = %x22 *char %x22
|
||||||
|
char = unescaped / %x5C escaped
|
||||||
|
unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
|
||||||
|
escaped = %x22 ; " quotation mark U+0022
|
||||||
|
/ %x5C ; \ reverse solidus U+005C
|
||||||
|
/ %x62 ; b backspace U+0008
|
||||||
|
/ %x66 ; f form feed U+000C
|
||||||
|
/ %x6E ; n line feed U+000A
|
||||||
|
/ %x72 ; r carriage return U+000D
|
||||||
|
/ %x74 ; t tab U+0009
|
||||||
|
/ %x75.30.30.30 (%x30-37 / %x62 / %x65-66) ; u000X
|
||||||
|
/ %x75.30.30.31 (%x30-39 / %x61-66) ; u001X
|
||||||
|
|
||||||
|
Signing Events
|
||||||
|
==============
|
||||||
|
|
||||||
|
Signing events is a more complicated process since servers can choose to redact
|
||||||
|
non-essential event contents. Before signing the event it is encoded as
|
||||||
|
Canonical JSON and hashed using SHA-256. The resulting hash is then stored
|
||||||
|
in the event JSON in a ``hash`` object under a ``sha256`` key. Then all
|
||||||
|
non-essential keys are stripped from the event object, and the resulting object
|
||||||
|
which included the ``hash`` key is signed using the JSON signing algorithm.
|
||||||
|
|
||||||
|
Servers can then transmit the entire event or the event with the non-essential
|
||||||
|
keys removed. Receiving servers can then check the entire event if it is
|
||||||
|
present by computing the SHA-256 of the event excluding the ``hash`` object, or
|
||||||
|
by using the ``hash`` object included in the event if keys have been redacted.
|
||||||
|
|
||||||
|
New hash functions can be introduced by adding additional keys to the ``hash``
|
||||||
|
object. Since the ``hash`` object cannot be redacted a server shouldn't allow
|
||||||
|
too many hashes to be listed, otherwise a server might embed illict data within
|
||||||
|
the ``hash`` object. For similar reasons a server shouldn't allow hash values
|
||||||
|
that are too long.
|
||||||
|
|
||||||
|
[[TODO(markjh): We might want to specify a maximum number of keys for the
|
||||||
|
``hash`` and we might want to specify the maximum output size of a hash]]
|
||||||
|
|
||||||
|
[[TODO(markjh) We might want to allow the server to omit the output of well
|
||||||
|
known hash functions like SHA-256 when none of the keys have been redacted]]
|
||||||
@@ -155,7 +155,7 @@ Protocol URLs
|
|||||||
|
|
||||||
All these URLs are namespaced within a prefix of
|
All these URLs are namespaced within a prefix of
|
||||||
|
|
||||||
/matrix/federation/v1/...
|
/_matrix/federation/v1/...
|
||||||
|
|
||||||
For active pushing of messages representing live activity "as it happens":
|
For active pushing of messages representing live activity "as it happens":
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
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
|
||||||
@@ -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.
|
||||||
|
|||||||
11
setup.py
11
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.
|
||||||
@@ -31,20 +31,23 @@ setup(
|
|||||||
packages=find_packages(exclude=["tests"]),
|
packages=find_packages(exclude=["tests"]),
|
||||||
description="Reference Synapse Home Server",
|
description="Reference Synapse Home Server",
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"syutil==0.0.1",
|
"syutil==0.0.2",
|
||||||
"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",
|
||||||
],
|
],
|
||||||
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,
|
||||||
|
|||||||
@@ -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.4.2"
|
||||||
|
|||||||
@@ -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,12 @@
|
|||||||
|
|
||||||
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,
|
||||||
|
)
|
||||||
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -44,16 +47,35 @@ class Auth(object):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if hasattr(event, "room_id"):
|
if hasattr(event, "room_id"):
|
||||||
|
is_state = hasattr(event, "state_key")
|
||||||
|
|
||||||
if event.type == RoomMemberEvent.TYPE:
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
|
yield self._can_replace_state(event)
|
||||||
allowed = yield self.is_membership_change_allowed(event)
|
allowed = yield self.is_membership_change_allowed(event)
|
||||||
defer.returnValue(allowed)
|
defer.returnValue(allowed)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._check_joined_room(
|
||||||
|
member=snapshot.membership_state,
|
||||||
|
user_id=snapshot.user_id,
|
||||||
|
room_id=snapshot.room_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_state:
|
||||||
|
# TODO (erikj): This really only should be called for *new*
|
||||||
|
# state
|
||||||
|
yield self._can_add_state(event)
|
||||||
|
yield self._can_replace_state(event)
|
||||||
else:
|
else:
|
||||||
self._check_joined_room(
|
yield self._can_send_event(event)
|
||||||
member=snapshot.membership_state,
|
|
||||||
user_id=snapshot.user_id,
|
if event.type == RoomPowerLevelsEvent.TYPE:
|
||||||
room_id=snapshot.room_id,
|
yield self._check_power_levels(event)
|
||||||
)
|
|
||||||
defer.returnValue(True)
|
if event.type == RoomRedactionEvent.TYPE:
|
||||||
|
yield self._check_redaction(event)
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown event: %s" % event)
|
raise AuthError(500, "Unknown event: %s" % event)
|
||||||
except AuthError as e:
|
except AuthError as e:
|
||||||
@@ -111,7 +133,14 @@ class Auth(object):
|
|||||||
|
|
||||||
membership = event.content["membership"]
|
membership = event.content["membership"]
|
||||||
|
|
||||||
|
join_rule = yield self.store.get_room_join_rule(event.room_id)
|
||||||
|
if not join_rule:
|
||||||
|
join_rule = JoinRules.INVITE
|
||||||
|
|
||||||
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, "You are not in room %s." % event.room_id)
|
||||||
@@ -124,23 +153,60 @@ 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 or room.is_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 (
|
||||||
raise AuthError(403, "You are not invited to this room.")
|
not caller or caller.membership not in
|
||||||
|
[Membership.INVITE, Membership.JOIN]
|
||||||
|
):
|
||||||
|
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, "You are not in room %s." % event.room_id)
|
||||||
elif target_user_id != event.user_id:
|
elif target_user_id != event.user_id:
|
||||||
# trying to force another user to leave
|
user_level = yield self.store.get_power_level(
|
||||||
raise AuthError(403, "Cannot force %s to leave." %
|
event.room_id,
|
||||||
target_user_id)
|
event.user_id,
|
||||||
|
)
|
||||||
|
_, kick_level, _ = yield self.store.get_ops_levels(event.room_id)
|
||||||
|
|
||||||
|
if kick_level:
|
||||||
|
kick_level = int(kick_level)
|
||||||
|
else:
|
||||||
|
kick_level = 50
|
||||||
|
|
||||||
|
if user_level < kick_level:
|
||||||
|
raise AuthError(
|
||||||
|
403, "You cannot kick user %s." % target_user_id
|
||||||
|
)
|
||||||
|
elif Membership.BAN == membership:
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
ban_level, _, _ = yield self.store.get_ops_levels(event.room_id)
|
||||||
|
|
||||||
|
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)
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
@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 +219,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:
|
||||||
|
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 +246,233 @@ 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
|
||||||
|
@log_function
|
||||||
|
def _can_send_event(self, event):
|
||||||
|
send_level = yield self.store.get_send_event_level(event.room_id)
|
||||||
|
|
||||||
|
if send_level:
|
||||||
|
send_level = int(send_level)
|
||||||
|
else:
|
||||||
|
send_level = 0
|
||||||
|
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 to the room"
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _can_add_state(self, event):
|
||||||
|
add_level = yield self.store.get_add_state_level(event.room_id)
|
||||||
|
|
||||||
|
if not add_level:
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
add_level = int(add_level)
|
||||||
|
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_level = int(user_level)
|
||||||
|
|
||||||
|
if user_level < add_level:
|
||||||
|
raise AuthError(
|
||||||
|
403, "You don't have permission to add state to the room"
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _can_replace_state(self, event):
|
||||||
|
current_state = yield self.store.get_current_state(
|
||||||
|
event.room_id,
|
||||||
|
event.type,
|
||||||
|
event.state_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_state:
|
||||||
|
current_state = current_state[0]
|
||||||
|
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_level:
|
||||||
|
user_level = int(user_level)
|
||||||
|
else:
|
||||||
|
user_level = 0
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Checking power level for %s, %s", event.user_id, user_level
|
||||||
|
)
|
||||||
|
if current_state and hasattr(current_state, "required_power_level"):
|
||||||
|
req = current_state.required_power_level
|
||||||
|
|
||||||
|
logger.debug("Checked power level for %s, %s", event.user_id, req)
|
||||||
|
if user_level < req:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to change that state"
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _check_redaction(self, event):
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_level:
|
||||||
|
user_level = int(user_level)
|
||||||
|
else:
|
||||||
|
user_level = 0
|
||||||
|
|
||||||
|
_, _, redact_level = yield self.store.get_ops_levels(event.room_id)
|
||||||
|
|
||||||
|
if not redact_level:
|
||||||
|
redact_level = 50
|
||||||
|
|
||||||
|
if user_level < redact_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to redact events"
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _check_power_levels(self, event):
|
||||||
|
for k, v in event.content.items():
|
||||||
|
if k == "default":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# FIXME (erikj): We don't want hsob_Ts in content.
|
||||||
|
if k == "hsob_ts":
|
||||||
|
continue
|
||||||
|
|
||||||
|
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,))
|
||||||
|
|
||||||
|
current_state = yield self.store.get_current_state(
|
||||||
|
event.room_id,
|
||||||
|
event.type,
|
||||||
|
event.state_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not current_state:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
current_state = current_state[0]
|
||||||
|
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_level:
|
||||||
|
user_level = int(user_level)
|
||||||
|
else:
|
||||||
|
user_level = 0
|
||||||
|
|
||||||
|
old_list = current_state.content
|
||||||
|
|
||||||
|
# FIXME (erikj)
|
||||||
|
old_people = {k: v for k, v in old_list.items() if k.startswith("@")}
|
||||||
|
new_people = {
|
||||||
|
k: v for k, v in event.content.items()
|
||||||
|
if k.startswith("@")
|
||||||
|
}
|
||||||
|
|
||||||
|
removed = set(old_people.keys()) - set(new_people.keys())
|
||||||
|
added = set(new_people.keys()) - set(old_people.keys())
|
||||||
|
same = set(old_people.keys()) & set(new_people.keys())
|
||||||
|
|
||||||
|
for r in removed:
|
||||||
|
if int(old_list[r]) > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to remove user: %s" % (r, )
|
||||||
|
)
|
||||||
|
|
||||||
|
for n in added:
|
||||||
|
if int(event.content[n]) > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to add ops level greater "
|
||||||
|
"than your own"
|
||||||
|
)
|
||||||
|
|
||||||
|
for s in same:
|
||||||
|
if int(event.content[s]) != int(old_list[s]):
|
||||||
|
if int(event.content[s]) > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to add ops level greater "
|
||||||
|
"than your own"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "default" in old_list:
|
||||||
|
old_default = int(old_list["default"])
|
||||||
|
|
||||||
|
if old_default > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to add ops level greater than "
|
||||||
|
"your own"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "default" in event.content:
|
||||||
|
new_default = int(event.content["default"])
|
||||||
|
|
||||||
|
if new_default > 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.
|
||||||
@@ -19,6 +19,7 @@ import logging
|
|||||||
|
|
||||||
|
|
||||||
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,6 +29,9 @@ 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):
|
||||||
@@ -38,21 +42,31 @@ class CodeMessageException(Exception):
|
|||||||
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,13 +105,40 @@ 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))
|
logging.error("Unknown exception type: %s", type(exception))
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +17,20 @@ 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 +56,22 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
"user_id", # sender/initiator
|
"user_id", # sender/initiator
|
||||||
"content", # HTTP body, JSON
|
"content", # HTTP body, JSON
|
||||||
"state_key",
|
"state_key",
|
||||||
|
"required_power_level",
|
||||||
|
"age_ts",
|
||||||
|
"prev_content",
|
||||||
|
"prev_state",
|
||||||
|
"redacted_because",
|
||||||
]
|
]
|
||||||
|
|
||||||
internal_keys = [
|
internal_keys = [
|
||||||
"is_state",
|
"is_state",
|
||||||
"prev_events",
|
"prev_events",
|
||||||
"prev_state",
|
|
||||||
"depth",
|
"depth",
|
||||||
"destinations",
|
"destinations",
|
||||||
"origin",
|
"origin",
|
||||||
"outlier",
|
"outlier",
|
||||||
|
"power_level",
|
||||||
|
"redacted",
|
||||||
]
|
]
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
@@ -139,7 +159,8 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
return "Missing %s key" % key
|
return "Missing %s key" % key
|
||||||
|
|
||||||
if type(content[key]) != type(template[key]):
|
if type(content[key]) != type(template[key]):
|
||||||
return "Key %s is of the wrong type." % 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:
|
if type(content[key]) == dict:
|
||||||
# we must go deeper
|
# we must go deeper
|
||||||
@@ -152,3 +173,11 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
msg = self._check_json(entry, template[key][0])
|
msg = self._check_json(entry, template[key][0])
|
||||||
if msg:
|
if msg:
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseStateEvent(SynapseEvent):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
if "state_key" not in kwargs:
|
||||||
|
kwargs["state_key"] = ""
|
||||||
|
super(SynapseStateEvent, self).__init__(**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.
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
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, RoomOpsPowerLevelsEvent,
|
||||||
|
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
@@ -30,7 +33,14 @@ class EventFactory(object):
|
|||||||
RoomMemberEvent,
|
RoomMemberEvent,
|
||||||
FeedbackEvent,
|
FeedbackEvent,
|
||||||
InviteJoinEvent,
|
InviteJoinEvent,
|
||||||
RoomConfigEvent
|
RoomConfigEvent,
|
||||||
|
RoomPowerLevelsEvent,
|
||||||
|
RoomJoinRulesEvent,
|
||||||
|
RoomCreateEvent,
|
||||||
|
RoomAddStateLevelEvent,
|
||||||
|
RoomSendEventLevelEvent,
|
||||||
|
RoomOpsPowerLevelsEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
@@ -39,14 +49,25 @@ 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
|
||||||
|
|
||||||
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"] = "%s@%s" % (
|
||||||
|
random_string(10), self.hs.hostname
|
||||||
|
)
|
||||||
|
|
||||||
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,61 @@ 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 RoomAddStateLevelEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.add_state_level"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomSendEventLevelEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.send_event_level"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomOpsPowerLevelsEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.ops_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 {}
|
||||||
|
|||||||
64
synapse/api/events/utils.py
Normal file
64
synapse/api/events/utils.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# -*- 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,
|
||||||
|
RoomAddStateLevelEvent, RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent,
|
||||||
|
RoomAliasesEvent, RoomCreateEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
def prune_event(event):
|
||||||
|
""" Prunes the given event of 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Remove all extraneous fields.
|
||||||
|
event.unrecognized_keys = {}
|
||||||
|
|
||||||
|
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:
|
||||||
|
# TODO: Actually check these are valid user_ids etc.
|
||||||
|
add_fields("default")
|
||||||
|
for k, v in event.content.items():
|
||||||
|
if k.startswith("@") and isinstance(v, (int, long)):
|
||||||
|
new_content[k] = v
|
||||||
|
elif event.type == RoomAddStateLevelEvent.TYPE:
|
||||||
|
add_fields("level")
|
||||||
|
elif event.type == RoomSendEventLevelEvent.TYPE:
|
||||||
|
add_fields("level")
|
||||||
|
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
|
||||||
|
add_fields("kick_level", "ban_level", "redact_level")
|
||||||
|
elif event.type == RoomAliasesEvent.TYPE:
|
||||||
|
add_fields("aliases")
|
||||||
|
|
||||||
|
event.content = new_content
|
||||||
|
|
||||||
|
return event
|
||||||
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,42 @@
|
|||||||
# 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.client import MatrixHttpClient
|
||||||
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 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
|
||||||
|
|
||||||
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 MatrixHttpClient(self)
|
||||||
|
|
||||||
def build_resource_for_client(self):
|
def build_resource_for_client(self):
|
||||||
return JsonResource()
|
return JsonResource()
|
||||||
@@ -74,47 +61,20 @@ class SynapseHomeServer(HomeServer):
|
|||||||
return File("webclient") # TODO configurable?
|
return File("webclient") # 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 +93,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.")
|
||||||
@@ -206,117 +167,80 @@ 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()
|
||||||
|
|
||||||
|
logging.info("Preparing database: %s...", db_name)
|
||||||
|
|
||||||
|
with sqlite3.connect(db_name) as db_conn:
|
||||||
|
prepare_database(db_conn)
|
||||||
|
|
||||||
|
logging.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:
|
hs.start_listening(config.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=reactor.run,
|
||||||
auto_close_fds=False,
|
auto_close_fds=False,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
@@ -324,7 +248,7 @@ def setup():
|
|||||||
|
|
||||||
daemon.start()
|
daemon.start()
|
||||||
else:
|
else:
|
||||||
run()
|
reactor.run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__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.
|
||||||
|
|
||||||
140
synapse/config/_base.py
Normal file
140
synapse/config/_base.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# -*- 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."
|
||||||
|
" Try running again with --generate-config"
|
||||||
|
% (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")
|
||||||
68
synapse/config/logger.py
Normal file
68
synapse/config/logger.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# -*- 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 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 - %(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
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=level,
|
||||||
|
filename=self.log_file,
|
||||||
|
format=log_format
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
)
|
||||||
112
synapse/config/server.py
Normal file
112
synapse/config/server.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# -*- 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
|
||||||
|
|
||||||
|
if not args.content_addr:
|
||||||
|
host = args.server_name
|
||||||
|
if ':' not in host:
|
||||||
|
host = "%s:%d" % (host, args.bind_port)
|
||||||
|
args.content_addr = "https://%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")
|
||||||
|
|
||||||
|
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
|
||||||
@@ -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,9 @@
|
|||||||
|
|
||||||
|
|
||||||
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_endpoint
|
||||||
from twisted.names.srvconnect import SRVConnector
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -30,15 +30,19 @@ 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_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,
|
protocol = yield endpoint.connect(factory)
|
||||||
connectFuncKwArgs=dict(contextFactory=ssl_context_factory)).connect()
|
server_response, server_certificate = yield protocol.remote_key
|
||||||
|
defer.returnValue((server_response, server_certificate))
|
||||||
server_key, server_certificate = yield factory.remote_key
|
return
|
||||||
|
except Exception as e:
|
||||||
defer.returnValue((server_key, server_certificate))
|
logger.exception(e)
|
||||||
|
raise IOError("Cannot get key for %s" % server_name)
|
||||||
|
|
||||||
|
|
||||||
class SynapseKeyClientError(Exception):
|
class SynapseKeyClientError(Exception):
|
||||||
@@ -51,69 +55,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()
|
||||||
|
|
||||||
|
self.store.store_server_certificate(
|
||||||
|
server_name,
|
||||||
|
server_name,
|
||||||
|
time_now_ms,
|
||||||
|
tls_certificate,
|
||||||
|
)
|
||||||
|
|
||||||
|
for key_id, key in verify_keys.items():
|
||||||
|
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)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user