Compare commits
621 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8a921f6a6 | ||
|
|
47c7dd590d | ||
|
|
9a7f7cb74f | ||
|
|
18f0247491 | ||
|
|
731c33dd97 | ||
|
|
c2c3ee8e4a | ||
|
|
211a8b288a | ||
|
|
e166e29e87 | ||
|
|
235f686da9 | ||
|
|
464e1fcfa5 | ||
|
|
e565a4bfc4 | ||
|
|
d5da6b0cef | ||
|
|
aa337f588c | ||
|
|
4b8244fbf8 | ||
|
|
4ac80b8570 | ||
|
|
5539251d82 | ||
|
|
5c778f2f15 | ||
|
|
fdcb876495 | ||
|
|
d9d6fbb085 | ||
|
|
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 | ||
|
|
52203edbce | ||
|
|
c3a774e414 | ||
|
|
d5c94c922f | ||
|
|
f07f538ac7 | ||
|
|
463b95f0c2 | ||
|
|
0ef54caa28 | ||
|
|
1118f02689 | ||
|
|
1cdc29e260 | ||
|
|
339dd3dc6c | ||
|
|
27047d8f51 | ||
|
|
7c4b47652e | ||
|
|
bf6466f02a | ||
|
|
8e2d4c6da5 | ||
|
|
e76cd252fe | ||
|
|
4b7f6dd7fc | ||
|
|
fbdacce3fe | ||
|
|
898dde8812 | ||
|
|
073bec4830 | ||
|
|
cc413be446 | ||
|
|
ee06023573 | ||
|
|
3e6a19cf09 | ||
|
|
5308e3026a | ||
|
|
eab463fda5 | ||
|
|
47fb286184 | ||
|
|
5dd38d579b | ||
|
|
ac56ac67cc | ||
|
|
171d8b032f | ||
|
|
41d02ab674 | ||
|
|
1abc93d65c | ||
|
|
ee079cd250 | ||
|
|
d1bf659ed7 | ||
|
|
089d1b1b78 | ||
|
|
9b2cb41dcf | ||
|
|
96baf62e7a | ||
|
|
246b2a3c3e | ||
|
|
ca7426eee0 | ||
|
|
8113eb7c79 | ||
|
|
aaf4fd98ee | ||
|
|
722c19d033 | ||
|
|
d7ae9b90a0 | ||
|
|
1b7686329e | ||
|
|
068b348e7e | ||
|
|
2c7c12bc6e | ||
|
|
54d0a75573 | ||
|
|
a8d318cf82 | ||
|
|
efc5f3440d | ||
|
|
113342a756 | ||
|
|
b1da3fa0a7 | ||
|
|
c46c806126 | ||
|
|
eb3094ed31 | ||
|
|
b09e531159 | ||
|
|
62dfa3c741 | ||
|
|
7b079a26a5 | ||
|
|
bddc1d9fff | ||
|
|
e0ba81344c | ||
|
|
c44293db2f | ||
|
|
7c99ebdbd1 | ||
|
|
06c79a23d4 | ||
|
|
466fbe4c4e | ||
|
|
b8b52ca09d | ||
|
|
8d7d251c35 | ||
|
|
52cfdfd5f1 | ||
|
|
7acede1e42 | ||
|
|
15ab5f5ad8 | ||
|
|
b485d622cc | ||
|
|
64e927108b | ||
|
|
d2bc5d6f29 | ||
|
|
f3f32addca | ||
|
|
6ac298f2f1 | ||
|
|
660129deb1 | ||
|
|
7d34a1c108 | ||
|
|
d027e859cd | ||
|
|
407c86c013 | ||
|
|
c2b4b73751 | ||
|
|
04fdcf302d | ||
|
|
357dd1871d | ||
|
|
e111a06e0a | ||
|
|
410a74b0f3 | ||
|
|
92033e4ebc | ||
|
|
2aeaa7b77c | ||
|
|
7c89d5e97a | ||
|
|
226025e9ca | ||
|
|
f54b70520a | ||
|
|
f53c4300fd | ||
|
|
6ad9d9c226 | ||
|
|
234c50b834 | ||
|
|
1d95e78759 | ||
|
|
b30358f439 | ||
|
|
f64887e15c | ||
|
|
52cb5e6324 | ||
|
|
4e8d19ee2b | ||
|
|
8af5e360d6 | ||
|
|
d9155b6a25 | ||
|
|
7ee5288849 | ||
|
|
e179ed1f60 | ||
|
|
89c044c2a0 | ||
|
|
7917ff1271 | ||
|
|
abe2035d85 | ||
|
|
08881d808d | ||
|
|
bfe9faad5a | ||
|
|
05672a6a8c | ||
|
|
fb9661898d | ||
|
|
a0d1f5a014 | ||
|
|
87190a9673 | ||
|
|
308c9273fa | ||
|
|
c67cac134f | ||
|
|
43242a0657 | ||
|
|
b1352f97ac | ||
|
|
6691ca6f8d | ||
|
|
e40d829363 | ||
|
|
c585c87c4b | ||
|
|
1d9d287c7c | ||
|
|
46a2f6a816 | ||
|
|
a03c7f27a8 | ||
|
|
77a255c7c3 | ||
|
|
47519cd8c2 | ||
|
|
bd16b93e8f | ||
|
|
474d913712 | ||
|
|
dddf5c0cc8 | ||
|
|
05fa81fee4 | ||
|
|
71095f4e6e | ||
|
|
6c609425ba | ||
|
|
5eff05a4ce | ||
|
|
d63f775e06 | ||
|
|
e677a3114e | ||
|
|
648796ef1d | ||
|
|
a8774cf351 | ||
|
|
135a1aa229 | ||
|
|
474dcecb11 | ||
|
|
dd661769e1 | ||
|
|
bf05218c4b | ||
|
|
c65885e166 | ||
|
|
dfa0cd1d90 | ||
|
|
d2798de660 | ||
|
|
67c5f89244 | ||
|
|
c1cf0b334e | ||
|
|
93cff1668c | ||
|
|
3a2a5b959c | ||
|
|
6966971a28 | ||
|
|
a498df0428 | ||
|
|
64e2a5d58e | ||
|
|
f84ddc75cb | ||
|
|
5dd8087ea4 | ||
|
|
73a1022bca | ||
|
|
5a3df1d029 | ||
|
|
6f0bba1934 | ||
|
|
5a93bfe1f0 | ||
|
|
ad6d5ac06c | ||
|
|
8885c8546c | ||
|
|
9a93e83d90 | ||
|
|
66a4d33524 | ||
|
|
d0103400b5 | ||
|
|
2e70de09b9 | ||
|
|
47c1a3d454 | ||
|
|
3281fec07a | ||
|
|
a29d12a18a | ||
|
|
4b63b06cad | ||
|
|
3df5cb804f | ||
|
|
b1e98ddc09 | ||
|
|
ac21dfff6d | ||
|
|
32347bfcc9 | ||
|
|
bcf8eb687a | ||
|
|
0e7a41dc99 | ||
|
|
8bd55cfdcb | ||
|
|
ff3709e577 | ||
|
|
c21fcb3373 | ||
|
|
b07bc9bdbd | ||
|
|
27979028b2 | ||
|
|
9ff9caeb74 | ||
|
|
5c0be8fde3 | ||
|
|
4b2ad549d5 | ||
|
|
732d954f89 | ||
|
|
485bb64ddb | ||
|
|
1291ac93f3 | ||
|
|
a664ec20e0 | ||
|
|
7d79021c42 | ||
|
|
f6daa9f170 | ||
|
|
b01aeac842 | ||
|
|
5796232cb1 | ||
|
|
52b64617f9 | ||
|
|
fea7b60cf3 | ||
|
|
b52b33acf6 | ||
|
|
47c3a089c5 | ||
|
|
cab3095803 | ||
|
|
be6abdff19 | ||
|
|
95839212a7 | ||
|
|
66d752dd1b | ||
|
|
1bd380c816 | ||
|
|
8b0473d5b9 | ||
|
|
2c4908ed26 | ||
|
|
4521c2d277 | ||
|
|
0c3b4a1f63 | ||
|
|
9d86c8c7a6 | ||
|
|
a9a5329a11 | ||
|
|
3f08a7ad21 | ||
|
|
d2bb28d2df | ||
|
|
45e70a6b70 | ||
|
|
68f4d73717 | ||
|
|
104808107a | ||
|
|
cda4ff8519 | ||
|
|
5b058a79cb | ||
|
|
b18db63c06 | ||
|
|
537ecd4e99 | ||
|
|
9f514915af | ||
|
|
31e7cec486 | ||
|
|
41d1db2d4a | ||
|
|
de0706493a | ||
|
|
4c7df52360 | ||
|
|
1379dcae6f | ||
|
|
61cac4df6e | ||
|
|
aaf623fa53 | ||
|
|
f40844def2 | ||
|
|
a96076f335 | ||
|
|
f690b7b827 | ||
|
|
e1297c922d | ||
|
|
239622f80b | ||
|
|
9521e6758f | ||
|
|
f81692dab4 | ||
|
|
a0e114fe64 | ||
|
|
7d3a841a83 | ||
|
|
87b315ce21 | ||
|
|
e3c6c9057b | ||
|
|
808f663ed1 | ||
|
|
1317afcb9a | ||
|
|
5494815c70 | ||
|
|
c2e983b8db | ||
|
|
c7d7bc0254 | ||
|
|
f3cea238b9 | ||
|
|
47a4bff139 | ||
|
|
6118a102c1 | ||
|
|
74c90f7815 | ||
|
|
dde50d4245 | ||
|
|
3c349b408b | ||
|
|
acf5127604 | ||
|
|
53f4fbd99a | ||
|
|
c8d0c4762d | ||
|
|
be2f948da5 | ||
|
|
8f7fbc1bb0 | ||
|
|
8d5ceccfc7 | ||
|
|
3248aed03b | ||
|
|
868fa1a1e3 | ||
|
|
fd47f55e94 | ||
|
|
ab27b49ded | ||
|
|
019f3a66f6 | ||
|
|
1b0d427285 | ||
|
|
3277a65052 | ||
|
|
0045a2647a | ||
|
|
2e1ab9db08 | ||
|
|
5670da1c1e | ||
|
|
7dac1bfc91 | ||
|
|
e7ee0b9fc1 | ||
|
|
ad869fa4b3 | ||
|
|
2b1297c501 | ||
|
|
4c228df167 | ||
|
|
14b9989660 | ||
|
|
01a129cb9a | ||
|
|
bb4490c2d7 | ||
|
|
3d1cae0e79 | ||
|
|
c6950b18cc | ||
|
|
063e1b22e6 | ||
|
|
1587ea26fe | ||
|
|
e4f0e1af1a | ||
|
|
aac52fce15 | ||
|
|
9d4bc8985f | ||
|
|
efe5aa6464 | ||
|
|
81a95937de | ||
|
|
7bec359408 | ||
|
|
d12a7c3939 | ||
|
|
ebd3c41ede | ||
|
|
7371e68f55 | ||
|
|
5048f4a915 | ||
|
|
2f52e8ee18 | ||
|
|
96da42085c | ||
|
|
583add34fe | ||
|
|
50718825bd | ||
|
|
e01bdf2432 | ||
|
|
9c0e570496 | ||
|
|
ba88c9105c | ||
|
|
6d3391f2f0 | ||
|
|
da2f5aac0e | ||
|
|
e8244c23ba | ||
|
|
d100ac8c82 | ||
|
|
5c4c591c61 | ||
|
|
beb0a179bd | ||
|
|
5ef0948eaa | ||
|
|
f60e5a1aec | ||
|
|
d6a3639269 | ||
|
|
955662d64c | ||
|
|
849627b82e | ||
|
|
2ffb075772 | ||
|
|
ecce301632 | ||
|
|
f4839ea042 | ||
|
|
89ed81bb1f | ||
|
|
9ca5bc7892 | ||
|
|
d4fb1c8a92 | ||
|
|
ae493c9418 | ||
|
|
e2b861cc67 | ||
|
|
eea2dc7dde | ||
|
|
d94765999d | ||
|
|
b796d4b9d0 | ||
|
|
cc48e920d6 | ||
|
|
41333452e5 | ||
|
|
7c60905ee7 | ||
|
|
5c00614aab | ||
|
|
4f773de6ba | ||
|
|
c5d601d5cd | ||
|
|
22dd0b37c4 | ||
|
|
89cabba3e0 | ||
|
|
347242a5c4 | ||
|
|
7e83a58c4d | ||
|
|
840771190f | ||
|
|
234128586b | ||
|
|
d7cfb91a7a | ||
|
|
992782b9f5 | ||
|
|
fcdc40a5dd | ||
|
|
e636e8799e | ||
|
|
75b6d982a0 | ||
|
|
598a1d8ff9 | ||
|
|
77f1cc7d6d | ||
|
|
8464009a66 | ||
|
|
185a68b473 | ||
|
|
caef65d819 | ||
|
|
ece7a6d995 | ||
|
|
88f7482b92 | ||
|
|
83f031207e | ||
|
|
6fafa878f6 | ||
|
|
bb793019a5 | ||
|
|
f48792eec4 | ||
|
|
509ce6c137 | ||
|
|
ff21d4d93b | ||
|
|
d7a4f2ed7f | ||
|
|
38f5c1c378 | ||
|
|
f144f8cc56 | ||
|
|
c3f1548bb4 | ||
|
|
cdc5ffe2a2 | ||
|
|
e37b040bc3 | ||
|
|
58548ab557 | ||
|
|
590ab24c85 | ||
|
|
35da1bf4a3 | ||
|
|
a18b1a649c | ||
|
|
ecfdf23250 | ||
|
|
301e55d11d | ||
|
|
f8693c6b48 | ||
|
|
43772d0b15 | ||
|
|
1422a22970 | ||
|
|
4eb8f84aa8 | ||
|
|
cebceb7b9d | ||
|
|
e5257b21b3 | ||
|
|
709a92cee8 | ||
|
|
b4a1f2ccb5 | ||
|
|
fc26275bb3 | ||
|
|
b37ced8f63 | ||
|
|
c12f55aa3b | ||
|
|
faf25e3a83 | ||
|
|
7d324612ec | ||
|
|
1c2caacd67 | ||
|
|
663a259d64 | ||
|
|
291010f100 | ||
|
|
2f91d16033 | ||
|
|
1a1e0384ef | ||
|
|
bc2512fa95 | ||
|
|
dccb2f57be | ||
|
|
f65176564f | ||
|
|
71584930cb | ||
|
|
0b5674ccc5 | ||
|
|
d5bebc9eaa | ||
|
|
39ff6c840f | ||
|
|
62b67879cd | ||
|
|
60245c4f90 | ||
|
|
48f4497fe9 | ||
|
|
1c202f9f7a | ||
|
|
a56a346343 | ||
|
|
00c0737b0e | ||
|
|
831c218a93 | ||
|
|
54c47f962b | ||
|
|
1c36118d98 | ||
|
|
8c69eff14c | ||
|
|
f1d140eea8 | ||
|
|
fe25e65f3f | ||
|
|
e4770bb039 | ||
|
|
dc6212b6fb | ||
|
|
ce4ca473cb | ||
|
|
b60283473a | ||
|
|
98ed3d0222 | ||
|
|
00e8be516a | ||
|
|
fc846aa771 | ||
|
|
0f9b633af7 | ||
|
|
207ef144c5 | ||
|
|
4068339770 | ||
|
|
9f7c5f161c | ||
|
|
60a9f27edb | ||
|
|
7f5c7ddea9 | ||
|
|
0e6a2f87f9 | ||
|
|
f5fca6f787 | ||
|
|
5b817ecd44 | ||
|
|
02e45da895 | ||
|
|
1731781145 | ||
|
|
9c41f635a9 | ||
|
|
40c020ad13 | ||
|
|
ec1fd20e59 | ||
|
|
0e938b1ff7 | ||
|
|
6efc688917 | ||
|
|
506711749f | ||
|
|
a17b371384 | ||
|
|
d260a42ca2 | ||
|
|
8fa3cc37f9 | ||
|
|
19946509a4 | ||
|
|
cd2967d271 | ||
|
|
86be66c34e | ||
|
|
8d1f763209 | ||
|
|
3c532314ec | ||
|
|
01f089d9fb | ||
|
|
5c88e57555 | ||
|
|
5ac87292c4 | ||
|
|
7ddb7a5cbb | ||
|
|
c51cf4efca | ||
|
|
5dbceaf5a4 | ||
|
|
8bf3994c2e | ||
|
|
114984a236 | ||
|
|
d72f897f07 | ||
|
|
c5f2da5875 | ||
|
|
1a26905cc9 | ||
|
|
33d62c2c66 | ||
|
|
5002efa31b | ||
|
|
286e90e58f | ||
|
|
0b179db36d | ||
|
|
7a025d6368 | ||
|
|
2c46bb6208 | ||
|
|
7e681ad778 | ||
|
|
3ddfc949dc | ||
|
|
24dfdb4a7d | ||
|
|
94eb2560f4 | ||
|
|
856f29c03c | ||
|
|
5de086b736 | ||
|
|
e6c62d5d7f | ||
|
|
deae7f4f5d | ||
|
|
f5973d8ddb | ||
|
|
661c711765 | ||
|
|
30da8c81c7 | ||
|
|
78b501eba6 | ||
|
|
2529f2bc01 | ||
|
|
fef3183461 | ||
|
|
ca3747fb2f | ||
|
|
53147e5ae4 | ||
|
|
93a8be7bef | ||
|
|
6f925f61ff | ||
|
|
657ab9ba9d | ||
|
|
fb93e14e53 | ||
|
|
937c175029 | ||
|
|
40c998336d | ||
|
|
24bd133d9d | ||
|
|
db3e1d73c6 | ||
|
|
76005c44f7 | ||
|
|
6d6a1c3454 | ||
|
|
5a5f37ca17 | ||
|
|
0fa05ea331 | ||
|
|
cbd5d55222 | ||
|
|
e4061383b8 | ||
|
|
e37de2aef3 | ||
|
|
7143f358f1 | ||
|
|
613e468b89 | ||
|
|
61933f8e52 | ||
|
|
d5033849a5 | ||
|
|
2a793a6c42 | ||
|
|
d253a35539 | ||
|
|
c75add6ec8 | ||
|
|
9fd445eb92 | ||
|
|
e543d6a91d | ||
|
|
60b0fca103 | ||
|
|
28a49a9eaf | ||
|
|
d05ff3e098 | ||
|
|
7dc0a28e17 | ||
|
|
de65c34fcf | ||
|
|
9a1638ed21 | ||
|
|
29aa13f0d4 | ||
|
|
10294b6082 | ||
|
|
9f863d3466 | ||
|
|
beaf4384d9 | ||
|
|
336987bb8d | ||
|
|
3dfa84bec8 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -10,10 +10,17 @@ docs/build/
|
||||
*.egg-info
|
||||
|
||||
cmdclient_config.json
|
||||
homeserver.db
|
||||
homeserver*.db
|
||||
|
||||
.coverage
|
||||
htmlcov
|
||||
|
||||
demo/*.db
|
||||
demo/*.log
|
||||
demo/*.pid
|
||||
|
||||
graph/*.svg
|
||||
graph/*.png
|
||||
graph/*.dot
|
||||
|
||||
uploads
|
||||
|
||||
85
CHANGES.rst
Normal file
85
CHANGES.rst
Normal file
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
=====================================
|
||||
|
||||
Homeserver:
|
||||
* Fix bug that caused the event stream to not notify some clients about
|
||||
changes.
|
||||
|
||||
Changes in synapse 0.1.0 (2014-08-29)
|
||||
=====================================
|
||||
Presence has been reenabled in this release.
|
||||
|
||||
Homeserver:
|
||||
* Update client to server API, including:
|
||||
- Use a more consistent url scheme.
|
||||
- Provide more useful information in the initial sync api.
|
||||
* Change the presence handling to be much more efficient.
|
||||
* Change the presence server to server API to not require explicit polling of
|
||||
all users who share a room with a user.
|
||||
* Fix races in the event streaming logic.
|
||||
|
||||
Webclient:
|
||||
* Update to use new client to server API.
|
||||
* Add basic VOIP support.
|
||||
* Add idle timers that change your status to away.
|
||||
* Add recent rooms column when viewing a room.
|
||||
* Various network efficiency improvements.
|
||||
* Add basic mobile browser support.
|
||||
* Add a settings page.
|
||||
|
||||
Changes in synapse 0.0.1 (2014-08-22)
|
||||
=====================================
|
||||
Presence has been disabled in this release due to a bug that caused the
|
||||
homeserver to spam other remote homeservers.
|
||||
|
||||
Homeserver:
|
||||
* Completely change the database schema to support generic event types.
|
||||
* Improve presence reliability.
|
||||
* Improve reliability of joining remote rooms.
|
||||
* Fix bug where room join events were duplicated.
|
||||
* Improve initial sync API to return more information to the client.
|
||||
* Stop generating fake messages for room membership events.
|
||||
|
||||
Webclient:
|
||||
* Add tab completion of names.
|
||||
* Add ability to upload and send images.
|
||||
* Add profile pages.
|
||||
* Improve CSS layout of room.
|
||||
* Disambiguate identical display names.
|
||||
* Don't get remote users display names and avatars individually.
|
||||
* Use the new initial sync API to reduce number of round trips to the homeserver.
|
||||
* Change url scheme to use room aliases instead of room ids where known.
|
||||
* Increase longpoll timeout.
|
||||
|
||||
Changes in synapse 0.0.0 (2014-08-13)
|
||||
=====================================
|
||||
|
||||
* Initial alpha release
|
||||
61
README.rst
61
README.rst
@@ -1,5 +1,5 @@
|
||||
Quick Start
|
||||
===========
|
||||
Introduction
|
||||
============
|
||||
|
||||
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:
|
||||
@@ -17,6 +17,9 @@ The overall architecture is::
|
||||
client <----> homeserver <=================> homeserver <-----> client
|
||||
e.g. matrix.org:8080 e.g. mydomain.net:8080
|
||||
|
||||
Quick Start
|
||||
===========
|
||||
|
||||
To get up and running:
|
||||
|
||||
- To simply play with an **existing** homeserver you can
|
||||
@@ -24,11 +27,8 @@ To get up and running:
|
||||
|
||||
- To run your own **private** homeserver on localhost:8080, install synapse
|
||||
with ``python setup.py develop --user`` and then run one with
|
||||
``python synapse/app/homeserver.py``
|
||||
|
||||
- To run your own webclient:
|
||||
``cd webclient; python -m SimpleHTTPServer`` and hit http://localhost:8000
|
||||
in your web browser (a recent Chrome, Safari or Firefox for now,
|
||||
``python synapse/app/homeserver.py`` - you will find a webclient running
|
||||
at http://localhost:8080 (use a recent Chrome, Safari or Firefox for now,
|
||||
please...)
|
||||
|
||||
- To make the homeserver **public** and let it exchange messages with
|
||||
@@ -36,7 +36,12 @@ To get up and running:
|
||||
up port 8080 and run ``python synapse/app/homeserver.py --host
|
||||
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
|
||||
say hi! :)
|
||||
|
||||
|
||||
For more detailed setup instructions, please see further down this document.
|
||||
|
||||
[1] VoIP currently in development
|
||||
|
||||
|
||||
About Matrix
|
||||
============
|
||||
|
||||
@@ -87,8 +92,6 @@ https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
|
||||
|
||||
Thanks for trying Matrix!
|
||||
|
||||
[1] VoIP currently in development
|
||||
|
||||
[2] Cryptographic signing of messages isn't turned on yet
|
||||
|
||||
[3] End-to-end encryption is currently in development
|
||||
@@ -120,6 +123,10 @@ may need to also run:
|
||||
$ sudo apt-get install python-pip
|
||||
$ sudo pip install --upgrade setuptools
|
||||
|
||||
If you don't have access to github, then you may need to install ``syutil``
|
||||
manually by checking it out and running ``python setup.py develop --user`` on it
|
||||
too.
|
||||
|
||||
If you get errors about ``sodium.h`` being missing, you may also need to
|
||||
manually install a newer PyNaCl via pip as setuptools installs an old one. Or
|
||||
you can check PyNaCl out of git directly (https://github.com/pyca/pynacl) and
|
||||
@@ -142,6 +149,13 @@ This should end with a 'PASSED' result::
|
||||
PASSED (successes=143)
|
||||
|
||||
|
||||
Upgrading an existing homeserver
|
||||
================================
|
||||
|
||||
Before upgrading an existing homeserver to a new version, please refer to
|
||||
UPGRADE.rst for any additional instructions.
|
||||
|
||||
|
||||
Setting up Federation
|
||||
=====================
|
||||
|
||||
@@ -162,7 +176,11 @@ IDs:
|
||||
For the first form, simply pass the required hostname (of the machine) as the
|
||||
--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
|
||||
|
||||
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
|
||||
@@ -175,7 +193,13 @@ record would then look something like::
|
||||
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::
|
||||
|
||||
$ 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
|
||||
increase the verbosity of logging output; at least for initial testing.
|
||||
@@ -189,22 +213,15 @@ Running a Demo Federation of Homeservers
|
||||
|
||||
If you want to get up and running quickly with a trio of homeservers in a
|
||||
private federation (``localhost:8080``, ``localhost:8081`` and
|
||||
``localhost:8082``) which you can then access through the webclient running at http://localhost:8080. Simply run::
|
||||
``localhost:8082``) which you can then access through the webclient running at
|
||||
http://localhost:8080. Simply run::
|
||||
|
||||
$ demo/start.sh
|
||||
|
||||
Running The Demo Web Client
|
||||
===========================
|
||||
|
||||
At the present time, the web client is not directly served by the homeserver's
|
||||
HTTP server. To serve this in a form the web browser can reach, arrange for the
|
||||
'webclient' sub-directory to be made available by any sort of HTTP server that
|
||||
can serve static files. For example, python's SimpleHTTPServer will suffice::
|
||||
|
||||
$ cd webclient
|
||||
$ python -m SimpleHTTPServer
|
||||
|
||||
You can now point your browser at http://localhost:8000/ to find the client.
|
||||
The homeserver runs a web client by default at http://localhost:8080.
|
||||
|
||||
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
|
||||
|
||||
55
UPGRADE.rst
Normal file
55
UPGRADE.rst
Normal file
@@ -0,0 +1,55 @@
|
||||
Upgrading to v0.2.0
|
||||
===================
|
||||
|
||||
To upgrade the database schema, run::
|
||||
|
||||
./database-prepare-for-0.2.0.sh "<database>.db"
|
||||
|
||||
|
||||
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
|
||||
===================
|
||||
|
||||
This release completely changes the database schema and so requires upgrading
|
||||
it before starting the new version of the homeserver.
|
||||
|
||||
The script "database-prepare-for-0.0.1.sh" should be used to upgrade the
|
||||
database. This will save all user information, such as logins and profiles,
|
||||
but will otherwise purge the database. This includes messages, which
|
||||
rooms the home server was a member of and room alias mappings.
|
||||
|
||||
Before running the command the homeserver should be first completely
|
||||
shutdown. To run it, simply specify the location of the database, e.g.:
|
||||
|
||||
./database-prepare-for-0.0.1.sh "homeserver.db"
|
||||
|
||||
Once this has successfully completed it will be safe to restart the
|
||||
homeserver. You may notice that the homeserver takes a few seconds longer to
|
||||
restart than usual as it reinitializes the database.
|
||||
|
||||
On startup of the new version, users can either rejoin remote rooms using room
|
||||
aliases or by being reinvited. Alternatively, if any other homeserver sends a
|
||||
message to a room that the homeserver was previously in the local HS will
|
||||
automatically rejoin the room.
|
||||
7
WISHLIST.rst
Normal file
7
WISHLIST.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
Broad-sweeping stuff which would be nice to have
|
||||
================================================
|
||||
|
||||
- Additional SQL backends beyond sqlite
|
||||
- homeserver implementation in go
|
||||
- homeserver implementation in node.js
|
||||
- client SDKs
|
||||
@@ -60,8 +60,8 @@ class SynapseCmd(cmd.Cmd):
|
||||
"complete_usernames": "on",
|
||||
"send_delivery_receipts": "on"
|
||||
}
|
||||
self.path_prefix = "/matrix/client/api/v1"
|
||||
self.event_stream_token = "START"
|
||||
self.path_prefix = "/_matrix/client/api/v1"
|
||||
self.event_stream_token = "END"
|
||||
self.prompt = ">>> "
|
||||
|
||||
def do_EOF(self, line): # allows CTRL+D quitting
|
||||
@@ -225,59 +225,76 @@ class SynapseCmd(cmd.Cmd):
|
||||
json_res = yield self.http_client.do_request("GET", url)
|
||||
print json_res
|
||||
|
||||
if ("type" not in json_res or "m.login.password" != json_res["type"] or
|
||||
"stages" in json_res):
|
||||
if "flows" not in json_res:
|
||||
print "Failed to find any login flows."
|
||||
defer.returnValue(False)
|
||||
|
||||
flow = json_res["flows"][0] # assume first is the one we want.
|
||||
if ("type" not in flow or "m.login.password" != flow["type"] or
|
||||
"stages" in flow):
|
||||
fallback_url = self._url() + "/login/fallback"
|
||||
print ("Unable to login via the command line client. Please visit "
|
||||
"%s to login." % fallback_url)
|
||||
defer.returnValue(False)
|
||||
defer.returnValue(True)
|
||||
|
||||
def do_3pidrequest(self, line):
|
||||
def do_emailrequest(self, line):
|
||||
"""Requests the association of a third party identifier
|
||||
<medium> The medium of the identifer (currently only 'email')
|
||||
<address> The address of the identifer (ie. the email address)
|
||||
<address> The email address)
|
||||
<clientSecret> A string of characters generated when requesting an email that you'll supply in subsequent calls to identify yourself
|
||||
<sendAttempt> The number of times the user has requested an email. Leave this the same between requests to retry the request at the transport level. Increment it to request that the email be sent again.
|
||||
"""
|
||||
args = self._parse(line, ['medium', 'address'])
|
||||
args = self._parse(line, ['address', 'clientSecret', 'sendAttempt'])
|
||||
|
||||
if not args['medium'] == 'email':
|
||||
print "Only email is supported currently"
|
||||
return
|
||||
postArgs = {'email': args['address'], 'clientSecret': args['clientSecret'], 'sendAttempt': args['sendAttempt']}
|
||||
|
||||
postArgs = {'email': args['address'], 'clientSecret': '____'}
|
||||
|
||||
reactor.callFromThread(self._do_3pidrequest, postArgs)
|
||||
reactor.callFromThread(self._do_emailrequest, postArgs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_3pidrequest(self, args):
|
||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken"
|
||||
def _do_emailrequest(self, args):
|
||||
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,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
print json_res
|
||||
if 'tokenId' in json_res:
|
||||
print "Token ID %s sent" % (json_res['tokenId'])
|
||||
if 'sid' in json_res:
|
||||
print "Token sent. Your session ID is %s" % (json_res['sid'])
|
||||
|
||||
def do_3pidvalidate(self, line):
|
||||
def do_emailvalidate(self, line):
|
||||
"""Validate and associate a third party ID
|
||||
<medium> The medium of the identifer (currently only 'email')
|
||||
<tokenId> The identifier iof the token given in 3pidrequest
|
||||
<sid> The session ID (sid) given to you in the response to requestToken
|
||||
<token> The token sent to your third party identifier address
|
||||
<clientSecret> The same clientSecret you supplied in requestToken
|
||||
"""
|
||||
args = self._parse(line, ['medium', 'tokenId', 'token'])
|
||||
args = self._parse(line, ['sid', 'token', 'clientSecret'])
|
||||
|
||||
if not args['medium'] == 'email':
|
||||
print "Only email is supported currently"
|
||||
return
|
||||
postArgs = { 'sid' : args['sid'], 'token' : args['token'], 'clientSecret': args['clientSecret'] }
|
||||
|
||||
postArgs = { 'tokenId' : args['tokenId'], 'token' : args['token'] }
|
||||
postArgs['mxId'] = self.config["user"]
|
||||
|
||||
reactor.callFromThread(self._do_3pidvalidate, postArgs)
|
||||
reactor.callFromThread(self._do_emailvalidate, postArgs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_3pidvalidate(self, args):
|
||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken"
|
||||
def _do_emailvalidate(self, args):
|
||||
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,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
print json_res
|
||||
|
||||
def do_3pidbind(self, line):
|
||||
"""Validate and associate a third party ID
|
||||
<sid> The session ID (sid) given to you in the response to requestToken
|
||||
<clientSecret> The same clientSecret you supplied in requestToken
|
||||
"""
|
||||
args = self._parse(line, ['sid', 'clientSecret'])
|
||||
|
||||
postArgs = { 'sid' : args['sid'], 'clientSecret': args['clientSecret'] }
|
||||
postArgs['mxid'] = self.config["user"]
|
||||
|
||||
reactor.callFromThread(self._do_3pidbind, postArgs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_3pidbind(self, args):
|
||||
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,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
@@ -343,14 +360,14 @@ class SynapseCmd(cmd.Cmd):
|
||||
def _do_invite(self, roomid, userstring):
|
||||
if (not userstring.startswith('@') and
|
||||
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})
|
||||
|
||||
mxid = None
|
||||
|
||||
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
|
||||
pubKeyObj = yield self.http_client.do_request("GET", url)
|
||||
@@ -390,19 +407,16 @@ class SynapseCmd(cmd.Cmd):
|
||||
"""Leaves a room: "leave <roomid>" """
|
||||
try:
|
||||
args = self._parse(line, ["roomid"], force_keys=True)
|
||||
path = ("/rooms/%s/members/%s/state" %
|
||||
(urllib.quote(args["roomid"]), self._usr()))
|
||||
reactor.callFromThread(self._run_and_pprint, "DELETE", path)
|
||||
self._do_membership_change(args["roomid"], "leave", self._usr())
|
||||
except Exception as e:
|
||||
print e
|
||||
|
||||
def do_send(self, line):
|
||||
"""Sends a message. "send <roomid> <body>" """
|
||||
args = self._parse(line, ["roomid", "body"])
|
||||
msg_id = "m%s" % int(time.time())
|
||||
path = "/rooms/%s/messages/%s/%s" % (urllib.quote(args["roomid"]),
|
||||
self._usr(),
|
||||
msg_id)
|
||||
txn_id = "txn%s" % int(time.time())
|
||||
path = "/rooms/%s/send/m.room.message/%s" % (urllib.quote(args["roomid"]),
|
||||
txn_id)
|
||||
body_json = {
|
||||
"msgtype": "m.text",
|
||||
"body": args["body"]
|
||||
@@ -426,7 +440,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
print "Unrecognised type: %s" % args["type"]
|
||||
return
|
||||
room_id = args["roomid"]
|
||||
path = "/rooms/%s/%s/list" % (urllib.quote(room_id), args["type"])
|
||||
path = "/rooms/%s/%s" % (urllib.quote(room_id), args["type"])
|
||||
|
||||
qp = {"access_token": self._tok()}
|
||||
if "qp" in args:
|
||||
@@ -462,7 +476,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
room_name = args["vis"]
|
||||
body["room_alias_name"] = room_name
|
||||
|
||||
reactor.callFromThread(self._run_and_pprint, "POST", "/rooms", body)
|
||||
reactor.callFromThread(self._run_and_pprint, "POST", "/createRoom", body)
|
||||
|
||||
def do_raw(self, line):
|
||||
"""Directly send a JSON object: "raw <method> <path> <data> <notoken>"
|
||||
@@ -555,7 +569,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
alt_text="Sent receipt for %s" % event["msg_id"])
|
||||
|
||||
def _do_membership_change(self, roomid, membership, userid):
|
||||
path = "/rooms/%s/members/%s/state" % (urllib.quote(roomid), userid)
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (urllib.quote(roomid), urllib.quote(userid))
|
||||
data = {
|
||||
"membership": membership
|
||||
}
|
||||
|
||||
21
database-prepare-for-0.0.1.sh
Executable file
21
database-prepare-for-0.0.1.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This is will prepare a synapse database for running with v0.0.1 of synapse.
|
||||
# It will store all the user information, but will *delete* all messages and
|
||||
# room data.
|
||||
|
||||
set -e
|
||||
|
||||
cp "$1" "$1.bak"
|
||||
|
||||
DUMP=$(sqlite3 "$1" << 'EOF'
|
||||
.dump users
|
||||
.dump access_tokens
|
||||
.dump presence
|
||||
.dump profiles
|
||||
EOF
|
||||
)
|
||||
|
||||
rm "$1"
|
||||
|
||||
sqlite3 "$1" <<< "$DUMP"
|
||||
10
database-prepare-for-0.2.0.sh
Executable file
10
database-prepare-for-0.2.0.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This is will prepare a synapse database for running with v0.2.0 of synapse.
|
||||
|
||||
set -e
|
||||
|
||||
cp "$1" "$1.bak"
|
||||
|
||||
sqlite3 "$1" < "synapse/storage/schema/im.sql"
|
||||
sqlite3 "$1" <<< "PRAGMA user_version = 2;"
|
||||
@@ -8,7 +8,7 @@
|
||||
#
|
||||
# $ sqlite3 homeserver.db < table-save.sql
|
||||
|
||||
sqlite3 homeserver.db <<'EOF' >table-save.sql
|
||||
sqlite3 "$1" <<'EOF' >table-save.sql
|
||||
.dump users
|
||||
.dump access_tokens
|
||||
.dump presence
|
||||
|
||||
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,16 +6,29 @@ CWD=$(pwd)
|
||||
|
||||
cd "$DIR/.."
|
||||
|
||||
for port in "8080" "8081" "8082"; do
|
||||
mkdir -p demo/etc
|
||||
|
||||
for port in 8080 8081 8082; do
|
||||
echo "Starting server on port $port... "
|
||||
|
||||
https_port=$((port + 400))
|
||||
|
||||
python -m synapse.app.homeserver \
|
||||
-p "$port" \
|
||||
-H "localhost:$port" \
|
||||
--generate-config \
|
||||
--config-path "demo/etc/$port.config" \
|
||||
-p "$https_port" \
|
||||
--unsecure-port "$port" \
|
||||
-H "localhost:$https_port" \
|
||||
-f "$DIR/$port.log" \
|
||||
-d "$DIR/$port.db" \
|
||||
-D --pid-file "$DIR/$port.pid" \
|
||||
--manhole $((port + 1000)) \
|
||||
--tls-dh-params-path "demo/demo.tls.dh"
|
||||
|
||||
python -m synapse.app.homeserver \
|
||||
--config-path "demo/etc/$port.config" \
|
||||
-vv \
|
||||
-D --pid-file "$DIR/$port.pid"
|
||||
|
||||
done
|
||||
|
||||
echo "Starting webclient on port 8000..."
|
||||
|
||||
@@ -2,9 +2,32 @@ import argparse
|
||||
import BaseHTTPServer
|
||||
import os
|
||||
import SimpleHTTPServer
|
||||
import cgi, logging
|
||||
|
||||
from daemonize import Daemonize
|
||||
|
||||
class SimpleHTTPRequestHandlerWithPOST(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
UPLOAD_PATH = "upload"
|
||||
|
||||
"""
|
||||
Accept all post request as file upload
|
||||
"""
|
||||
def do_POST(self):
|
||||
|
||||
path = os.path.join(self.UPLOAD_PATH, os.path.basename(self.path))
|
||||
length = self.headers['content-length']
|
||||
data = self.rfile.read(int(length))
|
||||
|
||||
with open(path, 'wb') as fh:
|
||||
fh.write(data)
|
||||
|
||||
self.send_response(200)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
|
||||
# Return the absolute path of the uploaded file
|
||||
self.wfile.write('{"url":"/%s"}' % path)
|
||||
|
||||
|
||||
def setup():
|
||||
parser = argparse.ArgumentParser()
|
||||
@@ -19,7 +42,7 @@ def setup():
|
||||
|
||||
httpd = BaseHTTPServer.HTTPServer(
|
||||
('', args.port),
|
||||
SimpleHTTPServer.SimpleHTTPRequestHandler
|
||||
SimpleHTTPRequestHandlerWithPOST
|
||||
)
|
||||
|
||||
def run():
|
||||
|
||||
303
docs/client-server/howto.rst
Normal file
303
docs/client-server/howto.rst
Normal file
@@ -0,0 +1,303 @@
|
||||
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,6 +1,6 @@
|
||||
=========================
|
||||
Synapse Client-Server API
|
||||
=========================
|
||||
========================
|
||||
Matrix Client-Server API
|
||||
========================
|
||||
|
||||
The following specification outlines how a client can send and receive data from
|
||||
a home server.
|
||||
@@ -262,7 +262,10 @@ the error, but the keys 'error' and 'errcode' will always be present.
|
||||
Some standard error codes are below:
|
||||
|
||||
M_FORBIDDEN:
|
||||
Forbidden access, e.g. bad access token, failed login.
|
||||
Forbidden access, e.g. joining a room without permission, failed login.
|
||||
|
||||
M_UNKNOWN_TOKEN:
|
||||
The access token specified was not recognised.
|
||||
|
||||
M_BAD_JSON:
|
||||
Request contained valid JSON, but it was malformed in some way, e.g. missing
|
||||
@@ -303,11 +306,11 @@ POST requests MUST be submitted as application/json.
|
||||
All paths MUST be namespaced by the version of the API being used. This should
|
||||
be:
|
||||
|
||||
/matrix/client/api/v1
|
||||
/_matrix/client/api/v1
|
||||
|
||||
All REST paths in this section MUST be prefixed with this. E.g.
|
||||
REST Path: /rooms/$room_id
|
||||
Absolute Path: /matrix/client/api/v1/rooms/$room_id
|
||||
Absolute Path: /_matrix/client/api/v1/rooms/$room_id
|
||||
|
||||
Registration
|
||||
============
|
||||
@@ -411,6 +414,9 @@ The server checks this, finds it is valid, and returns:
|
||||
{
|
||||
"access_token": "abcdef0123456789"
|
||||
}
|
||||
The server may optionally return "user_id" to confirm or change the user's ID.
|
||||
This is particularly useful if the home server wishes to support localpart entry
|
||||
of usernames (e.g. "bob" rather than "@bob:matrix.org").
|
||||
|
||||
OAuth2-based
|
||||
------------
|
||||
@@ -688,6 +694,16 @@ Invite/Joining/Leaving a room
|
||||
Required keys:
|
||||
membership : [join|invite] - The membership state of $user_id in room
|
||||
$room_id.
|
||||
Optional keys:
|
||||
displayname,
|
||||
avatar_url : String fields from the member user's profile
|
||||
state,
|
||||
status_msg,
|
||||
mtime_age : Presence information
|
||||
|
||||
These optional keys provide extra information that the client is likely to
|
||||
be interested in so it doesn't have to perform an additional profile or
|
||||
presence information fetch.
|
||||
|
||||
Where:
|
||||
join - Indicate you ($user_id) are joining the room $room_id.
|
||||
@@ -991,26 +1007,15 @@ for users from other servers entirely.
|
||||
Presence
|
||||
========
|
||||
|
||||
In the following messages, the presence state is an integer enumeration of the
|
||||
following states:
|
||||
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.
|
||||
In the following messages, the presence state is a presence string as described in
|
||||
the main specification document.
|
||||
|
||||
Getting/Setting your own presence state
|
||||
---------------------------------------
|
||||
REST Path: /presence/$user_id/status
|
||||
Valid methods: GET/PUT
|
||||
Required keys:
|
||||
state : [0|1|2|3] - The user's new presence state
|
||||
presence : <string> - The user's new presence state
|
||||
Optional keys:
|
||||
status_msg : text string provided by the user to explain their status
|
||||
|
||||
@@ -1023,7 +1028,7 @@ Fetching your presence list
|
||||
following keys:
|
||||
{
|
||||
"user_id" : string giving the observed user's ID
|
||||
"state" : int giving their status
|
||||
"presence" : int giving their status
|
||||
"status_msg" : optional text string
|
||||
"displayname" : optional text string from the user's profile
|
||||
"avatar_url" : optional text string from the user's profile
|
||||
|
||||
46
docs/client-server/swagger_matrix/api-docs
Normal file
46
docs/client-server/swagger_matrix/api-docs
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
83
docs/client-server/swagger_matrix/api-docs-directory
Normal file
83
docs/client-server/swagger_matrix/api-docs-directory
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
247
docs/client-server/swagger_matrix/api-docs-events
Normal file
247
docs/client-server/swagger_matrix/api-docs-events
Normal file
@@ -0,0 +1,247 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
docs/client-server/swagger_matrix/api-docs-login
Normal file
102
docs/client-server/swagger_matrix/api-docs-login
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
|
||||
164
docs/client-server/swagger_matrix/api-docs-presence
Normal file
164
docs/client-server/swagger_matrix/api-docs-presence
Normal file
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
docs/client-server/swagger_matrix/api-docs-profile
Normal file
122
docs/client-server/swagger_matrix/api-docs-profile
Normal file
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
docs/client-server/swagger_matrix/api-docs-registration
Normal file
75
docs/client-server/swagger_matrix/api-docs-registration
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
|
||||
875
docs/client-server/swagger_matrix/api-docs-rooms
Normal file
875
docs/client-server/swagger_matrix/api-docs-rooms
Normal file
@@ -0,0 +1,875 @@
|
||||
{
|
||||
"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}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Send a generic non-state event to this room.",
|
||||
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Send a message in this room.",
|
||||
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Send feedback to a message.",
|
||||
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad feedback type."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/invite",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Invite a user to this room.",
|
||||
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||
"type": "void",
|
||||
"nickname": "invite",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The user to invite.",
|
||||
"required": true,
|
||||
"type": "InviteRequest",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/join",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Join this room.",
|
||||
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||
"type": "void",
|
||||
"nickname": "join_room",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to join.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/leave",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Leave this room.",
|
||||
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||
"type": "void",
|
||||
"nickname": "leave",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to leave.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/ban",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Ban a user in the room.",
|
||||
"notes": "This operation can also be done as a PUT by suffixing /{txnId}. The caller must have the required power level to do this operation.",
|
||||
"type": "void",
|
||||
"nickname": "ban",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has the user to ban.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The user to ban.",
|
||||
"required": true,
|
||||
"type": "BanRequest",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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. This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||
"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
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Sets the name of the room. Send a m.room.name event after creating the room with the 'name' key specified.",
|
||||
"required": false
|
||||
},
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"description": "Sets the topic for the room. Send a m.room.topic event after creating the room with the 'topic' key specified.",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
},
|
||||
"BanRequest": {
|
||||
"id": "BanRequest",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "The fully-qualified user ID."
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "The reason for the ban."
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitialSyncRoomData": {
|
||||
"id": "InitialSyncRoomData",
|
||||
"properties": {
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"description": "This user's membership state in this room.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The ID of this room.",
|
||||
"required": true
|
||||
},
|
||||
"messages": {
|
||||
"type": "MessagePaginationChunk",
|
||||
"description": "The most recent messages for this room, governed by the limit parameter.",
|
||||
"required": false
|
||||
},
|
||||
"state": {
|
||||
"type": "array",
|
||||
"description": "A list of state events representing the current state of the room.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
=========================
|
||||
Client-Server URL Summary
|
||||
=========================
|
||||
|
||||
A brief overview of the URL scheme involved in the Synapse Client-Server API.
|
||||
|
||||
|
||||
URLs
|
||||
====
|
||||
|
||||
Fetch events:
|
||||
GET /events
|
||||
|
||||
Registering an account
|
||||
POST /register
|
||||
|
||||
Unregistering an account
|
||||
POST /unregister
|
||||
|
||||
Rooms
|
||||
-----
|
||||
|
||||
Creating a room by ID
|
||||
PUT /rooms/$roomid
|
||||
|
||||
Creating an anonymous room
|
||||
POST /rooms
|
||||
|
||||
Room topic
|
||||
GET /rooms/$roomid/topic
|
||||
PUT /rooms/$roomid/topic
|
||||
|
||||
List rooms
|
||||
GET /rooms/list
|
||||
|
||||
Invite/Join/Leave
|
||||
GET /rooms/$roomid/members/$userid/state
|
||||
PUT /rooms/$roomid/members/$userid/state
|
||||
DELETE /rooms/$roomid/members/$userid/state
|
||||
|
||||
List members
|
||||
GET /rooms/$roomid/members/list
|
||||
|
||||
Sending/reading messages
|
||||
PUT /rooms/$roomid/messages/$sender/$msgid
|
||||
|
||||
Feedback
|
||||
GET /rooms/$roomid/messages/$sender/$msgid/feedback/$feedbackuser/$feedback
|
||||
PUT /rooms/$roomid/messages/$sender/$msgid/feedback/$feedbackuser/$feedback
|
||||
|
||||
Paginating messages
|
||||
GET /rooms/$roomid/messages/list
|
||||
|
||||
Profiles
|
||||
--------
|
||||
|
||||
Display name
|
||||
GET /profile/$userid/displayname
|
||||
PUT /profile/$userid/displayname
|
||||
|
||||
Avatar URL
|
||||
GET /profile/$userid/avatar_url
|
||||
PUT /profile/$userid/avatar_url
|
||||
|
||||
Metadata
|
||||
GET /profile/$userid/metadata
|
||||
POST /profile/$userid/metadata
|
||||
|
||||
Presence
|
||||
--------
|
||||
|
||||
My state or status message
|
||||
GET /presence/$userid/status
|
||||
PUT /presence/$userid/status
|
||||
also 'GET' for fetching others
|
||||
|
||||
TODO(paul): per-device idle time, device type; similar to above
|
||||
|
||||
My presence list
|
||||
GET /presence_list/$myuserid
|
||||
POST /presence_list/$myuserid
|
||||
body is JSON-encoded dict of keys:
|
||||
invite: list of UserID strings to invite
|
||||
drop: list of UserID strings to remove
|
||||
TODO(paul): define other ops: accept, group management, ordering?
|
||||
|
||||
Presence polling start/stop
|
||||
POST /presence_list/$myuserid?op=start
|
||||
POST /presence_list/$myuserid?op=stop
|
||||
|
||||
Presence invite
|
||||
POST /presence_list/$myuserid/invite/$targetuserid
|
||||
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,8 +1,8 @@
|
||||
============================
|
||||
Synapse Server-to-Server API
|
||||
============================
|
||||
===========================
|
||||
Matrix Server-to-Server API
|
||||
===========================
|
||||
|
||||
A description of the protocol used to communicate between Synapse home servers;
|
||||
A description of the protocol used to communicate between Matrix home servers;
|
||||
also known as Federation.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ Overview
|
||||
========
|
||||
|
||||
The server-server API is a mechanism by which two home servers can exchange
|
||||
Synapse event messages, both as a real-time push of current events, and as a
|
||||
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
|
||||
@@ -19,7 +19,7 @@ historic data for the purpose of back-filling scrollback buffers and the like
|
||||
can also be performed.
|
||||
|
||||
|
||||
{ Synapse entities } { Synapse entities }
|
||||
{ Matrix clients } { Matrix clients }
|
||||
^ | ^ |
|
||||
| events | | events |
|
||||
| V | V
|
||||
@@ -29,27 +29,53 @@ can also be performed.
|
||||
| |<--------( HTTP )-----------| |
|
||||
+------------------+ +------------------+
|
||||
|
||||
There are three main kinds of communication that occur between home servers:
|
||||
|
||||
Transactions and PDUs
|
||||
=====================
|
||||
* Queries
|
||||
These are single request/response interactions between a given pair of
|
||||
servers, initiated by one side sending an HTTP request to obtain some
|
||||
information, and responded by the other. They are not persisted and contain
|
||||
no long-term significant history. They simply request a snapshot state at the
|
||||
instant the query is made.
|
||||
|
||||
The communication between home servers is performed by a bidirectional exchange
|
||||
of messages. These messages are called Transactions, and are encoded as JSON
|
||||
objects with a dict as the top-level element, passed over HTTP. A Transaction is
|
||||
meaningful only to the pair of home servers that exchanged it; they are not
|
||||
globally-meaningful.
|
||||
* EDUs - Ephemeral Data Units
|
||||
These are notifications of events that are pushed from one home server to
|
||||
another. They are not persisted and contain no long-term significant history,
|
||||
nor does the receiving home server have to reply to them.
|
||||
|
||||
Each transaction has an opaque ID and timestamp (UNIX epoch time in miliseconds)
|
||||
generated by its origin server, an origin and destination server name, a list of
|
||||
"previous IDs", and a list of PDUs - the actual message payload that the
|
||||
Transaction carries.
|
||||
* PDUs - Persisted Data Units
|
||||
These are notifications of events that are broadcast from one home server to
|
||||
any others that are interested in the same "context" (namely, a Room ID).
|
||||
They are persisted to long-term storage and form the record of history for
|
||||
that context.
|
||||
|
||||
Where Queries are presented directly across the HTTP connection as GET requests
|
||||
to specific URLs, EDUs and PDUs are further wrapped in an envelope called a
|
||||
Transaction, which is transferred from the origin to the destination home server
|
||||
using a PUT request.
|
||||
|
||||
|
||||
Transactions and EDUs/PDUs
|
||||
==========================
|
||||
|
||||
The transfer of EDUs and PDUs between home servers is performed by an exchange
|
||||
of Transaction messages, which are encoded as JSON objects with a dict as the
|
||||
top-level element, passed over an HTTP PUT request. A Transaction is meaningful
|
||||
only to the pair of home servers that exchanged it; they are not globally-
|
||||
meaningful.
|
||||
|
||||
Each transaction has an opaque ID and timestamp (UNIX epoch time in
|
||||
milliseconds) generated by its origin server, an origin and destination server
|
||||
name, a list of "previous IDs", and a list of PDUs - the actual message payload
|
||||
that the Transaction carries.
|
||||
|
||||
{"transaction_id":"916d630ea616342b42e98a3be0b74113",
|
||||
"ts":1404835423000,
|
||||
"origin":"red",
|
||||
"destination":"blue",
|
||||
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
|
||||
"pdus":[...]}
|
||||
"pdus":[...],
|
||||
"edus":[...]}
|
||||
|
||||
The "previous IDs" field will contain a list of previous transaction IDs that
|
||||
the origin server has sent to this destination. Its purpose is to act as a
|
||||
@@ -58,7 +84,9 @@ successfully received that Transaction, or ask for a retransmission if not.
|
||||
|
||||
The "pdus" field of a transaction is a list, containing zero or more PDUs.[*]
|
||||
Each PDU is itself a dict containing a number of keys, the exact details of
|
||||
which will vary depending on the type of PDU.
|
||||
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
|
||||
@@ -86,7 +114,7 @@ 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 Synapse conversation, it
|
||||
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
|
||||
@@ -112,13 +140,22 @@ so on. This part needs refining. And writing in its own document as the details
|
||||
relate to the server/system as a whole, not specifically to server-server
|
||||
federation.]]
|
||||
|
||||
EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
|
||||
"previous" IDs. The only mandatory fields for these are the type, origin and
|
||||
destination home server names, and the actual nested content.
|
||||
|
||||
{"edu_type":"m.presence",
|
||||
"origin":"blue",
|
||||
"destination":"orange",
|
||||
"content":...}
|
||||
|
||||
|
||||
Protocol URLs
|
||||
=============
|
||||
|
||||
All these URLs are namespaced within a prefix of
|
||||
|
||||
/matrix/federation/v1/...
|
||||
/_matrix/federation/v1/...
|
||||
|
||||
For active pushing of messages representing live activity "as it happens":
|
||||
|
||||
@@ -179,3 +216,16 @@ To stream events all the events:
|
||||
Retrieves all of the transactions later than any version given by the "v"
|
||||
arguments. [[TODO(paul): I'm not sure what the "origin" argument does because
|
||||
I think at some point in the code it's got swapped around.]]
|
||||
|
||||
|
||||
To make a query:
|
||||
|
||||
GET .../query/:query_type
|
||||
Query args: as specified by the individual query types
|
||||
|
||||
Response: JSON encoding of a response object
|
||||
|
||||
Performs a single query request on the receiving home server. The Query Type
|
||||
part of the path specifies the kind of query being made, and its query
|
||||
arguments have a meaning specific to that kind of query. The response is a
|
||||
JSON-encoded object whose meaning also depends on the kind of query.
|
||||
|
||||
1387
docs/specification.rst
Normal file
1387
docs/specification.rst
Normal file
File diff suppressed because it is too large
Load Diff
@@ -113,14 +113,14 @@ def make_graph(pdus, room, filename_prefix):
|
||||
graph.add_edge(state_edge)
|
||||
|
||||
graph.write('%s.dot' % filename_prefix, format='raw', prog='dot')
|
||||
graph.write_png("%s.png" % filename_prefix, prog='dot')
|
||||
# graph.write_png("%s.png" % filename_prefix, prog='dot')
|
||||
graph.write_svg("%s.svg" % filename_prefix, prog='dot')
|
||||
|
||||
|
||||
def get_pdus(host, room):
|
||||
transaction = json.loads(
|
||||
urllib2.urlopen(
|
||||
"http://%s/matrix/federation/v1/context/%s/" % (host, room)
|
||||
"http://%s/_matrix/federation/v1/context/%s/" % (host, room)
|
||||
).read()
|
||||
)
|
||||
|
||||
|
||||
17
jsfiddles/create_room_send_msg/demo.css
Normal file
17
jsfiddles/create_room_send_msg/demo.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.loggedin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing:5px;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
padding:5px;
|
||||
}
|
||||
30
jsfiddles/create_room_send_msg/demo.html
Normal file
30
jsfiddles/create_room_send_msg/demo.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<div>
|
||||
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8080</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
<div class="loggedin">
|
||||
<form class="createRoomForm">
|
||||
<input type="text" id="roomAlias" placeholder="Room alias (optional)"></input>
|
||||
<input type="button" class="createRoom" value="Create Room"></input>
|
||||
</form>
|
||||
<form class="sendMessageForm">
|
||||
<input type="text" id="roomId" placeholder="Room ID"></input>
|
||||
<input type="text" id="messageBody" placeholder="Message body"></input>
|
||||
<input type="button" class="sendMessage" value="Send Message"></input>
|
||||
</form>
|
||||
<table id="rooms">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Room ID</th>
|
||||
<th>My state</th>
|
||||
<th>Room Alias</th>
|
||||
<th>Latest message</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
108
jsfiddles/create_room_send_msg/demo.js
Normal file
108
jsfiddles/create_room_send_msg/demo.js
Normal file
@@ -0,0 +1,108 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var showLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
getCurrentRoomList();
|
||||
$(".loggedin").css({visibility: "visible"});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_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) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getCurrentRoomList = function() {
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
addRoom(rooms[i]);
|
||||
}
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
};
|
||||
|
||||
$('.createRoom').live('click', function() {
|
||||
var roomAlias = $("#roomAlias").val();
|
||||
var data = {};
|
||||
if (roomAlias.length > 0) {
|
||||
data.room_alias_name = roomAlias;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
data.membership = "join"; // you are automatically joined into every room you make.
|
||||
data.latest_message = "";
|
||||
addRoom(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var addRoom = function(data) {
|
||||
row = "<tr>" +
|
||||
"<td>"+data.room_id+"</td>" +
|
||||
"<td>"+data.membership+"</td>" +
|
||||
"<td>"+data.room_alias+"</td>" +
|
||||
"<td>"+data.latest_message+"</td>" +
|
||||
"</tr>";
|
||||
$("#rooms").append(row);
|
||||
};
|
||||
|
||||
$('.sendMessage').live('click', function() {
|
||||
var roomId = $("#roomId").val();
|
||||
var body = $("#messageBody").val();
|
||||
var msgId = $.now();
|
||||
|
||||
if (roomId.length === 0 || body.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
body: body
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
$("#messageBody").val("");
|
||||
// wipe the table and reload it. Using the event stream would be the best
|
||||
// solution but that is out of scope of this fiddle.
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
17
jsfiddles/event_stream/demo.css
Normal file
17
jsfiddles/event_stream/demo.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.loggedin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing:5px;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
padding:5px;
|
||||
}
|
||||
23
jsfiddles/event_stream/demo.html
Normal file
23
jsfiddles/event_stream/demo.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<div>
|
||||
<p>This event stream demo requires a home server to be running on http://localhost:8080</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
<div class="loggedin">
|
||||
<form class="sendMessageForm">
|
||||
<input type="button" class="sendMessage" value="Send random message"></input>
|
||||
</form>
|
||||
<p id="streamErrorText"></p>
|
||||
<table id="rooms">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Room ID</th>
|
||||
<th>Latest message</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
140
jsfiddles/event_stream/demo.js
Normal file
140
jsfiddles/event_stream/demo.js
Normal file
@@ -0,0 +1,140 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var eventStreamInfo = {
|
||||
from: "END"
|
||||
};
|
||||
|
||||
var roomInfo = [];
|
||||
|
||||
var longpollEventStream = function() {
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$from", eventStreamInfo.from);
|
||||
|
||||
$.getJSON(url, function(data) {
|
||||
eventStreamInfo.from = data.end;
|
||||
|
||||
var hasNewLatestMessage = false;
|
||||
for (var i=0; i<data.chunk.length; ++i) {
|
||||
if (data.chunk[i].type === "m.room.message") {
|
||||
for (var j=0; j<roomInfo.length; ++j) {
|
||||
if (roomInfo[j].room_id === data.chunk[i].room_id) {
|
||||
roomInfo[j].latest_message = data.chunk[i].content.body;
|
||||
hasNewLatestMessage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewLatestMessage) {
|
||||
setRooms(roomInfo);
|
||||
}
|
||||
$("#streamErrorText").text("");
|
||||
longpollEventStream();
|
||||
}).fail(function(err) {
|
||||
$("#streamErrorText").text("Event stream error: "+JSON.stringify($.parseJSON(err.responseText)));
|
||||
setTimeout(longpollEventStream, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
var showLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
longpollEventStream();
|
||||
getCurrentRoomList();
|
||||
$(".loggedin").css({visibility: "visible"});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_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) {
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getCurrentRoomList = function() {
|
||||
$("#roomId").val("");
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
if ("messages" in rooms[i]) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
}
|
||||
}
|
||||
roomInfo = rooms;
|
||||
setRooms(roomInfo);
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
};
|
||||
|
||||
$('.sendMessage').live('click', function() {
|
||||
if (roomInfo.length === 0) {
|
||||
alert("There is no room to send a message to!");
|
||||
return;
|
||||
}
|
||||
|
||||
var index = Math.floor(Math.random() * roomInfo.length);
|
||||
|
||||
sendMessage(roomInfo[index].room_id);
|
||||
});
|
||||
|
||||
var sendMessage = function(roomId) {
|
||||
var body = "jsfiddle message @" + $.now();
|
||||
|
||||
if (roomId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
body: body
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
$("#messageBody").val("");
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var setRooms = function(roomList) {
|
||||
// wipe existing entries
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
|
||||
var rows = "";
|
||||
for (var i=0; i<roomList.length; ++i) {
|
||||
row = "<tr>" +
|
||||
"<td>"+roomList[i].room_id+"</td>" +
|
||||
"<td>"+roomList[i].latest_message+"</td>" +
|
||||
"</tr>";
|
||||
rows += row;
|
||||
}
|
||||
|
||||
$("#rooms").append(rows);
|
||||
};
|
||||
|
||||
43
jsfiddles/example_app/demo.css
Normal file
43
jsfiddles/example_app/demo.css
Normal file
@@ -0,0 +1,43 @@
|
||||
.roomListDashboard, .roomContents, .sendMessageForm {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.roomList {
|
||||
background-color: #909090;
|
||||
}
|
||||
|
||||
.messageWrapper {
|
||||
background-color: #EEEEEE;
|
||||
height: 400px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.membersWrapper {
|
||||
background-color: #EEEEEE;
|
||||
height: 200px;
|
||||
width: 50%;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.textEntry {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing:5px;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
padding:5px;
|
||||
}
|
||||
|
||||
.roomList tr:not(:first-child):hover {
|
||||
background-color: orange;
|
||||
cursor: pointer;
|
||||
}
|
||||
56
jsfiddles/example_app/demo.html
Normal file
56
jsfiddles/example_app/demo.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<div class="signUp">
|
||||
<p>Matrix example application: Requires a local home server running at http://localhost:8080</p>
|
||||
<form class="registrationForm">
|
||||
<p>No account? Register:</p>
|
||||
<input type="text" id="userReg" placeholder="Username"></input>
|
||||
<input type="password" id="passwordReg" placeholder="Password"></input>
|
||||
<input type="button" class="register" value="Register"></input>
|
||||
</form>
|
||||
<form class="loginForm">
|
||||
<p>Got an account? Login:</p>
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="roomListDashboard">
|
||||
<form class="createRoomForm">
|
||||
<input type="text" id="roomAlias" placeholder="Room alias"></input>
|
||||
<input type="button" class="createRoom" value="Create Room"></input>
|
||||
</form>
|
||||
<table id="rooms" class="roomList">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Room</th>
|
||||
<th>My state</th>
|
||||
<th>Latest message</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="roomContents">
|
||||
<p id="roomName">Select a room</p>
|
||||
<div class="messageWrapper">
|
||||
<table id="messages">
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<form class="sendMessageForm">
|
||||
<input type="text" class="textEntry" id="body" placeholder="Enter text here..." onkeydown="javascript:if (event.keyCode == 13) document.getElementById('sendMsg').focus()"></input>
|
||||
<input type="button" class="sendMessage" id="sendMsg" value="Send"></input>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Member list:</p>
|
||||
<div class="membersWrapper">
|
||||
<table id="members">
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
321
jsfiddles/example_app/demo.js
Normal file
321
jsfiddles/example_app/demo.js
Normal file
@@ -0,0 +1,321 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var eventStreamInfo = {
|
||||
from: "END"
|
||||
};
|
||||
|
||||
var roomInfo = [];
|
||||
var memberInfo = [];
|
||||
var viewingRoomId;
|
||||
|
||||
// ************** Event Streaming **************
|
||||
var longpollEventStream = function() {
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$from", eventStreamInfo.from);
|
||||
|
||||
$.getJSON(url, function(data) {
|
||||
eventStreamInfo.from = data.end;
|
||||
|
||||
var hasNewLatestMessage = false;
|
||||
var updatedMemberList = false;
|
||||
var i=0;
|
||||
var j=0;
|
||||
for (i=0; i<data.chunk.length; ++i) {
|
||||
if (data.chunk[i].type === "m.room.message") {
|
||||
console.log("Got new message: " + JSON.stringify(data.chunk[i]));
|
||||
if (viewingRoomId === data.chunk[i].room_id) {
|
||||
addMessage(data.chunk[i]);
|
||||
}
|
||||
|
||||
for (j=0; j<roomInfo.length; ++j) {
|
||||
if (roomInfo[j].room_id === data.chunk[i].room_id) {
|
||||
roomInfo[j].latest_message = data.chunk[i].content.body;
|
||||
hasNewLatestMessage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (data.chunk[i].type === "m.room.member") {
|
||||
if (viewingRoomId === data.chunk[i].room_id) {
|
||||
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
|
||||
addMessage(data.chunk[i]);
|
||||
for (j=0; j<memberInfo.length; ++j) {
|
||||
if (memberInfo[j].state_key === data.chunk[i].state_key) {
|
||||
memberInfo[j] = data.chunk[i];
|
||||
updatedMemberList = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!updatedMemberList) {
|
||||
memberInfo.push(data.chunk[i]);
|
||||
updatedMemberList = true;
|
||||
}
|
||||
}
|
||||
if (data.chunk[i].state_key === accountInfo.user_id) {
|
||||
getCurrentRoomList(); // update our join/invite list
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log("Discarding: " + JSON.stringify(data.chunk[i]));
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNewLatestMessage) {
|
||||
setRooms(roomInfo);
|
||||
}
|
||||
if (updatedMemberList) {
|
||||
$("#members").empty();
|
||||
for (i=0; i<memberInfo.length; ++i) {
|
||||
addMember(memberInfo[i]);
|
||||
}
|
||||
}
|
||||
longpollEventStream();
|
||||
}).fail(function(err) {
|
||||
setTimeout(longpollEventStream, 5000);
|
||||
});
|
||||
};
|
||||
|
||||
// ************** Registration and Login **************
|
||||
var onLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
longpollEventStream();
|
||||
getCurrentRoomList();
|
||||
$(".roomListDashboard").css({visibility: "visible"});
|
||||
$(".roomContents").css({visibility: "visible"});
|
||||
$(".signUp").css({display: "none"});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_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) {
|
||||
onLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert("Unable to login: is the home server running?");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.register').live('click', function() {
|
||||
var user = $("#userReg").val();
|
||||
var password = $("#passwordReg").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_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) {
|
||||
onLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
var msg = "Is the home server running?";
|
||||
var errJson = $.parseJSON(err.responseText);
|
||||
if (errJson !== null) {
|
||||
msg = errJson.error;
|
||||
}
|
||||
alert("Unable to register: "+msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ************** Creating a room ******************
|
||||
$('.createRoom').live('click', function() {
|
||||
var roomAlias = $("#roomAlias").val();
|
||||
var data = {};
|
||||
if (roomAlias.length > 0) {
|
||||
data.room_alias_name = roomAlias;
|
||||
}
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(response) {
|
||||
$("#roomAlias").val("");
|
||||
response.membership = "join"; // you are automatically joined into every room you make.
|
||||
response.latest_message = "";
|
||||
|
||||
roomInfo.push(response);
|
||||
setRooms(roomInfo);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ************** Getting current state **************
|
||||
var getCurrentRoomList = function() {
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
if ("messages" in rooms[i]) {
|
||||
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||
}
|
||||
}
|
||||
roomInfo = rooms;
|
||||
setRooms(roomInfo);
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
};
|
||||
|
||||
var loadRoomContent = function(roomId) {
|
||||
console.log("loadRoomContent " + roomId);
|
||||
viewingRoomId = roomId;
|
||||
$("#roomName").text("Room: "+roomId);
|
||||
$(".sendMessageForm").css({visibility: "visible"});
|
||||
getMessages(roomId);
|
||||
getMemberList(roomId);
|
||||
};
|
||||
|
||||
var getMessages = function(roomId) {
|
||||
$("#messages").empty();
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +
|
||||
encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
|
||||
$.getJSON(url, function(data) {
|
||||
for (var i=data.chunk.length-1; i>=0; --i) {
|
||||
addMessage(data.chunk[i]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var getMemberList = function(roomId) {
|
||||
$("#members").empty();
|
||||
memberInfo = [];
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +
|
||||
encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
|
||||
$.getJSON(url, function(data) {
|
||||
for (var i=0; i<data.chunk.length; ++i) {
|
||||
memberInfo.push(data.chunk[i]);
|
||||
addMember(data.chunk[i]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ************** Sending messages **************
|
||||
$('.sendMessage').live('click', function() {
|
||||
if (viewingRoomId === undefined) {
|
||||
alert("There is no room to send a message to!");
|
||||
return;
|
||||
}
|
||||
var body = $("#body").val();
|
||||
sendMessage(viewingRoomId, body);
|
||||
});
|
||||
|
||||
var sendMessage = function(roomId, body) {
|
||||
var msgId = $.now();
|
||||
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
|
||||
var data = {
|
||||
msgtype: "m.text",
|
||||
body: body
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
$("#body").val("");
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ************** Navigation and DOM manipulation **************
|
||||
var setRooms = function(roomList) {
|
||||
// wipe existing entries
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
|
||||
var rows = "";
|
||||
for (var i=0; i<roomList.length; ++i) {
|
||||
row = "<tr>" +
|
||||
"<td>"+roomList[i].room_id+"</td>" +
|
||||
"<td>"+roomList[i].membership+"</td>" +
|
||||
"<td>"+roomList[i].latest_message+"</td>" +
|
||||
"</tr>";
|
||||
rows += row;
|
||||
}
|
||||
|
||||
$("#rooms").append(rows);
|
||||
|
||||
$('#rooms').find("tr").click(function(){
|
||||
var roomId = $(this).find('td:eq(0)').text();
|
||||
var membership = $(this).find('td:eq(1)').text();
|
||||
if (membership !== "join") {
|
||||
console.log("Joining room " + roomId);
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({membership: "join"}),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
loadRoomContent(roomId);
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
loadRoomContent(roomId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var addMessage = function(data) {
|
||||
|
||||
var msg = data.content.body;
|
||||
if (data.type === "m.room.member") {
|
||||
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 {
|
||||
msg = "<em>" + data.content.membership + "</em>";
|
||||
}
|
||||
}
|
||||
|
||||
var row = "<tr>" +
|
||||
"<td>"+data.user_id+"</td>" +
|
||||
"<td>"+msg+"</td>" +
|
||||
"</tr>";
|
||||
$("#messages").append(row);
|
||||
};
|
||||
|
||||
var addMember = function(data) {
|
||||
var row = "<tr>" +
|
||||
"<td>"+data.state_key+"</td>" +
|
||||
"<td>"+data.content.membership+"</td>" +
|
||||
"</tr>";
|
||||
$("#members").append(row);
|
||||
};
|
||||
|
||||
7
jsfiddles/register_login/demo.css
Normal file
7
jsfiddles/register_login/demo.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.loggedin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
20
jsfiddles/register_login/demo.html
Normal file
20
jsfiddles/register_login/demo.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<div>
|
||||
<p>This registration/login demo requires a home server to be running on http://localhost:8080</p>
|
||||
</div>
|
||||
<form class="registrationForm">
|
||||
<input type="text" id="user" placeholder="Username"></input>
|
||||
<input type="password" id="password" placeholder="Password"></input>
|
||||
<input type="button" class="register" value="Register"></input>
|
||||
</form>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
<div class="loggedin">
|
||||
<p id="welcomeText"></p>
|
||||
<input type="button" class="testToken" value="Test token"></input>
|
||||
<input type="button" class="logout" value="Logout"></input>
|
||||
<p id="imSyncText"></p>
|
||||
</div>
|
||||
|
||||
69
jsfiddles/register_login/demo.js
Normal file
69
jsfiddles/register_login/demo.js
Normal file
@@ -0,0 +1,69 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var showLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
$(".loggedin").css({visibility: "visible"});
|
||||
$("#welcomeText").text("Welcome " + accountInfo.user_id+". Your access token is: " +
|
||||
accountInfo.access_token);
|
||||
};
|
||||
|
||||
$('.register').live('click', function() {
|
||||
var user = $("#user").val();
|
||||
var password = $("#password").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_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",
|
||||
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) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.getJSON("http://localhost:8080/_matrix/client/api/v1/login", function(data) {
|
||||
if (data.flows[0].type !== "m.login.password") {
|
||||
alert("I don't know how to login with this type: " + data.type);
|
||||
return;
|
||||
}
|
||||
login(user, password);
|
||||
});
|
||||
});
|
||||
|
||||
$('.logout').live('click', function() {
|
||||
accountInfo = {};
|
||||
$("#imSyncText").text("");
|
||||
$(".loggedin").css({visibility: "hidden"});
|
||||
});
|
||||
|
||||
$('.testToken').live('click', function() {
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
|
||||
}).fail(function(err) {
|
||||
$("#imSyncText").text(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
});
|
||||
17
jsfiddles/room_memberships/demo.css
Normal file
17
jsfiddles/room_memberships/demo.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.loggedin {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table
|
||||
{
|
||||
border-spacing:5px;
|
||||
}
|
||||
|
||||
th,td
|
||||
{
|
||||
padding:5px;
|
||||
}
|
||||
37
jsfiddles/room_memberships/demo.html
Normal file
37
jsfiddles/room_memberships/demo.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<div>
|
||||
<p>This room membership demo requires a home server to be running on http://localhost:8080</p>
|
||||
</div>
|
||||
<form class="loginForm">
|
||||
<input type="text" id="userLogin" placeholder="Username"></input>
|
||||
<input type="password" id="passwordLogin" placeholder="Password"></input>
|
||||
<input type="button" class="login" value="Login"></input>
|
||||
</form>
|
||||
<div class="loggedin">
|
||||
<form class="createRoomForm">
|
||||
<input type="button" class="createRoom" value="Create Room"></input>
|
||||
</form>
|
||||
<form class="changeMembershipForm">
|
||||
<input type="text" id="roomId" placeholder="Room ID"></input>
|
||||
<input type="text" id="targetUser" placeholder="Target User ID"></input>
|
||||
<select id="membership">
|
||||
<option value="invite">invite</option>
|
||||
<option value="join">join</option>
|
||||
<option value="leave">leave</option>
|
||||
</select>
|
||||
<input type="button" class="changeMembership" value="Change Membership"></input>
|
||||
</form>
|
||||
<form class="joinAliasForm">
|
||||
<input type="text" id="roomAlias" placeholder="Room Alias (#name:domain)"></input>
|
||||
<input type="button" class="joinAlias" value="Join via Alias"></input>
|
||||
</form>
|
||||
<table id="rooms">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Room ID</th>
|
||||
<th>My state</th>
|
||||
<th>Room Alias</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
136
jsfiddles/room_memberships/demo.js
Normal file
136
jsfiddles/room_memberships/demo.js
Normal file
@@ -0,0 +1,136 @@
|
||||
var accountInfo = {};
|
||||
|
||||
var showLoggedIn = function(data) {
|
||||
accountInfo = data;
|
||||
getCurrentRoomList();
|
||||
$(".loggedin").css({visibility: "visible"});
|
||||
$("#membership").change(function() {
|
||||
if ($("#membership").val() === "invite") {
|
||||
$("#targetUser").css({visibility: "visible"});
|
||||
}
|
||||
else {
|
||||
$("#targetUser").css({visibility: "hidden"});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$('.login').live('click', function() {
|
||||
var user = $("#userLogin").val();
|
||||
var password = $("#passwordLogin").val();
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_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) {
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
showLoggedIn(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var getCurrentRoomList = function() {
|
||||
$("#roomId").val("");
|
||||
// wipe the table and reload it. Using the event stream would be the best
|
||||
// solution but that is out of scope of this fiddle.
|
||||
$("#rooms").find("tr:gt(0)").remove();
|
||||
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||
$.getJSON(url, function(data) {
|
||||
var rooms = data.rooms;
|
||||
for (var i=0; i<rooms.length; ++i) {
|
||||
addRoom(rooms[i]);
|
||||
}
|
||||
}).fail(function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
});
|
||||
};
|
||||
|
||||
$('.createRoom').live('click', function() {
|
||||
var data = {};
|
||||
$.ajax({
|
||||
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify(data),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
data.membership = "join"; // you are automatically joined into every room you make.
|
||||
data.latest_message = "";
|
||||
addRoom(data);
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var addRoom = function(data) {
|
||||
row = "<tr>" +
|
||||
"<td>"+data.room_id+"</td>" +
|
||||
"<td>"+data.membership+"</td>" +
|
||||
"<td>"+data.room_alias+"</td>" +
|
||||
"</tr>";
|
||||
$("#rooms").append(row);
|
||||
};
|
||||
|
||||
$('.changeMembership').live('click', function() {
|
||||
var roomId = $("#roomId").val();
|
||||
var member = $("#targetUser").val();
|
||||
var membership = $("#membership").val();
|
||||
|
||||
if (roomId.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||
url = url.replace("$membership", membership);
|
||||
|
||||
var data = {};
|
||||
|
||||
if (membership === "invite") {
|
||||
data = {
|
||||
user_id: member
|
||||
};
|
||||
}
|
||||
|
||||
$.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() {
|
||||
var roomAlias = $("#roomAlias").val();
|
||||
var url = "http://localhost:8080/_matrix/client/api/v1/join/$roomalias?access_token=$token";
|
||||
url = url.replace("$token", accountInfo.access_token);
|
||||
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({}),
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
getCurrentRoomList();
|
||||
},
|
||||
error: function(err) {
|
||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||
}
|
||||
});
|
||||
});
|
||||
24
nuke-room-from-db.sh
Executable file
24
nuke-room-from-db.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
## CAUTION:
|
||||
## This script will remove (hopefully) all trace of the given room ID from
|
||||
## your homeserver.db
|
||||
|
||||
## Do not run it lightly.
|
||||
|
||||
ROOMID="$1"
|
||||
|
||||
sqlite3 homeserver.db <<EOF
|
||||
DELETE FROM context_depth WHERE context = '$ROOMID';
|
||||
DELETE FROM current_state WHERE context = '$ROOMID';
|
||||
DELETE FROM feedback WHERE room_id = '$ROOMID';
|
||||
DELETE FROM messages WHERE room_id = '$ROOMID';
|
||||
DELETE FROM pdu_backward_extremities WHERE context = '$ROOMID';
|
||||
DELETE FROM pdu_edges WHERE context = '$ROOMID';
|
||||
DELETE FROM pdu_forward_extremities WHERE context = '$ROOMID';
|
||||
DELETE FROM pdus WHERE context = '$ROOMID';
|
||||
DELETE FROM room_data WHERE room_id = '$ROOMID';
|
||||
DELETE FROM room_memberships WHERE room_id = '$ROOMID';
|
||||
DELETE FROM rooms WHERE room_id = '$ROOMID';
|
||||
DELETE FROM state_pdus WHERE context = '$ROOMID';
|
||||
EOF
|
||||
5
setup.py
Normal file → Executable file
5
setup.py
Normal file → Executable file
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -25,13 +27,14 @@ def read(fname):
|
||||
|
||||
setup(
|
||||
name="SynapseHomeServer",
|
||||
version="0.1",
|
||||
version="0.0.1",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
description="Reference Synapse Home Server",
|
||||
install_requires=[
|
||||
"syutil==0.0.1",
|
||||
"Twisted>=14.0.0",
|
||||
"service_identity>=1.0.0",
|
||||
"pyyaml",
|
||||
"pyasn1",
|
||||
"pynacl",
|
||||
"daemonize",
|
||||
|
||||
@@ -15,3 +15,5 @@
|
||||
|
||||
""" This is a reference implementation of a synapse home server.
|
||||
"""
|
||||
|
||||
__version__ = "0.2.0"
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.errors import AuthError, StoreError
|
||||
from synapse.api.events.room import (RoomTopicEvent, RoomMemberEvent,
|
||||
MessageEvent, FeedbackEvent)
|
||||
from synapse.api.constants import Membership, JoinRules
|
||||
from synapse.api.errors import AuthError, StoreError, Codes
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import logging
|
||||
|
||||
@@ -34,7 +34,7 @@ class Auth(object):
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check(self, event, raises=False):
|
||||
def check(self, event, snapshot, raises=False):
|
||||
""" Checks if this event is correctly authed.
|
||||
|
||||
Returns:
|
||||
@@ -44,15 +44,32 @@ class Auth(object):
|
||||
be raised only if raises=True.
|
||||
"""
|
||||
try:
|
||||
if event.type in [RoomTopicEvent.TYPE, MessageEvent.TYPE,
|
||||
FeedbackEvent.TYPE]:
|
||||
yield self.check_joined_room(event.room_id, event.user_id)
|
||||
if hasattr(event, "room_id"):
|
||||
is_state = hasattr(event, "state_key")
|
||||
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
yield self._can_replace_state(event)
|
||||
allowed = yield self.is_membership_change_allowed(event)
|
||||
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:
|
||||
yield self._can_send_event(event)
|
||||
|
||||
defer.returnValue(True)
|
||||
elif event.type == RoomMemberEvent.TYPE:
|
||||
allowed = yield self.is_membership_change_allowed(event)
|
||||
defer.returnValue(allowed)
|
||||
else:
|
||||
raise AuthError(500, "Unknown event type %s" % event.type)
|
||||
raise AuthError(500, "Unknown event: %s" % event)
|
||||
except AuthError as e:
|
||||
logger.info("Event auth check failed on event %s with msg: %s",
|
||||
event, e.msg)
|
||||
@@ -67,16 +84,22 @@ class Auth(object):
|
||||
room_id=room_id,
|
||||
user_id=user_id
|
||||
)
|
||||
if not member or member.membership != Membership.JOIN:
|
||||
raise AuthError(403, "User %s not in room %s" %
|
||||
(user_id, room_id))
|
||||
self._check_joined_room(member, user_id, room_id)
|
||||
defer.returnValue(member)
|
||||
except AttributeError:
|
||||
pass
|
||||
defer.returnValue(None)
|
||||
|
||||
def _check_joined_room(self, member, user_id, room_id):
|
||||
if not member or member.membership != Membership.JOIN:
|
||||
raise AuthError(403, "User %s not in room %s (%s)" % (
|
||||
user_id, room_id, repr(member)
|
||||
))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def is_membership_change_allowed(self, event):
|
||||
target_user_id = event.state_key
|
||||
|
||||
# does this room even exist
|
||||
room = yield self.store.get_room(event.room_id)
|
||||
if not room:
|
||||
@@ -94,7 +117,7 @@ class Auth(object):
|
||||
# get info about the target
|
||||
try:
|
||||
target = yield self.store.get_room_member(
|
||||
user_id=event.target_user_id,
|
||||
user_id=target_user_id,
|
||||
room_id=event.room_id)
|
||||
except:
|
||||
target = None
|
||||
@@ -102,31 +125,74 @@ class Auth(object):
|
||||
|
||||
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:
|
||||
# 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.
|
||||
if not caller_in_room: # caller isn't joined
|
||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||
elif target_in_room: # the target is already in the room.
|
||||
raise AuthError(403, "%s is already in the room." %
|
||||
event.target_user_id)
|
||||
target_user_id)
|
||||
elif Membership.JOIN == membership:
|
||||
# Joins are valid iff caller == target and they were:
|
||||
# invited: They are accepting the invitation
|
||||
# joined: It's a NOOP
|
||||
if event.user_id != event.target_user_id:
|
||||
if event.user_id != target_user_id:
|
||||
raise AuthError(403, "Cannot force another user to join.")
|
||||
elif room.is_public:
|
||||
pass # anyone can join public rooms.
|
||||
elif (not caller or caller.membership not in
|
||||
[Membership.INVITE, Membership.JOIN]):
|
||||
raise AuthError(403, "You are not invited to this room.")
|
||||
elif join_rule == JoinRules.PUBLIC or room.is_public:
|
||||
pass
|
||||
elif join_rule == JoinRules.INVITE:
|
||||
if (
|
||||
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:
|
||||
# TODO (erikj): Implement kicks.
|
||||
|
||||
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)
|
||||
elif event.target_user_id != event.user_id:
|
||||
# trying to force another user to leave
|
||||
raise AuthError(403, "Cannot force %s to leave." %
|
||||
event.target_user_id)
|
||||
elif target_user_id != event.user_id:
|
||||
user_level = yield self.store.get_power_level(
|
||||
event.room_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 = 5
|
||||
|
||||
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 = 5 # FIXME (erikj): What should we do here?
|
||||
|
||||
if user_level < ban_level:
|
||||
raise AuthError(403, "You don't have permission to ban")
|
||||
else:
|
||||
raise AuthError(500, "Unknown membership %s" % membership)
|
||||
|
||||
@@ -161,6 +227,91 @@ class Auth(object):
|
||||
"""
|
||||
try:
|
||||
user_id = yield self.store.get_user_by_token(token=token)
|
||||
if not user_id:
|
||||
raise StoreError()
|
||||
defer.returnValue(self.hs.parse_userid(user_id))
|
||||
except StoreError:
|
||||
raise AuthError(403, "Unrecognised access token.")
|
||||
raise AuthError(403, "Unrecognised access token.",
|
||||
errcode=Codes.UNKNOWN_TOKEN)
|
||||
|
||||
@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"
|
||||
)
|
||||
|
||||
@@ -23,6 +23,8 @@ class Membership(object):
|
||||
JOIN = u"join"
|
||||
KNOCK = u"knock"
|
||||
LEAVE = u"leave"
|
||||
BAN = u"ban"
|
||||
LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN)
|
||||
|
||||
|
||||
class Feedback(object):
|
||||
@@ -30,8 +32,8 @@ class Feedback(object):
|
||||
"""Represents the types of feedback a user can send in response to a
|
||||
message."""
|
||||
|
||||
DELIVERED = u"d"
|
||||
READ = u"r"
|
||||
DELIVERED = u"delivered"
|
||||
READ = u"read"
|
||||
LIST = (DELIVERED, READ)
|
||||
|
||||
|
||||
@@ -41,3 +43,10 @@ class PresenceState(object):
|
||||
UNAVAILABLE = u"unavailable"
|
||||
ONLINE = u"online"
|
||||
FREE_FOR_CHAT = u"free_for_chat"
|
||||
|
||||
|
||||
class JoinRules(object):
|
||||
PUBLIC = u"public"
|
||||
KNOCK = u"knock"
|
||||
INVITE = u"invite"
|
||||
PRIVATE = u"private"
|
||||
|
||||
@@ -27,6 +27,7 @@ class Codes(object):
|
||||
BAD_PAGINATION = "M_BAD_PAGINATION"
|
||||
UNKNOWN = "M_UNKNOWN"
|
||||
NOT_FOUND = "M_NOT_FOUND"
|
||||
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||
|
||||
|
||||
class CodeMessageException(Exception):
|
||||
@@ -74,7 +75,10 @@ class AuthError(SynapseError):
|
||||
|
||||
class EventStreamError(SynapseError):
|
||||
"""An error raised when there a problem with the event stream."""
|
||||
pass
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "errcode" not in kwargs:
|
||||
kwargs["errcode"] = Codes.BAD_PAGINATION
|
||||
super(EventStreamError, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class LoginError(SynapseError):
|
||||
|
||||
@@ -41,16 +41,19 @@ class SynapseEvent(JsonEncodedObject):
|
||||
"room_id",
|
||||
"user_id", # sender/initiator
|
||||
"content", # HTTP body, JSON
|
||||
"state_key",
|
||||
"required_power_level",
|
||||
]
|
||||
|
||||
internal_keys = [
|
||||
"is_state",
|
||||
"state_key",
|
||||
"prev_events",
|
||||
"prev_state",
|
||||
"depth",
|
||||
"destinations",
|
||||
"origin",
|
||||
"outlier",
|
||||
"power_level",
|
||||
]
|
||||
|
||||
required_keys = [
|
||||
@@ -151,3 +154,10 @@ class SynapseEvent(JsonEncodedObject):
|
||||
msg = self._check_json(entry, template[key][0])
|
||||
if msg:
|
||||
return msg
|
||||
|
||||
|
||||
class SynapseStateEvent(SynapseEvent):
|
||||
def __init__(self, **kwargs):
|
||||
if "state_key" not in kwargs:
|
||||
kwargs["state_key"] = ""
|
||||
super(SynapseStateEvent, self).__init__(**kwargs)
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
|
||||
from synapse.api.events.room import (
|
||||
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
||||
InviteJoinEvent, RoomConfigEvent
|
||||
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
||||
RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
|
||||
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent
|
||||
)
|
||||
|
||||
from synapse.util.stringutils import random_string
|
||||
@@ -25,27 +27,38 @@ class EventFactory(object):
|
||||
|
||||
_event_classes = [
|
||||
RoomTopicEvent,
|
||||
RoomNameEvent,
|
||||
MessageEvent,
|
||||
RoomMemberEvent,
|
||||
FeedbackEvent,
|
||||
InviteJoinEvent,
|
||||
RoomConfigEvent
|
||||
RoomConfigEvent,
|
||||
RoomPowerLevelsEvent,
|
||||
RoomJoinRulesEvent,
|
||||
RoomCreateEvent,
|
||||
RoomAddStateLevelEvent,
|
||||
RoomSendEventLevelEvent,
|
||||
RoomOpsPowerLevelsEvent,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, hs):
|
||||
self._event_list = {} # dict of TYPE to event class
|
||||
for event_class in EventFactory._event_classes:
|
||||
self._event_list[event_class.TYPE] = event_class
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
def create_event(self, etype=None, **kwargs):
|
||||
kwargs["type"] = etype
|
||||
if "event_id" not in kwargs:
|
||||
kwargs["event_id"] = random_string(10)
|
||||
|
||||
try:
|
||||
if "ts" not in kwargs:
|
||||
kwargs["ts"] = int(self.clock.time_msec())
|
||||
|
||||
if etype in self._event_list:
|
||||
handler = self._event_list[etype]
|
||||
except KeyError: # unknown event type
|
||||
# TODO allow custom event types.
|
||||
raise NotImplementedError("Unknown etype=%s" % etype)
|
||||
else:
|
||||
handler = GenericEvent
|
||||
|
||||
return handler(**kwargs)
|
||||
|
||||
@@ -13,31 +13,63 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from . import SynapseEvent
|
||||
from synapse.api.constants import Feedback, Membership
|
||||
from synapse.api.errors import SynapseError
|
||||
from . import SynapseEvent, SynapseStateEvent
|
||||
|
||||
|
||||
class GenericEvent(SynapseEvent):
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
|
||||
|
||||
class RoomTopicEvent(SynapseEvent):
|
||||
TYPE = "m.room.topic"
|
||||
|
||||
internal_keys = SynapseEvent.internal_keys + [
|
||||
"topic",
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["state_key"] = ""
|
||||
if "topic" in kwargs["content"]:
|
||||
kwargs["topic"] = kwargs["content"]["topic"]
|
||||
super(RoomTopicEvent, self).__init__(**kwargs)
|
||||
|
||||
def get_content_template(self):
|
||||
return {"topic": u"string"}
|
||||
|
||||
|
||||
class RoomNameEvent(SynapseEvent):
|
||||
TYPE = "m.room.name"
|
||||
|
||||
internal_keys = SynapseEvent.internal_keys + [
|
||||
"name",
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["state_key"] = ""
|
||||
if "name" in kwargs["content"]:
|
||||
kwargs["name"] = kwargs["content"]["name"]
|
||||
super(RoomNameEvent, self).__init__(**kwargs)
|
||||
|
||||
def get_content_template(self):
|
||||
return {"name": u"string"}
|
||||
|
||||
|
||||
class RoomMemberEvent(SynapseEvent):
|
||||
TYPE = "m.room.member"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + [
|
||||
"target_user_id", # target
|
||||
# target is the state_key
|
||||
"membership", # action
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if "target_user_id" in kwargs:
|
||||
kwargs["state_key"] = kwargs["target_user_id"]
|
||||
if "membership" not in kwargs:
|
||||
kwargs["membership"] = kwargs.get("content", {}).get("membership")
|
||||
if not kwargs["membership"] in Membership.LIST:
|
||||
raise SynapseError(400, "Bad membership value.")
|
||||
super(RoomMemberEvent, self).__init__(**kwargs)
|
||||
|
||||
def get_content_template(self):
|
||||
@@ -61,24 +93,26 @@ class MessageEvent(SynapseEvent):
|
||||
class FeedbackEvent(SynapseEvent):
|
||||
TYPE = "m.room.message.feedback"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + [
|
||||
"msg_id", # the message ID being acknowledged
|
||||
"msg_sender_id", # person who is sending the feedback is 'user_id'
|
||||
"feedback_type", # the type of feedback (delivery, read, etc)
|
||||
]
|
||||
valid_keys = SynapseEvent.valid_keys
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(FeedbackEvent, self).__init__(**kwargs)
|
||||
if not kwargs["content"]["type"] in Feedback.LIST:
|
||||
raise SynapseError(400, "Bad feedback value.")
|
||||
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
return {
|
||||
"type": u"string",
|
||||
"target_event_id": u"string",
|
||||
"msg_sender_id": u"string"
|
||||
}
|
||||
|
||||
|
||||
class InviteJoinEvent(SynapseEvent):
|
||||
TYPE = "m.room.invite_join"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + [
|
||||
"target_user_id",
|
||||
# target_user_id is the state_key
|
||||
"target_host",
|
||||
]
|
||||
|
||||
@@ -98,3 +132,45 @@ class RoomConfigEvent(SynapseEvent):
|
||||
|
||||
def get_content_template(self):
|
||||
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 {}
|
||||
|
||||
@@ -1,191 +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 synapse.api.constants import Membership
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import reactor
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Notifier(object):
|
||||
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
self.hs = hs
|
||||
self.stored_event_listeners = {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_new_room_event(self, event, store_id):
|
||||
"""Called when there is a new room event which may potentially be sent
|
||||
down listening users' event streams.
|
||||
|
||||
This function looks for interested *users* who may want to be notified
|
||||
for this event. This is different to users requesting from the event
|
||||
stream which looks for interested *events* for this user.
|
||||
|
||||
Args:
|
||||
event (SynapseEvent): The new event, which must have a room_id
|
||||
store_id (int): The ID of this event after it was stored with the
|
||||
data store.
|
||||
'"""
|
||||
member_list = yield self.store.get_room_members(room_id=event.room_id,
|
||||
membership="join")
|
||||
if not member_list:
|
||||
member_list = []
|
||||
|
||||
member_list = [u.user_id for u in member_list]
|
||||
|
||||
# invites MUST prod the person being invited, who won't be in the room.
|
||||
if (event.type == RoomMemberEvent.TYPE and
|
||||
event.content["membership"] == Membership.INVITE):
|
||||
member_list.append(event.target_user_id)
|
||||
|
||||
for user_id in member_list:
|
||||
if user_id in self.stored_event_listeners:
|
||||
self._notify_and_callback(
|
||||
user_id=user_id,
|
||||
event_data=event.get_dict(),
|
||||
stream_type=event.type,
|
||||
store_id=store_id)
|
||||
|
||||
def on_new_user_event(self, user_id, event_data, stream_type, store_id):
|
||||
if user_id in self.stored_event_listeners:
|
||||
self._notify_and_callback(
|
||||
user_id=user_id,
|
||||
event_data=event_data,
|
||||
stream_type=stream_type,
|
||||
store_id=store_id
|
||||
)
|
||||
|
||||
def _notify_and_callback(self, user_id, event_data, stream_type, store_id):
|
||||
logger.debug(
|
||||
"Notifying %s of a new event.",
|
||||
user_id
|
||||
)
|
||||
|
||||
stream_ids = list(self.stored_event_listeners[user_id])
|
||||
for stream_id in stream_ids:
|
||||
self._notify_and_callback_stream(user_id, stream_id, event_data,
|
||||
stream_type, store_id)
|
||||
|
||||
if not self.stored_event_listeners[user_id]:
|
||||
del self.stored_event_listeners[user_id]
|
||||
|
||||
def _notify_and_callback_stream(self, user_id, stream_id, event_data,
|
||||
stream_type, store_id):
|
||||
|
||||
event_listener = self.stored_event_listeners[user_id].pop(stream_id)
|
||||
return_event_object = {
|
||||
k: event_listener[k] for k in ["start", "chunk", "end"]
|
||||
}
|
||||
|
||||
# work out the new end token
|
||||
token = event_listener["start"]
|
||||
end = self._next_token(stream_type, store_id, token)
|
||||
return_event_object["end"] = end
|
||||
|
||||
# add the event to the chunk
|
||||
chunk = event_listener["chunk"]
|
||||
chunk.append(event_data)
|
||||
|
||||
# callback the defer. We know this can't have been resolved before as
|
||||
# we always remove the event_listener from the map before resolving.
|
||||
event_listener["defer"].callback(return_event_object)
|
||||
|
||||
def _next_token(self, stream_type, store_id, current_token):
|
||||
stream_handler = self.hs.get_handlers().event_stream_handler
|
||||
return stream_handler.get_event_stream_token(
|
||||
stream_type,
|
||||
store_id,
|
||||
current_token
|
||||
)
|
||||
|
||||
def store_events_for(self, user_id=None, stream_id=None, from_tok=None):
|
||||
"""Store all incoming events for this user. This should be paired with
|
||||
get_events_for to return chunked data.
|
||||
|
||||
Args:
|
||||
user_id (str): The user to monitor incoming events for.
|
||||
stream (object): The stream that is receiving events
|
||||
from_tok (str): The token to monitor incoming events from.
|
||||
"""
|
||||
event_listener = {
|
||||
"start": from_tok,
|
||||
"chunk": [],
|
||||
"end": from_tok,
|
||||
"defer": defer.Deferred(),
|
||||
}
|
||||
|
||||
if user_id not in self.stored_event_listeners:
|
||||
self.stored_event_listeners[user_id] = {stream_id: event_listener}
|
||||
else:
|
||||
self.stored_event_listeners[user_id][stream_id] = event_listener
|
||||
|
||||
def purge_events_for(self, user_id=None, stream_id=None):
|
||||
"""Purges any stored events for this user.
|
||||
|
||||
Args:
|
||||
user_id (str): The user to purge stored events for.
|
||||
"""
|
||||
try:
|
||||
del self.stored_event_listeners[user_id][stream_id]
|
||||
if not self.stored_event_listeners[user_id]:
|
||||
del self.stored_event_listeners[user_id]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_events_for(self, user_id=None, stream_id=None, timeout=0):
|
||||
"""Retrieve stored events for this user, waiting if necessary.
|
||||
|
||||
It is advisable to wrap this call in a maybeDeferred.
|
||||
|
||||
Args:
|
||||
user_id (str): The user to get events for.
|
||||
timeout (int): The time in seconds to wait before giving up.
|
||||
Returns:
|
||||
A Deferred or a dict containing the chunk data, depending on if
|
||||
there was data to return yet. The Deferred callback may be None if
|
||||
there were no events before the timeout expired.
|
||||
"""
|
||||
logger.debug("%s is listening for events.", user_id)
|
||||
|
||||
try:
|
||||
streams = self.stored_event_listeners[user_id][stream_id]["chunk"]
|
||||
if streams:
|
||||
logger.debug("%s returning existing chunk.", user_id)
|
||||
return streams
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
reactor.callLater(
|
||||
(timeout / 1000.0), self._timeout, user_id, stream_id
|
||||
)
|
||||
return self.stored_event_listeners[user_id][stream_id]["defer"]
|
||||
|
||||
def _timeout(self, user_id, stream_id):
|
||||
try:
|
||||
# We remove the event_listener from the map so that we can't
|
||||
# resolve the deferred twice.
|
||||
event_listeners = self.stored_event_listeners[user_id]
|
||||
event_listener = event_listeners.pop(stream_id)
|
||||
event_listener["defer"].callback(None)
|
||||
logger.debug("%s event listening timed out.", user_id)
|
||||
except KeyError:
|
||||
pass
|
||||
@@ -1,97 +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 synapse.api.errors import SynapseError
|
||||
|
||||
|
||||
class PaginationConfig(object):
|
||||
|
||||
"""A configuration object which stores pagination parameters."""
|
||||
|
||||
def __init__(self, from_tok=None, to_tok=None, limit=0):
|
||||
self.from_tok = from_tok
|
||||
self.to_tok = to_tok
|
||||
self.limit = limit
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, request, raise_invalid_params=True):
|
||||
params = {
|
||||
"from_tok": PaginationStream.TOK_START,
|
||||
"to_tok": PaginationStream.TOK_END,
|
||||
"limit": 0
|
||||
}
|
||||
|
||||
query_param_mappings = [ # 3-tuple of qp_key, attribute, rules
|
||||
("from", "from_tok", lambda x: type(x) == str),
|
||||
("to", "to_tok", lambda x: type(x) == str),
|
||||
("limit", "limit", lambda x: x.isdigit())
|
||||
]
|
||||
|
||||
for qp, attr, is_valid in query_param_mappings:
|
||||
if qp in request.args:
|
||||
if is_valid(request.args[qp][0]):
|
||||
params[attr] = request.args[qp][0]
|
||||
elif raise_invalid_params:
|
||||
raise SynapseError(400, "%s parameter is invalid." % qp)
|
||||
|
||||
return PaginationConfig(**params)
|
||||
|
||||
|
||||
class PaginationStream(object):
|
||||
|
||||
""" An interface for streaming data as chunks. """
|
||||
|
||||
TOK_START = "START"
|
||||
TOK_END = "END"
|
||||
|
||||
def get_chunk(self, config=None):
|
||||
""" Return the next chunk in the stream.
|
||||
|
||||
Args:
|
||||
config (PaginationConfig): The config to aid which chunk to get.
|
||||
Returns:
|
||||
A dict containing the new start token "start", the new end token
|
||||
"end" and the data "chunk" as a list.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class StreamData(object):
|
||||
|
||||
""" An interface for obtaining streaming data from a table. """
|
||||
|
||||
def __init__(self, hs):
|
||||
self.hs = hs
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
def get_rows(self, user_id, from_pkey, to_pkey, limit):
|
||||
""" Get event stream data between the specified pkeys.
|
||||
|
||||
Args:
|
||||
user_id : The user's ID
|
||||
from_pkey : The starting pkey.
|
||||
to_pkey : The end pkey. May be -1 to mean "latest".
|
||||
limit: The max number of results to return.
|
||||
Returns:
|
||||
A tuple containing the list of event stream data and the last pkey.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def max_token(self):
|
||||
""" Get the latest currently-valid token.
|
||||
|
||||
Returns:
|
||||
The latest token."""
|
||||
raise NotImplementedError()
|
||||
@@ -1,248 +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.
|
||||
|
||||
"""This module contains classes for streaming from the event stream: /events.
|
||||
"""
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import EventStreamError
|
||||
from synapse.api.events.room import (
|
||||
RoomMemberEvent, MessageEvent, FeedbackEvent, RoomTopicEvent
|
||||
)
|
||||
from synapse.api.streams import PaginationStream, StreamData
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MessagesStreamData(StreamData):
|
||||
EVENT_TYPE = MessageEvent.TYPE
|
||||
|
||||
def __init__(self, hs, room_id=None, feedback=False):
|
||||
super(MessagesStreamData, self).__init__(hs)
|
||||
self.room_id = room_id
|
||||
self.with_feedback = feedback
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_rows(self, user_id, from_key, to_key, limit):
|
||||
(data, latest_ver) = yield self.store.get_message_stream(
|
||||
user_id=user_id,
|
||||
from_key=from_key,
|
||||
to_key=to_key,
|
||||
limit=limit,
|
||||
room_id=self.room_id,
|
||||
with_feedback=self.with_feedback
|
||||
)
|
||||
defer.returnValue((data, latest_ver))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def max_token(self):
|
||||
val = yield self.store.get_max_message_id()
|
||||
defer.returnValue(val)
|
||||
|
||||
|
||||
class RoomMemberStreamData(StreamData):
|
||||
EVENT_TYPE = RoomMemberEvent.TYPE
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_rows(self, user_id, from_key, to_key, limit):
|
||||
(data, latest_ver) = yield self.store.get_room_member_stream(
|
||||
user_id=user_id,
|
||||
from_key=from_key,
|
||||
to_key=to_key
|
||||
)
|
||||
|
||||
defer.returnValue((data, latest_ver))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def max_token(self):
|
||||
val = yield self.store.get_max_room_member_id()
|
||||
defer.returnValue(val)
|
||||
|
||||
|
||||
class FeedbackStreamData(StreamData):
|
||||
EVENT_TYPE = FeedbackEvent.TYPE
|
||||
|
||||
def __init__(self, hs, room_id=None):
|
||||
super(FeedbackStreamData, self).__init__(hs)
|
||||
self.room_id = room_id
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_rows(self, user_id, from_key, to_key, limit):
|
||||
(data, latest_ver) = yield self.store.get_feedback_stream(
|
||||
user_id=user_id,
|
||||
from_key=from_key,
|
||||
to_key=to_key,
|
||||
limit=limit,
|
||||
room_id=self.room_id
|
||||
)
|
||||
defer.returnValue((data, latest_ver))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def max_token(self):
|
||||
val = yield self.store.get_max_feedback_id()
|
||||
defer.returnValue(val)
|
||||
|
||||
|
||||
class RoomDataStreamData(StreamData):
|
||||
EVENT_TYPE = RoomTopicEvent.TYPE # TODO need multiple event types
|
||||
|
||||
def __init__(self, hs, room_id=None):
|
||||
super(RoomDataStreamData, self).__init__(hs)
|
||||
self.room_id = room_id
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_rows(self, user_id, from_key, to_key, limit):
|
||||
(data, latest_ver) = yield self.store.get_room_data_stream(
|
||||
user_id=user_id,
|
||||
from_key=from_key,
|
||||
to_key=to_key,
|
||||
limit=limit,
|
||||
room_id=self.room_id
|
||||
)
|
||||
defer.returnValue((data, latest_ver))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def max_token(self):
|
||||
val = yield self.store.get_max_room_data_id()
|
||||
defer.returnValue(val)
|
||||
|
||||
|
||||
class EventStream(PaginationStream):
|
||||
|
||||
SEPARATOR = '_'
|
||||
|
||||
def __init__(self, user_id, stream_data_list):
|
||||
super(EventStream, self).__init__()
|
||||
self.user_id = user_id
|
||||
self.stream_data = stream_data_list
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def fix_tokens(self, pagination_config):
|
||||
pagination_config.from_tok = yield self.fix_token(
|
||||
pagination_config.from_tok)
|
||||
pagination_config.to_tok = yield self.fix_token(
|
||||
pagination_config.to_tok)
|
||||
defer.returnValue(pagination_config)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def fix_token(self, token):
|
||||
"""Fixes unknown values in a token to known values.
|
||||
|
||||
Args:
|
||||
token (str): The token to fix up.
|
||||
Returns:
|
||||
The fixed-up token, which may == token.
|
||||
"""
|
||||
# replace TOK_START and TOK_END with 0_0_0 or -1_-1_-1 depending.
|
||||
replacements = [
|
||||
(PaginationStream.TOK_START, "0"),
|
||||
(PaginationStream.TOK_END, "-1")
|
||||
]
|
||||
for magic_token, key in replacements:
|
||||
if magic_token == token:
|
||||
token = EventStream.SEPARATOR.join(
|
||||
[key] * len(self.stream_data)
|
||||
)
|
||||
|
||||
# replace -1 values with an actual pkey
|
||||
token_segments = self._split_token(token)
|
||||
for i, tok in enumerate(token_segments):
|
||||
if tok == -1:
|
||||
# add 1 to the max token because results are EXCLUSIVE from the
|
||||
# latest version.
|
||||
token_segments[i] = 1 + (yield self.stream_data[i].max_token())
|
||||
defer.returnValue(EventStream.SEPARATOR.join(
|
||||
str(x) for x in token_segments
|
||||
))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_chunk(self, config=None):
|
||||
# no support for limit on >1 streams, makes no sense.
|
||||
if config.limit and len(self.stream_data) > 1:
|
||||
raise EventStreamError(
|
||||
400, "Limit not supported on multiplexed streams."
|
||||
)
|
||||
|
||||
(chunk_data, next_tok) = yield self._get_chunk_data(config.from_tok,
|
||||
config.to_tok,
|
||||
config.limit)
|
||||
|
||||
defer.returnValue({
|
||||
"chunk": chunk_data,
|
||||
"start": config.from_tok,
|
||||
"end": next_tok
|
||||
})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _get_chunk_data(self, from_tok, to_tok, limit):
|
||||
""" Get event data between the two tokens.
|
||||
|
||||
Tokens are SEPARATOR separated values representing pkey values of
|
||||
certain tables, and the position determines the StreamData invoked
|
||||
according to the STREAM_DATA list.
|
||||
|
||||
The magic value '-1' can be used to get the latest value.
|
||||
|
||||
Args:
|
||||
from_tok - The token to start from.
|
||||
to_tok - The token to end at. Must have values > from_tok or be -1.
|
||||
Returns:
|
||||
A list of event data.
|
||||
Raises:
|
||||
EventStreamError if something went wrong.
|
||||
"""
|
||||
# sanity check
|
||||
if (from_tok.count(EventStream.SEPARATOR) !=
|
||||
to_tok.count(EventStream.SEPARATOR) or
|
||||
(from_tok.count(EventStream.SEPARATOR) + 1) !=
|
||||
len(self.stream_data)):
|
||||
raise EventStreamError(400, "Token lengths don't match.")
|
||||
|
||||
chunk = []
|
||||
next_ver = []
|
||||
for i, (from_pkey, to_pkey) in enumerate(zip(
|
||||
self._split_token(from_tok),
|
||||
self._split_token(to_tok)
|
||||
)):
|
||||
if from_pkey == to_pkey:
|
||||
# tokens are the same, we have nothing to do.
|
||||
next_ver.append(str(to_pkey))
|
||||
continue
|
||||
|
||||
(event_chunk, max_pkey) = yield self.stream_data[i].get_rows(
|
||||
self.user_id, from_pkey, to_pkey, limit
|
||||
)
|
||||
|
||||
chunk += event_chunk
|
||||
next_ver.append(str(max_pkey))
|
||||
|
||||
defer.returnValue((chunk, EventStream.SEPARATOR.join(next_ver)))
|
||||
|
||||
def _split_token(self, token):
|
||||
"""Splits the given token into a list of pkeys.
|
||||
|
||||
Args:
|
||||
token (str): The token with SEPARATOR values.
|
||||
Returns:
|
||||
A list of ints.
|
||||
"""
|
||||
segments = token.split(EventStream.SEPARATOR)
|
||||
try:
|
||||
int_segments = [int(x) for x in segments]
|
||||
except ValueError:
|
||||
raise EventStreamError(400, "Bad token: %s" % token)
|
||||
return int_segments
|
||||
21
synapse/api/urls.py
Normal file
21
synapse/api/urls.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- 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.
|
||||
|
||||
"""Contains the URL paths to prefix various aspects of the server with. """
|
||||
|
||||
CLIENT_PREFIX = "/_matrix/client/api/v1"
|
||||
FEDERATION_PREFIX = "/_matrix/federation/v1"
|
||||
WEB_CLIENT_PREFIX = "/_matrix/client"
|
||||
CONTENT_REPO_PREFIX = "/_matrix/content"
|
||||
287
synapse/app/homeserver.py
Normal file → Executable file
287
synapse/app/homeserver.py
Normal file → Executable file
@@ -20,86 +20,203 @@ from synapse.server import HomeServer
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.enterprise import adbapi
|
||||
from twisted.python.log import PythonLoggingObserver
|
||||
from synapse.http.server import TwistedHttpServer
|
||||
from twisted.web.resource import Resource
|
||||
from twisted.web.static import File
|
||||
from twisted.web.server import Site
|
||||
from synapse.http.server import JsonResource, RootRedirect, ContentRepoResource
|
||||
from synapse.http.client import TwistedHttpClient
|
||||
from synapse.api.urls import (
|
||||
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
|
||||
)
|
||||
from synapse.config.homeserver import HomeServerConfig
|
||||
from synapse.crypto import context_factory
|
||||
|
||||
from daemonize import Daemonize
|
||||
import twisted.manhole.telnet
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import logging.config
|
||||
import sqlite3
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
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 = 2
|
||||
|
||||
|
||||
class SynapseHomeServer(HomeServer):
|
||||
def build_http_server(self):
|
||||
return TwistedHttpServer()
|
||||
|
||||
def build_http_client(self):
|
||||
return TwistedHttpClient()
|
||||
return TwistedHttpClient(self)
|
||||
|
||||
def build_resource_for_client(self):
|
||||
return JsonResource()
|
||||
|
||||
def build_resource_for_federation(self):
|
||||
return JsonResource()
|
||||
|
||||
def build_resource_for_web_client(self):
|
||||
return File("webclient") # TODO configurable?
|
||||
|
||||
def build_resource_for_content_repo(self):
|
||||
return ContentRepoResource(self, self.upload_dir, self.auth)
|
||||
|
||||
def build_db_pool(self):
|
||||
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
||||
don't have to worry about overwriting existing content.
|
||||
"""
|
||||
logging.info("Preparing database: %s...", self.db_name)
|
||||
|
||||
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)
|
||||
|
||||
schemas = [
|
||||
"transactions",
|
||||
"pdu",
|
||||
"users",
|
||||
"profiles",
|
||||
"presence",
|
||||
"im",
|
||||
"room_aliases",
|
||||
]
|
||||
|
||||
for sql_loc in schemas:
|
||||
sql_script = read_schema(sql_loc)
|
||||
|
||||
with sqlite3.connect(self.db_name) as db_conn:
|
||||
c = db_conn.cursor()
|
||||
c.executescript(sql_script)
|
||||
c.close()
|
||||
db_conn.commit()
|
||||
|
||||
logging.info("Database prepared in %s.", self.db_name)
|
||||
|
||||
return pool
|
||||
|
||||
def create_resource_tree(self, web_client, redirect_root_to_web_client):
|
||||
"""Create the resource tree for this Home Server.
|
||||
|
||||
def setup_logging(verbosity=0, filename=None, config_path=None):
|
||||
""" Sets up logging with verbosity levels.
|
||||
This in unduly complicated because Twisted does not support putting
|
||||
child resources more than 1 level deep at a time.
|
||||
|
||||
Args:
|
||||
verbosity: The verbosity level.
|
||||
filename: Log to the given file rather than to the console.
|
||||
config_path: Path to a python logging config file.
|
||||
"""
|
||||
Args:
|
||||
web_client (bool): True to enable the web client.
|
||||
redirect_root_to_web_client (bool): True to redirect '/' to the
|
||||
location of the web client. This does nothing if web_client is not
|
||||
True.
|
||||
"""
|
||||
# list containing (path_str, Resource) e.g:
|
||||
# [ ("/aaa/bbb/cc", Resource1), ("/aaa/dummy", Resource2) ]
|
||||
desired_tree = [
|
||||
(CLIENT_PREFIX, self.get_resource_for_client()),
|
||||
(FEDERATION_PREFIX, self.get_resource_for_federation()),
|
||||
(CONTENT_REPO_PREFIX, self.get_resource_for_content_repo())
|
||||
]
|
||||
if web_client:
|
||||
logger.info("Adding the web client.")
|
||||
desired_tree.append((WEB_CLIENT_PREFIX,
|
||||
self.get_resource_for_web_client()))
|
||||
|
||||
if config_path is None:
|
||||
log_format = (
|
||||
'%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
|
||||
)
|
||||
if web_client and redirect_root_to_web_client:
|
||||
self.root_resource = RootRedirect(WEB_CLIENT_PREFIX)
|
||||
else:
|
||||
self.root_resource = Resource()
|
||||
|
||||
level = logging.INFO
|
||||
if verbosity:
|
||||
level = logging.DEBUG
|
||||
# ideally we'd just use getChild and putChild but getChild doesn't work
|
||||
# unless you give it a Request object IN ADDITION to the name :/ So
|
||||
# instead, we'll store a copy of this mapping so we can actually add
|
||||
# extra resources to existing nodes. See self._resource_id for the key.
|
||||
resource_mappings = {}
|
||||
for (full_path, resource) in desired_tree:
|
||||
logging.info("Attaching %s to path %s", resource, full_path)
|
||||
last_resource = self.root_resource
|
||||
for path_seg in full_path.split('/')[1:-1]:
|
||||
if not path_seg in last_resource.listNames():
|
||||
# resource doesn't exist, so make a "dummy resource"
|
||||
child_resource = Resource()
|
||||
last_resource.putChild(path_seg, child_resource)
|
||||
res_id = self._resource_id(last_resource, path_seg)
|
||||
resource_mappings[res_id] = child_resource
|
||||
last_resource = child_resource
|
||||
else:
|
||||
# we have an existing Resource, use that instead.
|
||||
res_id = self._resource_id(last_resource, path_seg)
|
||||
last_resource = resource_mappings[res_id]
|
||||
|
||||
# FIXME: we need a logging.WARN for a -q quiet option
|
||||
# ===========================
|
||||
# now attach the actual desired resource
|
||||
last_path_seg = full_path.split('/')[-1]
|
||||
|
||||
logging.basicConfig(level=level, filename=filename, format=log_format)
|
||||
else:
|
||||
logging.config.fileConfig(config_path)
|
||||
# if there is already a resource here, thieve its children and
|
||||
# replace it
|
||||
res_id = self._resource_id(last_resource, last_path_seg)
|
||||
if res_id in resource_mappings:
|
||||
# there is a dummy resource at this path already, which needs
|
||||
# to be replaced with the desired resource.
|
||||
existing_dummy_resource = resource_mappings[res_id]
|
||||
for child_name in existing_dummy_resource.listNames():
|
||||
child_res_id = self._resource_id(existing_dummy_resource,
|
||||
child_name)
|
||||
child_resource = resource_mappings[child_res_id]
|
||||
# steal the children
|
||||
resource.putChild(child_name, child_resource)
|
||||
|
||||
observer = PythonLoggingObserver()
|
||||
observer.start()
|
||||
# finally, insert the desired resource in the right place
|
||||
last_resource.putChild(last_path_seg, resource)
|
||||
res_id = self._resource_id(last_resource, last_path_seg)
|
||||
resource_mappings[res_id] = resource
|
||||
|
||||
return self.root_resource
|
||||
|
||||
def _resource_id(self, resource, path_seg):
|
||||
"""Construct an arbitrary resource ID so you can retrieve the mapping
|
||||
later.
|
||||
|
||||
If you want to represent resource A putChild resource B with path C,
|
||||
the mapping should looks like _resource_id(A,C) = B.
|
||||
|
||||
Args:
|
||||
resource (Resource): The *parent* Resource
|
||||
path_seg (str): The name of the child Resource to be attached.
|
||||
Returns:
|
||||
str: A unique string which can be a key to the child Resource.
|
||||
"""
|
||||
return "%s-%s" % (resource, path_seg)
|
||||
|
||||
def start_listening(self, secure_port, unsecure_port):
|
||||
if secure_port is not None:
|
||||
reactor.listenSSL(
|
||||
secure_port, Site(self.root_resource), self.tls_context_factory
|
||||
)
|
||||
logger.info("Synapse now listening on port %d", secure_port)
|
||||
if unsecure_port is not None:
|
||||
reactor.listenTCP(
|
||||
unsecure_port, Site(self.root_resource)
|
||||
)
|
||||
logger.info("Synapse now listening on port %d", unsecure_port)
|
||||
|
||||
|
||||
def run():
|
||||
@@ -107,57 +224,53 @@ def run():
|
||||
|
||||
|
||||
def setup():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-p", "--port", dest="port", type=int, default=8080,
|
||||
help="The port to listen on.")
|
||||
parser.add_argument("-d", "--database", dest="db", default="homeserver.db",
|
||||
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",
|
||||
action="store_true", help="Host the web client.")
|
||||
args = parser.parse_args()
|
||||
|
||||
verbosity = int(args.verbose) if args.verbose else None
|
||||
|
||||
setup_logging(
|
||||
verbosity=verbosity,
|
||||
filename=args.log_file,
|
||||
config_path=args.log_config,
|
||||
config = HomeServerConfig.load_config(
|
||||
"Synapse Homeserver",
|
||||
sys.argv[1:],
|
||||
generate_section="Homeserver"
|
||||
)
|
||||
|
||||
logger.info("Server hostname: %s", args.host)
|
||||
config.setup_logging()
|
||||
|
||||
logger.info("Server hostname: %s", config.server_name)
|
||||
|
||||
if re.search(":[0-9]+$", config.server_name):
|
||||
domain_with_port = config.server_name
|
||||
else:
|
||||
domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
|
||||
|
||||
tls_context_factory = context_factory.ServerContextFactory(config)
|
||||
|
||||
hs = SynapseHomeServer(
|
||||
args.host,
|
||||
db_name=args.db
|
||||
config.server_name,
|
||||
domain_with_port=domain_with_port,
|
||||
upload_dir=os.path.abspath("uploads"),
|
||||
db_name=config.database_path,
|
||||
tls_context_factory=tls_context_factory,
|
||||
)
|
||||
|
||||
# This object doesn't need to be saved because it's set as the handler for
|
||||
# the replication layer
|
||||
hs.get_federation()
|
||||
|
||||
hs.register_servlets()
|
||||
|
||||
hs.get_http_server().start_listening(args.port)
|
||||
hs.create_resource_tree(
|
||||
web_client=config.webclient,
|
||||
redirect_root_to_web_client=True,
|
||||
)
|
||||
hs.start_listening(config.bind_port, config.unsecure_port)
|
||||
|
||||
hs.build_db_pool()
|
||||
hs.get_db_pool()
|
||||
|
||||
if args.daemonize:
|
||||
if config.manhole:
|
||||
f = twisted.manhole.telnet.ShellFactory()
|
||||
f.username = "matrix"
|
||||
f.password = "rabbithole"
|
||||
f.namespace['hs'] = hs
|
||||
reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
|
||||
|
||||
if config.daemonize:
|
||||
print config.pid_file
|
||||
daemon = Daemonize(
|
||||
app="synapse-homeserver",
|
||||
pid=args.pid,
|
||||
pid=config.pid_file,
|
||||
action=run,
|
||||
auto_close_fds=False,
|
||||
verbose=True,
|
||||
|
||||
14
synapse/config/__init__.py
Normal file
14
synapse/config/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- 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.
|
||||
130
synapse/config/_base.py
Normal file
130
synapse/config/_base.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# -*- 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 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:
|
||||
yaml.dump(config, config_file, default_flow_style=False)
|
||||
sys.exit(0)
|
||||
|
||||
return cls(args)
|
||||
|
||||
|
||||
|
||||
37
synapse/config/database.py
Normal file
37
synapse/config/database.py
Normal file
@@ -0,0 +1,37 @@
|
||||
# -*- 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
|
||||
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)
|
||||
|
||||
@@ -13,21 +13,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module contains REST servlets to do with public paths: /public"""
|
||||
from twisted.internet import defer
|
||||
from .tls import TlsConfig
|
||||
from .server import ServerConfig
|
||||
from .logger import LoggingConfig
|
||||
from .database import DatabaseConfig
|
||||
|
||||
from base import RestServlet, client_path_pattern
|
||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig):
|
||||
pass
|
||||
|
||||
|
||||
class PublicRoomListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/public/rooms$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
handler = self.handlers.room_list_handler
|
||||
data = yield handler.get_public_room_list()
|
||||
defer.returnValue((200, data))
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
PublicRoomListRestServlet(hs).register(http_server)
|
||||
if __name__=='__main__':
|
||||
import sys
|
||||
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
|
||||
67
synapse/config/logger.py
Normal file
67
synapse/config/logger.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- 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
|
||||
|
||||
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()
|
||||
79
synapse/config/server.py
Normal file
79
synapse/config/server.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# -*- 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 nacl.signing
|
||||
import os
|
||||
from ._base import Config
|
||||
from syutil.base64util import encode_base64, decode_base64
|
||||
|
||||
|
||||
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
|
||||
|
||||
@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="hs.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.")
|
||||
|
||||
def read_signing_key(self, signing_key_path):
|
||||
signing_key_base64 = self.read_file(signing_key_path, "signing_key")
|
||||
signing_key_bytes = decode_base64(signing_key_base64)
|
||||
return nacl.signing.SigningKey(signing_key_bytes)
|
||||
|
||||
@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:
|
||||
key = nacl.signing.SigningKey.generate()
|
||||
signing_key_file.write(encode_base64(key.encode()))
|
||||
130
synapse/config/tls.py
Normal file
130
synapse/config/tls.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# -*- 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
|
||||
|
||||
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"
|
||||
)
|
||||
@@ -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:])
|
||||
29
synapse/crypto/context_factory.py
Normal file
29
synapse/crypto/context_factory.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from twisted.internet import reactor, ssl
|
||||
from OpenSSL import SSL
|
||||
from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName
|
||||
|
||||
|
||||
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:
|
||||
pass
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ from .transport import TransportLayer
|
||||
def initialize_http_replication(homeserver):
|
||||
transport = TransportLayer(
|
||||
homeserver.hostname,
|
||||
server=homeserver.get_http_server(),
|
||||
server=homeserver.get_resource_for_federation(),
|
||||
client=homeserver.get_http_client()
|
||||
)
|
||||
|
||||
|
||||
@@ -1,149 +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 defer
|
||||
|
||||
from .pdu_codec import PduCodec
|
||||
|
||||
from synapse.api.errors import AuthError
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FederationEventHandler(object):
|
||||
""" Responsible for:
|
||||
a) handling received Pdus before handing them on as Events to the rest
|
||||
of the home server (including auth and state conflict resoultion)
|
||||
b) converting events that were produced by local clients that may need
|
||||
to be sent to remote home servers.
|
||||
"""
|
||||
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
self.replication_layer = hs.get_replication_layer()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
# self.auth_handler = gs.get_auth_handler()
|
||||
self.event_handler = hs.get_handlers().federation_handler
|
||||
self.server_name = hs.hostname
|
||||
|
||||
self.lock_manager = hs.get_room_lock_manager()
|
||||
|
||||
self.replication_layer.set_handler(self)
|
||||
|
||||
self.pdu_codec = PduCodec(hs)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def handle_new_event(self, event):
|
||||
""" Takes in an event from the client to server side, that has already
|
||||
been authed and handled by the state module, and sends it to any
|
||||
remote home servers that may be interested.
|
||||
|
||||
Args:
|
||||
event
|
||||
|
||||
Returns:
|
||||
Deferred: Resolved when it has successfully been queued for
|
||||
processing.
|
||||
"""
|
||||
yield self._fill_out_prev_events(event)
|
||||
|
||||
pdu = self.pdu_codec.pdu_from_event(event)
|
||||
|
||||
if not hasattr(pdu, "destinations") or not pdu.destinations:
|
||||
pdu.destinations = []
|
||||
|
||||
yield self.replication_layer.send_pdu(pdu)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def backfill(self, room_id, limit):
|
||||
# TODO: Work out which destinations to ask for backfill
|
||||
# self.replication_layer.backfill(dest, room_id, limit)
|
||||
pass
|
||||
|
||||
@log_function
|
||||
def get_state_for_room(self, destination, room_id):
|
||||
return self.replication_layer.get_state_for_context(
|
||||
destination, room_id
|
||||
)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def on_receive_pdu(self, pdu):
|
||||
""" Called by the ReplicationLayer when we have a new pdu. We need to
|
||||
do auth checks and put it throught the StateHandler.
|
||||
"""
|
||||
event = self.pdu_codec.event_from_pdu(pdu)
|
||||
|
||||
try:
|
||||
with (yield self.lock_manager.lock(pdu.context)):
|
||||
if event.is_state:
|
||||
is_new_state = yield self.state_handler.handle_new_state(
|
||||
pdu
|
||||
)
|
||||
if not is_new_state:
|
||||
return
|
||||
else:
|
||||
is_new_state = False
|
||||
|
||||
yield self.event_handler.on_receive(event, is_new_state)
|
||||
|
||||
except AuthError:
|
||||
# TODO: Implement something in federation that allows us to
|
||||
# respond to PDU.
|
||||
raise
|
||||
|
||||
return
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _on_new_state(self, pdu, new_state_event):
|
||||
# TODO: Do any store stuff here. Notifiy C2S about this new
|
||||
# state.
|
||||
|
||||
yield self.store.update_current_state(
|
||||
pdu_id=pdu.pdu_id,
|
||||
origin=pdu.origin,
|
||||
context=pdu.context,
|
||||
pdu_type=pdu.pdu_type,
|
||||
state_key=pdu.state_key
|
||||
)
|
||||
|
||||
yield self.event_handler.on_receive(new_state_event)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _fill_out_prev_events(self, event):
|
||||
if hasattr(event, "prev_events"):
|
||||
return
|
||||
|
||||
results = yield self.store.get_latest_pdus_in_context(
|
||||
event.room_id
|
||||
)
|
||||
|
||||
es = [
|
||||
"%s@%s" % (p_id, origin) for p_id, origin, _ in results
|
||||
]
|
||||
|
||||
event.prev_events = [e for e in es if e != event.event_id]
|
||||
|
||||
if results:
|
||||
event.depth = max([int(v) for _, _, v in results]) + 1
|
||||
else:
|
||||
event.depth = 0
|
||||
@@ -25,7 +25,6 @@ from .units import Pdu
|
||||
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -40,28 +39,6 @@ class PduActions(object):
|
||||
def __init__(self, datastore):
|
||||
self.store = datastore
|
||||
|
||||
@log_function
|
||||
def persist_received(self, pdu):
|
||||
""" Persists the given `Pdu` that was received from a remote home
|
||||
server.
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
"""
|
||||
return self._persist(pdu)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def persist_outgoing(self, pdu):
|
||||
""" Persists the given `Pdu` that this home server created.
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
"""
|
||||
ret = yield self._persist(pdu)
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
@log_function
|
||||
def mark_as_processed(self, pdu):
|
||||
""" Persist the fact that we have fully processed the given `Pdu`
|
||||
@@ -71,25 +48,6 @@ class PduActions(object):
|
||||
"""
|
||||
return self.store.mark_pdu_as_processed(pdu.pdu_id, pdu.origin)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def populate_previous_pdus(self, pdu):
|
||||
""" Given an outgoing `Pdu` fill out its `prev_ids` key with the `Pdu`s
|
||||
that we have received.
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
"""
|
||||
results = yield self.store.get_latest_pdus_in_context(pdu.context)
|
||||
|
||||
pdu.prev_pdus = [(p_id, origin) for p_id, origin, _ in results]
|
||||
|
||||
vs = [int(v) for _, _, v in results]
|
||||
if vs:
|
||||
pdu.depth = max(vs) + 1
|
||||
else:
|
||||
pdu.depth = 0
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def after_transaction(self, transaction_id, destination, origin):
|
||||
@@ -143,28 +101,6 @@ class PduActions(object):
|
||||
depth=pdu.depth
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def _persist(self, pdu):
|
||||
kwargs = copy.copy(pdu.__dict__)
|
||||
unrec_keys = copy.copy(pdu.unrecognized_keys)
|
||||
del kwargs["content"]
|
||||
kwargs["content_json"] = json.dumps(pdu.content)
|
||||
kwargs["unrecognized_keys"] = json.dumps(unrec_keys)
|
||||
|
||||
logger.debug("Persisting: %s", repr(kwargs))
|
||||
|
||||
if pdu.is_state:
|
||||
ret = yield self.store.persist_state(**kwargs)
|
||||
else:
|
||||
ret = yield self.store.persist_pdu(**kwargs)
|
||||
|
||||
yield self.store.update_min_depth_for_context(
|
||||
pdu.context, pdu.depth
|
||||
)
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
|
||||
class TransactionActions(object):
|
||||
""" Defines persistence actions that relate to handling Transactions.
|
||||
|
||||
@@ -134,10 +134,8 @@ class ReplicationLayer(object):
|
||||
|
||||
logger.debug("[%s] Persisting PDU", pdu.pdu_id)
|
||||
|
||||
#yield self.pdu_actions.populate_previous_pdus(pdu)
|
||||
|
||||
# Save *before* trying to send
|
||||
yield self.pdu_actions.persist_outgoing(pdu)
|
||||
yield self.store.persist_event(pdu=pdu)
|
||||
|
||||
logger.debug("[%s] Persisted PDU", pdu.pdu_id)
|
||||
logger.debug("[%s] transaction_layer.enqueue_pdu... ", pdu.pdu_id)
|
||||
@@ -158,6 +156,7 @@ class ReplicationLayer(object):
|
||||
|
||||
# TODO, add errback, etc.
|
||||
self._transaction_queue.enqueue_edu(edu)
|
||||
return defer.succeed(None)
|
||||
|
||||
@log_function
|
||||
def make_query(self, destination, query_type, args):
|
||||
@@ -208,7 +207,7 @@ class ReplicationLayer(object):
|
||||
|
||||
pdus = [Pdu(outlier=False, **p) for p in transaction.pdus]
|
||||
for pdu in pdus:
|
||||
yield self._handle_new_pdu(pdu)
|
||||
yield self._handle_new_pdu(pdu, backfilled=True)
|
||||
|
||||
defer.returnValue(pdus)
|
||||
|
||||
@@ -415,7 +414,7 @@ class ReplicationLayer(object):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def _handle_new_pdu(self, pdu):
|
||||
def _handle_new_pdu(self, pdu, backfilled=False):
|
||||
# We reprocess pdus when we have seen them only as outliers
|
||||
existing = yield self._get_persisted_pdu(pdu.pdu_id, pdu.origin)
|
||||
|
||||
@@ -449,9 +448,12 @@ class ReplicationLayer(object):
|
||||
logger.exception("Failed to get PDU")
|
||||
|
||||
# Persist the Pdu, but don't mark it as processed yet.
|
||||
yield self.pdu_actions.persist_received(pdu)
|
||||
yield self.store.persist_event(pdu=pdu)
|
||||
|
||||
ret = yield self.handler.on_receive_pdu(pdu)
|
||||
if not backfilled:
|
||||
ret = yield self.handler.on_receive_pdu(pdu, backfilled=backfilled)
|
||||
else:
|
||||
ret = None
|
||||
|
||||
yield self.pdu_actions.mark_as_processed(pdu)
|
||||
|
||||
@@ -505,10 +507,10 @@ class _TransactionQueue(object):
|
||||
# a transaction in progress. If we do, stick it in the pending_pdus
|
||||
# table and we'll get back to it later.
|
||||
|
||||
destinations = [
|
||||
destinations = set([
|
||||
d for d in pdu.destinations
|
||||
if d != self.server_name
|
||||
]
|
||||
])
|
||||
|
||||
logger.debug("Sending to: %s", str(destinations))
|
||||
|
||||
@@ -539,7 +541,10 @@ class _TransactionQueue(object):
|
||||
)
|
||||
|
||||
def eb(failure):
|
||||
deferred.errback(failure)
|
||||
if not deferred.called:
|
||||
deferred.errback(failure)
|
||||
else:
|
||||
logger.exception("Failed to send edu", failure)
|
||||
self._attempt_new_transaction(destination).addErrback(eb)
|
||||
|
||||
return deferred
|
||||
|
||||
@@ -23,6 +23,7 @@ over a different (albeit still reliable) protocol.
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import logging
|
||||
@@ -33,9 +34,6 @@ import re
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PREFIX = "/matrix/federation/v1"
|
||||
|
||||
|
||||
class TransportLayer(object):
|
||||
"""This is a basic implementation of the transport layer that translates
|
||||
transactions and other requests to/from HTTP.
|
||||
|
||||
@@ -68,6 +68,7 @@ class Pdu(JsonEncodedObject):
|
||||
"power_level",
|
||||
"prev_state_id",
|
||||
"prev_state_origin",
|
||||
"required_power_level",
|
||||
]
|
||||
|
||||
internal_keys = [
|
||||
|
||||
@@ -15,14 +15,16 @@
|
||||
|
||||
from .register import RegistrationHandler
|
||||
from .room import (
|
||||
MessageHandler, RoomCreationHandler, RoomMemberHandler, RoomListHandler
|
||||
RoomCreationHandler, RoomMemberHandler, RoomListHandler
|
||||
)
|
||||
from .events import EventStreamHandler
|
||||
from .message import MessageHandler
|
||||
from .events import EventStreamHandler, EventHandler
|
||||
from .federation import FederationHandler
|
||||
from .login import LoginHandler
|
||||
from .profile import ProfileHandler
|
||||
from .presence import PresenceHandler
|
||||
from .directory import DirectoryHandler
|
||||
from .typing import TypingNotificationHandler
|
||||
|
||||
|
||||
class Handlers(object):
|
||||
@@ -39,9 +41,11 @@ class Handlers(object):
|
||||
self.room_creation_handler = RoomCreationHandler(hs)
|
||||
self.room_member_handler = RoomMemberHandler(hs)
|
||||
self.event_stream_handler = EventStreamHandler(hs)
|
||||
self.event_handler = EventHandler(hs)
|
||||
self.federation_handler = FederationHandler(hs)
|
||||
self.profile_handler = ProfileHandler(hs)
|
||||
self.presence_handler = PresenceHandler(hs)
|
||||
self.room_list_handler = RoomListHandler(hs)
|
||||
self.login_handler = LoginHandler(hs)
|
||||
self.directory_handler = DirectoryHandler(hs)
|
||||
self.typing_notification_handler = TypingNotificationHandler(hs)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
class BaseHandler(object):
|
||||
|
||||
@@ -24,4 +24,27 @@ class BaseHandler(object):
|
||||
self.notifier = hs.get_notifier()
|
||||
self.room_lock = hs.get_room_lock_manager()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
self.distributor = hs.get_distributor()
|
||||
self.hs = hs
|
||||
|
||||
|
||||
class BaseRoomHandler(BaseHandler):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _on_new_room_event(self, event, snapshot, extra_destinations=[],
|
||||
extra_users=[]):
|
||||
snapshot.fill_out_prev_events(event)
|
||||
|
||||
yield self.store.persist_event(event)
|
||||
|
||||
destinations = set(extra_destinations)
|
||||
# Send a PDU to all hosts who have joined the room.
|
||||
destinations.update((yield self.store.get_joined_hosts_for_room(
|
||||
event.room_id
|
||||
)))
|
||||
event.destinations = list(destinations)
|
||||
|
||||
self.notifier.on_new_room_event(event, extra_users=extra_users)
|
||||
|
||||
federation_handler = self.hs.get_handlers().federation_handler
|
||||
yield federation_handler.handle_new_event(event, snapshot)
|
||||
|
||||
@@ -20,17 +20,11 @@ from ._base import BaseHandler
|
||||
from synapse.api.errors import SynapseError
|
||||
|
||||
import logging
|
||||
import json
|
||||
import urllib
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO(erikj): This needs to be factored out somewere
|
||||
PREFIX = "/matrix/client/api/v1"
|
||||
|
||||
|
||||
class DirectoryHandler(BaseHandler):
|
||||
|
||||
def __init__(self, hs):
|
||||
|
||||
@@ -15,24 +15,19 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.events import SynapseEvent
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
from ._base import BaseHandler
|
||||
from synapse.api.streams.event import (
|
||||
EventStream, MessagesStreamData, RoomMemberStreamData, FeedbackStreamData,
|
||||
RoomDataStreamData
|
||||
)
|
||||
from synapse.handlers.presence import PresenceStreamData
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventStreamHandler(BaseHandler):
|
||||
|
||||
stream_data_classes = [
|
||||
MessagesStreamData,
|
||||
RoomMemberStreamData,
|
||||
FeedbackStreamData,
|
||||
RoomDataStreamData,
|
||||
PresenceStreamData,
|
||||
]
|
||||
|
||||
def __init__(self, hs):
|
||||
super(EventStreamHandler, self).__init__(hs)
|
||||
|
||||
@@ -47,45 +42,13 @@ class EventStreamHandler(BaseHandler):
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
def get_event_stream_token(self, stream_type, store_id, start_token):
|
||||
"""Return the next token after this event.
|
||||
|
||||
Args:
|
||||
stream_type (str): The StreamData.EVENT_TYPE
|
||||
store_id (int): The new storage ID assigned from the data store.
|
||||
start_token (str): The token the user started with.
|
||||
Returns:
|
||||
str: The end token.
|
||||
"""
|
||||
for i, stream_cls in enumerate(EventStreamHandler.stream_data_classes):
|
||||
if stream_cls.EVENT_TYPE == stream_type:
|
||||
# this is the stream for this event, so replace this part of
|
||||
# the token
|
||||
store_ids = start_token.split(EventStream.SEPARATOR)
|
||||
store_ids[i] = str(store_id)
|
||||
return EventStream.SEPARATOR.join(store_ids)
|
||||
raise RuntimeError("Didn't find a stream type %s" % stream_type)
|
||||
self.notifier = hs.get_notifier()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def get_stream(self, auth_user_id, pagin_config, timeout=0):
|
||||
"""Gets events as an event stream for this user.
|
||||
|
||||
This function looks for interesting *events* for this user. This is
|
||||
different from the notifier, which looks for interested *users* who may
|
||||
want to know about a single event.
|
||||
|
||||
Args:
|
||||
auth_user_id (str): The user requesting their event stream.
|
||||
pagin_config (synapse.api.streams.PaginationConfig): The config to
|
||||
use when obtaining the stream.
|
||||
timeout (int): The max time to wait for an incoming event in ms.
|
||||
Returns:
|
||||
A pagination stream API dict
|
||||
"""
|
||||
auth_user = self.hs.parse_userid(auth_user_id)
|
||||
|
||||
stream_id = object()
|
||||
|
||||
try:
|
||||
if auth_user not in self._streams_per_user:
|
||||
self._streams_per_user[auth_user] = 0
|
||||
@@ -98,41 +61,30 @@ class EventStreamHandler(BaseHandler):
|
||||
)
|
||||
self._streams_per_user[auth_user] += 1
|
||||
|
||||
# construct an event stream with the correct data ordering
|
||||
stream_data_list = []
|
||||
for stream_class in EventStreamHandler.stream_data_classes:
|
||||
stream_data_list.append(stream_class(self.hs))
|
||||
event_stream = EventStream(auth_user_id, stream_data_list)
|
||||
if pagin_config.from_token is None:
|
||||
pagin_config.from_token = None
|
||||
|
||||
# fix unknown tokens to known tokens
|
||||
pagin_config = yield event_stream.fix_tokens(pagin_config)
|
||||
rm_handler = self.hs.get_handlers().room_member_handler
|
||||
room_ids = yield rm_handler.get_rooms_for_user(auth_user)
|
||||
|
||||
# register interest in receiving new events
|
||||
self.notifier.store_events_for(user_id=auth_user_id,
|
||||
stream_id=stream_id,
|
||||
from_tok=pagin_config.from_tok)
|
||||
events, tokens = yield self.notifier.get_events_for(
|
||||
auth_user, room_ids, pagin_config, timeout
|
||||
)
|
||||
|
||||
# see if we can grab a chunk now
|
||||
data_chunk = yield event_stream.get_chunk(config=pagin_config)
|
||||
chunks = [
|
||||
e.get_dict() if isinstance(e, SynapseEvent) else e
|
||||
for e in events
|
||||
]
|
||||
|
||||
# if there are previous events, return those. If not, wait on the
|
||||
# new events for 'timeout' seconds.
|
||||
if len(data_chunk["chunk"]) == 0 and timeout != 0:
|
||||
results = yield defer.maybeDeferred(
|
||||
self.notifier.get_events_for,
|
||||
user_id=auth_user_id,
|
||||
stream_id=stream_id,
|
||||
timeout=timeout
|
||||
)
|
||||
if results:
|
||||
defer.returnValue(results)
|
||||
chunk = {
|
||||
"chunk": chunks,
|
||||
"start": tokens[0].to_string(),
|
||||
"end": tokens[1].to_string(),
|
||||
}
|
||||
|
||||
defer.returnValue(chunk)
|
||||
|
||||
defer.returnValue(data_chunk)
|
||||
finally:
|
||||
# cleanup
|
||||
self.notifier.purge_events_for(user_id=auth_user_id,
|
||||
stream_id=stream_id)
|
||||
|
||||
self._streams_per_user[auth_user] -= 1
|
||||
if not self._streams_per_user[auth_user]:
|
||||
del self._streams_per_user[auth_user]
|
||||
@@ -140,11 +92,39 @@ class EventStreamHandler(BaseHandler):
|
||||
# 10 seconds of grace to allow the client to reconnect again
|
||||
# before we think they're gone
|
||||
def _later():
|
||||
logger.debug("_later stopped_user_eventstream %s", auth_user)
|
||||
self.distributor.fire(
|
||||
"stopped_user_eventstream", auth_user
|
||||
)
|
||||
del self._stop_timer_per_user[auth_user]
|
||||
|
||||
logger.debug("Scheduling _later: for %s", auth_user)
|
||||
self._stop_timer_per_user[auth_user] = (
|
||||
self.clock.call_later(5, _later)
|
||||
self.clock.call_later(30, _later)
|
||||
)
|
||||
|
||||
|
||||
class EventHandler(BaseHandler):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_event(self, user, event_id):
|
||||
"""Retrieve a single specified event.
|
||||
|
||||
Args:
|
||||
user (synapse.types.UserID): The user requesting the event
|
||||
event_id (str): The event ID to obtain.
|
||||
Returns:
|
||||
dict: An event, or None if there is no event matching this ID.
|
||||
Raises:
|
||||
SynapseError if there was a problem retrieving this event, or
|
||||
AuthError if the user does not have the rights to inspect this
|
||||
event.
|
||||
"""
|
||||
event = yield self.store.get_event(event_id)
|
||||
|
||||
if not event:
|
||||
defer.returnValue(None)
|
||||
return
|
||||
|
||||
yield self.auth.check(event, raises=True)
|
||||
defer.returnValue(event)
|
||||
|
||||
@@ -20,6 +20,7 @@ from ._base import BaseHandler
|
||||
from synapse.api.events.room import InviteJoinEvent, RoomMemberEvent
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.util.logutils import log_function
|
||||
from synapse.federation.pdu_codec import PduCodec
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
@@ -30,12 +31,79 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FederationHandler(BaseHandler):
|
||||
"""Handles events that originated from federation.
|
||||
Responsible for:
|
||||
a) handling received Pdus before handing them on as Events to the rest
|
||||
of the home server (including auth and state conflict resoultion)
|
||||
b) converting events that were produced by local clients that may need
|
||||
to be sent to remote home servers.
|
||||
"""
|
||||
|
||||
"""Handles events that originated from federation."""
|
||||
def __init__(self, hs):
|
||||
super(FederationHandler, self).__init__(hs)
|
||||
|
||||
self.distributor.observe(
|
||||
"user_joined_room",
|
||||
self._on_user_joined
|
||||
)
|
||||
|
||||
self.waiting_for_join_list = {}
|
||||
|
||||
self.store = hs.get_datastore()
|
||||
self.replication_layer = hs.get_replication_layer()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
# self.auth_handler = gs.get_auth_handler()
|
||||
self.server_name = hs.hostname
|
||||
|
||||
self.lock_manager = hs.get_room_lock_manager()
|
||||
|
||||
self.replication_layer.set_handler(self)
|
||||
|
||||
self.pdu_codec = PduCodec(hs)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def on_receive(self, event, is_new_state):
|
||||
def handle_new_event(self, event, snapshot):
|
||||
""" Takes in an event from the client to server side, that has already
|
||||
been authed and handled by the state module, and sends it to any
|
||||
remote home servers that may be interested.
|
||||
|
||||
Args:
|
||||
event
|
||||
snapshot (.storage.Snapshot): THe snapshot the event happened after
|
||||
|
||||
Returns:
|
||||
Deferred: Resolved when it has successfully been queued for
|
||||
processing.
|
||||
"""
|
||||
|
||||
pdu = self.pdu_codec.pdu_from_event(event)
|
||||
|
||||
if not hasattr(pdu, "destinations") or not pdu.destinations:
|
||||
pdu.destinations = []
|
||||
|
||||
yield self.replication_layer.send_pdu(pdu)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def on_receive_pdu(self, pdu, backfilled):
|
||||
""" Called by the ReplicationLayer when we have a new pdu. We need to
|
||||
do auth checks and put it throught the StateHandler.
|
||||
"""
|
||||
event = self.pdu_codec.event_from_pdu(pdu)
|
||||
|
||||
with (yield self.lock_manager.lock(pdu.context)):
|
||||
if event.is_state and not backfilled:
|
||||
is_new_state = yield self.state_handler.handle_new_state(
|
||||
pdu
|
||||
)
|
||||
if not is_new_state:
|
||||
return
|
||||
else:
|
||||
is_new_state = False
|
||||
# TODO: Implement something in federation that allows us to
|
||||
# respond to PDU.
|
||||
|
||||
if hasattr(event, "state_key") and not is_new_state:
|
||||
logger.debug("Ignoring old state.")
|
||||
return
|
||||
@@ -56,7 +124,7 @@ class FederationHandler(BaseHandler):
|
||||
content.update({"membership": Membership.JOIN})
|
||||
new_event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
target_user_id=event.user_id,
|
||||
state_key=event.user_id,
|
||||
room_id=event.room_id,
|
||||
user_id=event.user_id,
|
||||
membership=Membership.JOIN,
|
||||
@@ -65,11 +133,121 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
yield self.hs.get_handlers().room_member_handler.change_membership(
|
||||
new_event,
|
||||
True
|
||||
do_auth=True
|
||||
)
|
||||
|
||||
else:
|
||||
with (yield self.room_lock.lock(event.room_id)):
|
||||
store_id = yield self.store.persist_event(event)
|
||||
yield self.store.persist_event(event, backfilled)
|
||||
|
||||
yield self.notifier.on_new_room_event(event, store_id)
|
||||
room = yield self.store.get_room(event.room_id)
|
||||
|
||||
if not room:
|
||||
# Huh, let's try and get the current state
|
||||
try:
|
||||
yield self.replication_layer.get_state_for_context(
|
||||
event.origin, event.room_id
|
||||
)
|
||||
|
||||
hosts = yield self.store.get_joined_hosts_for_room(
|
||||
event.room_id
|
||||
)
|
||||
if self.hs.hostname in hosts:
|
||||
try:
|
||||
yield self.store.store_room(
|
||||
room_id=event.room_id,
|
||||
room_creator_user_id="",
|
||||
is_public=False,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
logger.exception(
|
||||
"Failed to get current state for room %s",
|
||||
event.room_id
|
||||
)
|
||||
|
||||
if not backfilled:
|
||||
yield self.notifier.on_new_room_event(event)
|
||||
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
if event.membership == Membership.JOIN:
|
||||
user = self.hs.parse_userid(event.state_key)
|
||||
self.distributor.fire(
|
||||
"user_joined_room", user=user, room_id=event.room_id
|
||||
)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def backfill(self, dest, room_id, limit):
|
||||
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
|
||||
|
||||
events = []
|
||||
|
||||
for pdu in pdus:
|
||||
event = self.pdu_codec.event_from_pdu(pdu)
|
||||
events.append(event)
|
||||
yield self.store.persist_event(event, backfilled=True)
|
||||
|
||||
defer.returnValue(events)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def do_invite_join(self, target_host, room_id, joinee, content, snapshot):
|
||||
|
||||
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
||||
if self.hs.hostname in hosts:
|
||||
# We are already in the room.
|
||||
logger.debug("We're already in the room apparently")
|
||||
defer.returnValue(False)
|
||||
|
||||
# First get current state to see if we are already joined.
|
||||
try:
|
||||
yield self.replication_layer.get_state_for_context(
|
||||
target_host, room_id
|
||||
)
|
||||
|
||||
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
||||
if self.hs.hostname in hosts:
|
||||
# Oh, we were actually in the room already.
|
||||
logger.debug("We're already in the room apparently")
|
||||
defer.returnValue(False)
|
||||
except Exception:
|
||||
logger.exception("Failed to get current state")
|
||||
|
||||
new_event = self.event_factory.create_event(
|
||||
etype=InviteJoinEvent.TYPE,
|
||||
target_host=target_host,
|
||||
room_id=room_id,
|
||||
user_id=joinee,
|
||||
content=content
|
||||
)
|
||||
|
||||
new_event.destinations = [target_host]
|
||||
|
||||
snapshot.fill_out_prev_events(new_event)
|
||||
yield self.handle_new_event(new_event, snapshot)
|
||||
|
||||
# TODO (erikj): Time out here.
|
||||
d = defer.Deferred()
|
||||
self.waiting_for_join_list.setdefault((joinee, room_id), []).append(d)
|
||||
yield d
|
||||
|
||||
try:
|
||||
yield self.store.store_room(
|
||||
room_id=room_id,
|
||||
room_creator_user_id="",
|
||||
is_public=False
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
defer.returnValue(True)
|
||||
|
||||
|
||||
@log_function
|
||||
def _on_user_joined(self, user, room_id):
|
||||
waiters = self.waiting_for_join_list.get((user.to_string(), room_id), [])
|
||||
while waiters:
|
||||
waiters.pop().callback(None)
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
from twisted.internet import defer
|
||||
|
||||
from ._base import BaseHandler
|
||||
from synapse.api.errors import LoginError
|
||||
from synapse.api.errors import LoginError, Codes
|
||||
|
||||
import bcrypt
|
||||
import logging
|
||||
@@ -51,7 +51,7 @@ class LoginHandler(BaseHandler):
|
||||
user_info = yield self.store.get_user_by_id(user_id=user)
|
||||
if not user_info:
|
||||
logger.warn("Attempted to login as %s but they do not exist.", user)
|
||||
raise LoginError(403, "")
|
||||
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||
|
||||
stored_hash = user_info[0]["password_hash"]
|
||||
if bcrypt.checkpw(password, stored_hash):
|
||||
@@ -62,4 +62,4 @@ class LoginHandler(BaseHandler):
|
||||
defer.returnValue(token)
|
||||
else:
|
||||
logger.warn("Failed password login for user %s", user)
|
||||
raise LoginError(403, "")
|
||||
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user