mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-05 01:10:13 +00:00
Compare commits
2362 Commits
v0.6.0a
...
erikj/chec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
228465b0ec | ||
|
|
91cb3b630d | ||
|
|
dffc9c4ae0 | ||
|
|
184a5c81f0 | ||
|
|
30768dcf40 | ||
|
|
4ae73d16a9 | ||
|
|
a5b41b809f | ||
|
|
dd0867f5ba | ||
|
|
c0d1f37baf | ||
|
|
709ba99afd | ||
|
|
9e4dacd5e7 | ||
|
|
d23bc77e2c | ||
|
|
73e4ad4b8b | ||
|
|
076e19da28 | ||
|
|
3ead04ceef | ||
|
|
227b77409f | ||
|
|
efeeff29f6 | ||
|
|
1002bbd732 | ||
|
|
9ad38c9807 | ||
|
|
bdf2e5865a | ||
|
|
fd0a919af3 | ||
|
|
e90f32646f | ||
|
|
aaf319820a | ||
|
|
a9ad647fb2 | ||
|
|
77580addc3 | ||
|
|
8e8955bcea | ||
|
|
530896d9d2 | ||
|
|
24a5a8a118 | ||
|
|
7ab401d4dc | ||
|
|
a88e16152f | ||
|
|
00149c063b | ||
|
|
ab9e01809d | ||
|
|
236245f7d8 | ||
|
|
57df6fffa7 | ||
|
|
b62c1395d6 | ||
|
|
9c8eb4a809 | ||
|
|
b854a375b0 | ||
|
|
cd800ad99a | ||
|
|
3e4de64bc9 | ||
|
|
d71af2ee12 | ||
|
|
b143641b20 | ||
|
|
4d1ea40008 | ||
|
|
8256a8ece7 | ||
|
|
a7122692d9 | ||
|
|
b442217d91 | ||
|
|
c961cd7736 | ||
|
|
5371c2a1f7 | ||
|
|
4a6d894850 | ||
|
|
ddf4d2bd98 | ||
|
|
66ec6cf9b8 | ||
|
|
53c2eed862 | ||
|
|
f02532baad | ||
|
|
25b32b63ae | ||
|
|
e330c802e4 | ||
|
|
c9cb354b58 | ||
|
|
5a9e0c3682 | ||
|
|
e85c7873dc | ||
|
|
86fac9c95e | ||
|
|
3063383547 | ||
|
|
4c56928263 | ||
|
|
6f0c344ca7 | ||
|
|
d3c0e48859 | ||
|
|
06094591c5 | ||
|
|
fd246fde89 | ||
|
|
4f6fa981ec | ||
|
|
3cab86a122 | ||
|
|
e768d7b3a6 | ||
|
|
efdaa5dd55 | ||
|
|
da51acf0e7 | ||
|
|
f4d552589e | ||
|
|
90fde4b8d7 | ||
|
|
0de2aad061 | ||
|
|
3f6f74686a | ||
|
|
82145912c3 | ||
|
|
a2355fae7e | ||
|
|
ee3fa1a99c | ||
|
|
59891a294f | ||
|
|
3e1029fe80 | ||
|
|
af7c1397d1 | ||
|
|
460cad7c11 | ||
|
|
825f0875bc | ||
|
|
a9d8bd95e7 | ||
|
|
bfb66773a4 | ||
|
|
57619d6058 | ||
|
|
a0b181bd17 | ||
|
|
1925a38f95 | ||
|
|
747535f20f | ||
|
|
133d90abfb | ||
|
|
3a20cdcd27 | ||
|
|
d046adf4ec | ||
|
|
1d1c303b9b | ||
|
|
d33f31d741 | ||
|
|
c63df2d4e0 | ||
|
|
f63208a1c0 | ||
|
|
43f2e42bfd | ||
|
|
4bd05573e9 | ||
|
|
12b1a47ba4 | ||
|
|
37403ab06c | ||
|
|
2e31dd2ad3 | ||
|
|
8b52fe48b5 | ||
|
|
d9088c923f | ||
|
|
86cef6a91b | ||
|
|
1c847af28a | ||
|
|
cf8c04948f | ||
|
|
aa361f51dc | ||
|
|
037481a033 | ||
|
|
01fc3943f1 | ||
|
|
571ac105e6 | ||
|
|
51c53369a3 | ||
|
|
f093873d69 | ||
|
|
61f36d9939 | ||
|
|
f8f3d72e2b | ||
|
|
78323ccdb3 | ||
|
|
457970c724 | ||
|
|
1bd1a43073 | ||
|
|
0f6a25f670 | ||
|
|
b9490e8cbb | ||
|
|
5dbd102470 | ||
|
|
fd5ad0f00e | ||
|
|
745b72660a | ||
|
|
42f12ad92f | ||
|
|
aa3c9c7bd0 | ||
|
|
1f7642efa9 | ||
|
|
3e9ee62db0 | ||
|
|
21b71b6d7c | ||
|
|
b1e35eabf2 | ||
|
|
c7788685b0 | ||
|
|
8c74bd8960 | ||
|
|
f483340b3e | ||
|
|
ea570ffaeb | ||
|
|
7049e1564f | ||
|
|
d5a825edee | ||
|
|
225c244aba | ||
|
|
4e706ec82c | ||
|
|
31621c2e06 | ||
|
|
f90ea3dc73 | ||
|
|
ce2a7ed6e4 | ||
|
|
e8cf77fa49 | ||
|
|
cecbd636e9 | ||
|
|
b578c822e3 | ||
|
|
3befc9ccc3 | ||
|
|
d5c31e01f2 | ||
|
|
cb8201ba12 | ||
|
|
c141d47a28 | ||
|
|
13a6517d89 | ||
|
|
61cd03466f | ||
|
|
f764f92647 | ||
|
|
ca0d28ef34 | ||
|
|
8a951540f6 | ||
|
|
482648123f | ||
|
|
fd88ea19c0 | ||
|
|
bb9611bd46 | ||
|
|
9b63def388 | ||
|
|
23b21e5215 | ||
|
|
9d720223f2 | ||
|
|
617501dd2a | ||
|
|
099ce4bc38 | ||
|
|
22346a0ee7 | ||
|
|
cbd053bb8f | ||
|
|
be27d81808 | ||
|
|
ade5342752 | ||
|
|
4cf302de5b | ||
|
|
c50ad14bae | ||
|
|
a0b8e5f2fe | ||
|
|
aadb2238c9 | ||
|
|
f9e7493ac2 | ||
|
|
ecc59ae66e | ||
|
|
70e265e695 | ||
|
|
09d23b6209 | ||
|
|
daa01842f8 | ||
|
|
7f08ebb772 | ||
|
|
d7272f8d9d | ||
|
|
78fa346b07 | ||
|
|
a45ec7c651 | ||
|
|
40da1f200d | ||
|
|
abc6986a24 | ||
|
|
ce832c38d4 | ||
|
|
42e858daeb | ||
|
|
e624cdec64 | ||
|
|
c3dd2ecd5e | ||
|
|
38a965b816 | ||
|
|
a82938416d | ||
|
|
0bfdaf1f4f | ||
|
|
a5cbd20001 | ||
|
|
128ed32e6b | ||
|
|
3e6fdfda00 | ||
|
|
ee59af9ac0 | ||
|
|
1469141023 | ||
|
|
cacdb529ab | ||
|
|
2d3462714e | ||
|
|
f704c10f29 | ||
|
|
6e7d36a72c | ||
|
|
d3da63f766 | ||
|
|
8199475ce0 | ||
|
|
0d4abf7777 | ||
|
|
e55291ce5e | ||
|
|
8e254862f4 | ||
|
|
85d0bc3bdc | ||
|
|
cfc503681f | ||
|
|
dc2a105fca | ||
|
|
83eb627b5a | ||
|
|
776ee6d92b | ||
|
|
f72ed6c6a3 | ||
|
|
8899df13bf | ||
|
|
8f4165628b | ||
|
|
d3d582bc1c | ||
|
|
4d8e1e1f9e | ||
|
|
afef6f5d16 | ||
|
|
2d97e65558 | ||
|
|
1a9510bb84 | ||
|
|
47abebfd6d | ||
|
|
f9d4da7f45 | ||
|
|
30883d8409 | ||
|
|
891dfd90bd | ||
|
|
68b255c5a1 | ||
|
|
95b0f5449d | ||
|
|
129ee4e149 | ||
|
|
c5966b2a97 | ||
|
|
0cceb2ac92 | ||
|
|
d6bcc68ea7 | ||
|
|
b16cd18a86 | ||
|
|
9f7f228ec2 | ||
|
|
3d77e56c12 | ||
|
|
d884047d34 | ||
|
|
2bb2c02571 | ||
|
|
3d1cdda762 | ||
|
|
57877b01d7 | ||
|
|
5db5677969 | ||
|
|
7e77a82c5f | ||
|
|
7eb4d626ba | ||
|
|
06750140f6 | ||
|
|
adbd720fab | ||
|
|
8b7ce2945b | ||
|
|
a6c27de1aa | ||
|
|
c044aca1fd | ||
|
|
ba5d34a832 | ||
|
|
6a191d62ed | ||
|
|
21ac8be5f7 | ||
|
|
3e4e367f09 | ||
|
|
0fbed2a8fa | ||
|
|
998a72d4d9 | ||
|
|
c10ac7806e | ||
|
|
101ee3fd00 | ||
|
|
df361d08f7 | ||
|
|
7b0e797080 | ||
|
|
2eb91e6694 | ||
|
|
cfa62007a3 | ||
|
|
5ce903e2f7 | ||
|
|
a7eeb34c64 | ||
|
|
f7e2f981ea | ||
|
|
bcc1d34d35 | ||
|
|
f4122c64b5 | ||
|
|
415c2f0549 | ||
|
|
f43041aacd | ||
|
|
73605f8070 | ||
|
|
de3b7b55d6 | ||
|
|
d46208c12c | ||
|
|
4f11a5b2b5 | ||
|
|
7bbaab9432 | ||
|
|
7b49236b37 | ||
|
|
fdb724cb70 | ||
|
|
7e3d1c7d92 | ||
|
|
d7451e0f22 | ||
|
|
4807616e16 | ||
|
|
275f7c987c | ||
|
|
b24d7ebd6e | ||
|
|
2df8dd9b37 | ||
|
|
a23a760b3f | ||
|
|
7568fe880d | ||
|
|
4ff0228c25 | ||
|
|
dcd5983fe4 | ||
|
|
45610305ea | ||
|
|
88e03da39f | ||
|
|
9dba813234 | ||
|
|
53a817518b | ||
|
|
6eaa116867 | ||
|
|
4762c276cb | ||
|
|
dc8399ee00 | ||
|
|
1b994a97dd | ||
|
|
10b874067b | ||
|
|
017b798e4f | ||
|
|
2c019eea11 | ||
|
|
bb0a475c30 | ||
|
|
dcefac3b06 | ||
|
|
559c51debc | ||
|
|
6f274f7e13 | ||
|
|
7ce71f2ffc | ||
|
|
8c3a62b5c7 | ||
|
|
86eaaa885b | ||
|
|
e0b6e49466 | ||
|
|
2cd6cb9f65 | ||
|
|
aa88582e00 | ||
|
|
5119e416e8 | ||
|
|
8f04b6fa7a | ||
|
|
7dec0b2bee | ||
|
|
06218ab125 | ||
|
|
2352974aab | ||
|
|
9c5385b53a | ||
|
|
ffab798a38 | ||
|
|
62126c996c | ||
|
|
3213ff630c | ||
|
|
20addfa358 | ||
|
|
9eb5b23d3a | ||
|
|
0211890134 | ||
|
|
ffdb8c3828 | ||
|
|
e69b669083 | ||
|
|
0db40d3e93 | ||
|
|
e3c8e2c13c | ||
|
|
efe60d5e8c | ||
|
|
b2c7bd4b09 | ||
|
|
b3768ec10a | ||
|
|
b8e386db59 | ||
|
|
fe994e728f | ||
|
|
0ac61b1c78 | ||
|
|
0caf30f94b | ||
|
|
1d08bf7c17 | ||
|
|
63b1eaf32c | ||
|
|
b811c98574 | ||
|
|
433314cc34 | ||
|
|
8049c9a71e | ||
|
|
f596ff402e | ||
|
|
2efb93af52 | ||
|
|
953dbd28a7 | ||
|
|
7eea3e356f | ||
|
|
3e1b77efc2 | ||
|
|
b52b4a84ec | ||
|
|
1e62a3d3a9 | ||
|
|
a89559d797 | ||
|
|
07507643cb | ||
|
|
185ac7ee6c | ||
|
|
a0dea6eaed | ||
|
|
c67ba143fa | ||
|
|
e7768e77f5 | ||
|
|
883aabe423 | ||
|
|
07ad03d5df | ||
|
|
e124128542 | ||
|
|
c77048e12f | ||
|
|
2e35a733cc | ||
|
|
413a4c289b | ||
|
|
4d6cb8814e | ||
|
|
7148aaf5d0 | ||
|
|
28d07a02e4 | ||
|
|
2c963054f8 | ||
|
|
11b0a34074 | ||
|
|
c772dffc9f | ||
|
|
a4d62ba36a | ||
|
|
c472d6107e | ||
|
|
39e21ea51c | ||
|
|
2da3b1e60b | ||
|
|
62c010283d | ||
|
|
459085184c | ||
|
|
2b4f47db9c | ||
|
|
33d83f3615 | ||
|
|
ff7c2e41de | ||
|
|
6886bba988 | ||
|
|
103e1c2431 | ||
|
|
4e2e67fd50 | ||
|
|
a56eccbbfc | ||
|
|
20c0324e9c | ||
|
|
cf7a40b08a | ||
|
|
90dbd71c13 | ||
|
|
3b5823c74d | ||
|
|
bde97b988a | ||
|
|
53d1174aa9 | ||
|
|
ddef5ea126 | ||
|
|
b6ee0585bd | ||
|
|
4f973eb657 | ||
|
|
4cab2cfa34 | ||
|
|
b6d4a4c6d8 | ||
|
|
d155b318d2 | ||
|
|
a2ed7f437c | ||
|
|
c456d17daf | ||
|
|
09489499e7 | ||
|
|
4da05fa0ae | ||
|
|
8cedf3ce95 | ||
|
|
baa55fb69e | ||
|
|
002a44ac1a | ||
|
|
62b4b72fe4 | ||
|
|
b49a30a972 | ||
|
|
4624d6035e | ||
|
|
d5cc794598 | ||
|
|
5989637f37 | ||
|
|
016c089f13 | ||
|
|
e5991af629 | ||
|
|
17bb9a7eb9 | ||
|
|
a5ea22d468 | ||
|
|
7e3b14fe78 | ||
|
|
532fcc997a | ||
|
|
0b3389bcd2 | ||
|
|
0d7f0febf4 | ||
|
|
b7cb37b189 | ||
|
|
a01097d60b | ||
|
|
f3049d0b81 | ||
|
|
a887efa07a | ||
|
|
9158ad1abb | ||
|
|
b5f0d73ea3 | ||
|
|
ed88720952 | ||
|
|
f0979afdb0 | ||
|
|
bf0d59ed30 | ||
|
|
c2d08ca62a | ||
|
|
4019b48aaa | ||
|
|
294dbd712f | ||
|
|
1af188209a | ||
|
|
8cd34dfe95 | ||
|
|
d2caa5351a | ||
|
|
fb8d2862c1 | ||
|
|
8ad2d2d1cb | ||
|
|
f26a3df1bf | ||
|
|
19fa3731ae | ||
|
|
465acb0c6a | ||
|
|
64afbe6ccd | ||
|
|
04192ee05b | ||
|
|
8fb79eeea4 | ||
|
|
ce9e2f84ad | ||
|
|
304343f4d7 | ||
|
|
af812b68dd | ||
|
|
d85ce8d89b | ||
|
|
f53bae0c19 | ||
|
|
77c5db5977 | ||
|
|
81682d0f82 | ||
|
|
87311d1b8c | ||
|
|
f0dd6d4cbd | ||
|
|
ca041d5526 | ||
|
|
716e426933 | ||
|
|
e8b2f6f8a1 | ||
|
|
dfc74c30c9 | ||
|
|
28ef344077 | ||
|
|
2ef182ee93 | ||
|
|
b5770f8947 | ||
|
|
a7dcbfe430 | ||
|
|
1a3255b507 | ||
|
|
fb47c3cfbe | ||
|
|
65e69dec8b | ||
|
|
c3e2600c67 | ||
|
|
400894616d | ||
|
|
c0a975cc2e | ||
|
|
12b83f1a0d | ||
|
|
00ab882ed6 | ||
|
|
41938afed8 | ||
|
|
1a60545626 | ||
|
|
ac78e60de6 | ||
|
|
bd1236c0ee | ||
|
|
ddf7979531 | ||
|
|
0862fed2a8 | ||
|
|
80a61330ee | ||
|
|
67362a9a03 | ||
|
|
480d720388 | ||
|
|
901f56fa63 | ||
|
|
9beaedd164 | ||
|
|
2124f668db | ||
|
|
11374a77ef | ||
|
|
f0dd568e16 | ||
|
|
b5f55a1d85 | ||
|
|
5130d80d79 | ||
|
|
6825eef955 | ||
|
|
6924852592 | ||
|
|
f043b14bc0 | ||
|
|
9c72011fd7 | ||
|
|
2f556e0c55 | ||
|
|
7fa1363fb0 | ||
|
|
275dab6b55 | ||
|
|
a68abc79fd | ||
|
|
653533a3da | ||
|
|
9bf61ef97b | ||
|
|
0e58d19163 | ||
|
|
18968efa0a | ||
|
|
eb928c9f52 | ||
|
|
9d112f4440 | ||
|
|
ad460a8315 | ||
|
|
bf628cf6dd | ||
|
|
9e5a353663 | ||
|
|
6f6ebd216d | ||
|
|
73513ececc | ||
|
|
1f24c2e589 | ||
|
|
22049ea700 | ||
|
|
050ebccf30 | ||
|
|
d88e20cdb9 | ||
|
|
eceb554a2f | ||
|
|
b849a64f8d | ||
|
|
0460406298 | ||
|
|
9a3cd1c00d | ||
|
|
fb7def3344 | ||
|
|
f13890ddce | ||
|
|
aaa749d366 | ||
|
|
bc42ca121f | ||
|
|
cee69441d3 | ||
|
|
b5209c5744 | ||
|
|
66da8f60d0 | ||
|
|
f0583f65e1 | ||
|
|
44c9102e7a | ||
|
|
6a7cf6b41f | ||
|
|
2acee97c2b | ||
|
|
7f7ec84d6f | ||
|
|
cebde85b94 | ||
|
|
f00f8346f1 | ||
|
|
83f119a84a | ||
|
|
9d0326baa6 | ||
|
|
186f61a3ac | ||
|
|
fe9bac3749 | ||
|
|
6c01ceb8d0 | ||
|
|
2eda996a63 | ||
|
|
4706f3964d | ||
|
|
4df76b0a5d | ||
|
|
a005b7269a | ||
|
|
261ccd7f5f | ||
|
|
942e39e87c | ||
|
|
9c5fc81c2d | ||
|
|
fd2c07bfed | ||
|
|
405f8c4796 | ||
|
|
c42ed47660 | ||
|
|
1a87f5f26c | ||
|
|
a3dc31cab9 | ||
|
|
4dd47236e7 | ||
|
|
295b400d57 | ||
|
|
716cf144ec | ||
|
|
1e365e88bd | ||
|
|
2d41dc0069 | ||
|
|
f7f07dc517 | ||
|
|
b8690dd840 | ||
|
|
378a0f7a79 | ||
|
|
da84946de4 | ||
|
|
63a7b3ad1e | ||
|
|
5730b20c6d | ||
|
|
8047fd2434 | ||
|
|
3bbd0d0e09 | ||
|
|
9dda396baa | ||
|
|
13ed3b9985 | ||
|
|
bd2cf9d4bf | ||
|
|
d4902a7ad0 | ||
|
|
55bf90b9e4 | ||
|
|
53f0bf85d7 | ||
|
|
1c3d844e73 | ||
|
|
0d7d9c37b6 | ||
|
|
d8866d7277 | ||
|
|
2ef2f6d593 | ||
|
|
3483b78d1a | ||
|
|
d3ded420b1 | ||
|
|
22716774d5 | ||
|
|
5044e6c544 | ||
|
|
09e23334de | ||
|
|
02410e9239 | ||
|
|
e552b78d50 | ||
|
|
fde0da6f19 | ||
|
|
3f04a08a0c | ||
|
|
4bbfbf898e | ||
|
|
6e17463228 | ||
|
|
522f285f9b | ||
|
|
b8d49be5a1 | ||
|
|
90abdaf3bc | ||
|
|
b579a8ea18 | ||
|
|
53ef3a0bfe | ||
|
|
d70c847b4f | ||
|
|
d15f166093 | ||
|
|
ca580ef862 | ||
|
|
45bac68064 | ||
|
|
784aaa53df | ||
|
|
8355b4d074 | ||
|
|
a7b65bdedf | ||
|
|
d94590ed48 | ||
|
|
afbd3b2fc4 | ||
|
|
79e37a7ecb | ||
|
|
0f118e55db | ||
|
|
2f54522d44 | ||
|
|
dd74436ffd | ||
|
|
11f51e6ded | ||
|
|
086df80790 | ||
|
|
291e942332 | ||
|
|
31ade3b3e9 | ||
|
|
36b3b75b21 | ||
|
|
6d1dea337b | ||
|
|
99eb1172b0 | ||
|
|
6cb3212fc2 | ||
|
|
554c63ca60 | ||
|
|
fff7905409 | ||
|
|
00dd207f60 | ||
|
|
e417469af2 | ||
|
|
cb7dac3a5d | ||
|
|
2651fd5e24 | ||
|
|
764856777c | ||
|
|
e7b25a649c | ||
|
|
804b732aab | ||
|
|
45fffe8cbe | ||
|
|
9ba3c1ede4 | ||
|
|
a0bebeda8b | ||
|
|
27e093cbc1 | ||
|
|
d9f60e8dc8 | ||
|
|
0e42dfbe22 | ||
|
|
5ebd33302f | ||
|
|
17167898c8 | ||
|
|
6eadbfbea0 | ||
|
|
1a9a9abcc7 | ||
|
|
74b7de83ec | ||
|
|
36317f3dad | ||
|
|
052ac0c8d0 | ||
|
|
49a2c10279 | ||
|
|
5d53c14342 | ||
|
|
4752a990c8 | ||
|
|
106a3051b8 | ||
|
|
284f55a7fb | ||
|
|
1ce1509989 | ||
|
|
8bb85c8c5a | ||
|
|
c8135f808b | ||
|
|
b21d015c55 | ||
|
|
e70e8e053e | ||
|
|
1b446a5d85 | ||
|
|
59a0682f3e | ||
|
|
b6adfc59f5 | ||
|
|
254aa3c986 | ||
|
|
f43544eecc | ||
|
|
a04cde613e | ||
|
|
4429e720ae | ||
|
|
ee49098843 | ||
|
|
51f5d36f4f | ||
|
|
f8c2cd129d | ||
|
|
f6d1183fc5 | ||
|
|
2043527b9b | ||
|
|
53447e9cd3 | ||
|
|
d61ce3f670 | ||
|
|
a910984b58 | ||
|
|
e309b1045d | ||
|
|
0180bfe4aa | ||
|
|
1f3d1d85a9 | ||
|
|
39a3340f73 | ||
|
|
ae3bff3491 | ||
|
|
dc085ddf8c | ||
|
|
73d23c6ae8 | ||
|
|
6189d8e54d | ||
|
|
115ef3ddac | ||
|
|
4fb858d90a | ||
|
|
88f1ea36ce | ||
|
|
c2633907c5 | ||
|
|
ebfdd2eb5b | ||
|
|
a551c5dad7 | ||
|
|
27e4b45c06 | ||
|
|
ac5f2bf9db | ||
|
|
80a167b1f0 | ||
|
|
7ae8afb7ef | ||
|
|
9118a92862 | ||
|
|
8eca5bd50a | ||
|
|
e01b825cc9 | ||
|
|
ab45e12d31 | ||
|
|
f407cbd2f1 | ||
|
|
227f8ef031 | ||
|
|
2bc60c55af | ||
|
|
20814fabdd | ||
|
|
9084cdd70f | ||
|
|
5b731178b2 | ||
|
|
3a653515ec | ||
|
|
aa729349dd | ||
|
|
5b1631a4a9 | ||
|
|
291cba284b | ||
|
|
253f76a0a5 | ||
|
|
6837c5edab | ||
|
|
7223129916 | ||
|
|
5ae4a84211 | ||
|
|
118a760719 | ||
|
|
19505e0392 | ||
|
|
df431b127b | ||
|
|
882ac83d8d | ||
|
|
d3e09f12d0 | ||
|
|
677be13ffc | ||
|
|
350b88656a | ||
|
|
9de94d5a4d | ||
|
|
2b7120e233 | ||
|
|
8b256a7296 | ||
|
|
62ccc6d95f | ||
|
|
01858bcbf2 | ||
|
|
2aeee2a905 | ||
|
|
5e7883ec19 | ||
|
|
c6a03c46e6 | ||
|
|
e4c65b338d | ||
|
|
99914ec9f8 | ||
|
|
ef910a0358 | ||
|
|
591c4bf223 | ||
|
|
e1150cac4b | ||
|
|
165eb2dbe6 | ||
|
|
880fb46de0 | ||
|
|
9396723995 | ||
|
|
65878a2319 | ||
|
|
ad31fa3040 | ||
|
|
4d1b6f4ad1 | ||
|
|
0b0033c40b | ||
|
|
755def8083 | ||
|
|
1e90715a3d | ||
|
|
131bdf9bb1 | ||
|
|
10f1bdb9a2 | ||
|
|
d5cea26d45 | ||
|
|
c71176858b | ||
|
|
f8bd4de87d | ||
|
|
c3b37abdfd | ||
|
|
6c74fd62a0 | ||
|
|
9ff7f66a2b | ||
|
|
70f272f71c | ||
|
|
8763dd80ef | ||
|
|
807229f2f2 | ||
|
|
acb12cc811 | ||
|
|
d62dee7eae | ||
|
|
0f29cfabc3 | ||
|
|
e275a9c0d9 | ||
|
|
aa32bd38e4 | ||
|
|
372d4c6d7b | ||
|
|
575ec91d82 | ||
|
|
10be983f2c | ||
|
|
415b158ce2 | ||
|
|
de01438a57 | ||
|
|
a2c4f3f150 | ||
|
|
0a4330cd5d | ||
|
|
47ec693e29 | ||
|
|
1d566edb81 | ||
|
|
6e1ad283cf | ||
|
|
ef3d8754f5 | ||
|
|
142934084a | ||
|
|
96c5b9f87c | ||
|
|
7cd6a6f6cf | ||
|
|
c5d1b4986b | ||
|
|
7f4105a5c9 | ||
|
|
f4d58deba1 | ||
|
|
386b7330d2 | ||
|
|
0ad1c67234 | ||
|
|
7d6a1dae31 | ||
|
|
656223fbd3 | ||
|
|
67800f7626 | ||
|
|
2f7f8e1c2b | ||
|
|
4770cec7bc | ||
|
|
e1e9f0c5b2 | ||
|
|
ab78a8926e | ||
|
|
f6f902d459 | ||
|
|
cdb3757942 | ||
|
|
92e1c8983d | ||
|
|
0c894e1ebd | ||
|
|
084c365c3a | ||
|
|
c37a6e151f | ||
|
|
36ea26c5c0 | ||
|
|
7c549dd557 | ||
|
|
899d4675dd | ||
|
|
243c56e725 | ||
|
|
3edd2d5c93 | ||
|
|
47fb089eb5 | ||
|
|
4f1d984e56 | ||
|
|
5e0c533672 | ||
|
|
968b01a91a | ||
|
|
4071f29653 | ||
|
|
f1b83d88a3 | ||
|
|
a988361aea | ||
|
|
8888982db3 | ||
|
|
9af432257d | ||
|
|
cf706cc6ef | ||
|
|
5971d240d4 | ||
|
|
ca4f458787 | ||
|
|
df6db5c802 | ||
|
|
6edff11a88 | ||
|
|
63878c0379 | ||
|
|
02590c3e1d | ||
|
|
619a21812b | ||
|
|
fec4485e28 | ||
|
|
409bcc76bd | ||
|
|
cffe6057fb | ||
|
|
80fd2b574c | ||
|
|
e122685978 | ||
|
|
54ef09f860 | ||
|
|
d7b3ac46f8 | ||
|
|
4429e4bf24 | ||
|
|
ec07dba29e | ||
|
|
c167cbc9fd | ||
|
|
a6fb2aa2a5 | ||
|
|
1fce36b111 | ||
|
|
8b28209c60 | ||
|
|
30c72d377e | ||
|
|
e4eddf9b36 | ||
|
|
c1779a79bc | ||
|
|
74850d7f75 | ||
|
|
07a1223156 | ||
|
|
0d31ad5101 | ||
|
|
a0dfffb33c | ||
|
|
6e5ac4a28f | ||
|
|
8022b27fc2 | ||
|
|
95dedb866f | ||
|
|
78672a9fd5 | ||
|
|
da6a7bbdde | ||
|
|
2551b6645d | ||
|
|
5e4ba463b7 | ||
|
|
51da995806 | ||
|
|
5002056b16 | ||
|
|
5c75adff95 | ||
|
|
367382b575 | ||
|
|
4df11b5039 | ||
|
|
84e6b4001f | ||
|
|
17653a5dfe | ||
|
|
e269c511f6 | ||
|
|
5e3b254dc8 | ||
|
|
d244fa9741 | ||
|
|
e89ca34e0e | ||
|
|
79b7154454 | ||
|
|
4ef556f650 | ||
|
|
b036596b75 | ||
|
|
cd525c0f5a | ||
|
|
3c224f4d0e | ||
|
|
d38862a080 | ||
|
|
2640d6718d | ||
|
|
de87541862 | ||
|
|
22d2f498fa | ||
|
|
d79ffa1898 | ||
|
|
2236ef6c92 | ||
|
|
da1aa07db5 | ||
|
|
4ac1941592 | ||
|
|
476899295f | ||
|
|
fca28d243e | ||
|
|
37feb4031f | ||
|
|
0cd1401f8d | ||
|
|
724bb1e7d9 | ||
|
|
1c7912751e | ||
|
|
9d36eb4eab | ||
|
|
b0f71db3ff | ||
|
|
84e1cacea4 | ||
|
|
6538d445e8 | ||
|
|
52f98f8a5b | ||
|
|
22a7ba8b22 | ||
|
|
3a42f32134 | ||
|
|
4fa0f53521 | ||
|
|
326121aec4 | ||
|
|
9a9386226a | ||
|
|
126d562576 | ||
|
|
f08c33e834 | ||
|
|
45543028bb | ||
|
|
f683b5de47 | ||
|
|
db0dca2f6f | ||
|
|
89c0cd4acc | ||
|
|
6101ce427a | ||
|
|
5fe26a9b5c | ||
|
|
35698484a5 | ||
|
|
63562f6d5a | ||
|
|
a151693a3b | ||
|
|
ac29318b84 | ||
|
|
dfa98f911b | ||
|
|
4605953b0f | ||
|
|
ef8e8ebd91 | ||
|
|
3188e94ac4 | ||
|
|
97a64f3ebe | ||
|
|
b850c9fa04 | ||
|
|
4a7a4a5b6c | ||
|
|
771fc05d30 | ||
|
|
938939fd89 | ||
|
|
028a570e17 | ||
|
|
0e4393652f | ||
|
|
b994fb2b96 | ||
|
|
f10fd8a470 | ||
|
|
3c11c9c122 | ||
|
|
673375fe2d | ||
|
|
3c92231094 | ||
|
|
119e5d7702 | ||
|
|
271ee604f8 | ||
|
|
04c01882fc | ||
|
|
f4664a6cbd | ||
|
|
ecb26beda5 | ||
|
|
0c4ac271ca | ||
|
|
0cf7e480b4 | ||
|
|
ed2584050f | ||
|
|
977338a7af | ||
|
|
31049c4d72 | ||
|
|
deb0237166 | ||
|
|
e45b05647e | ||
|
|
3d5a955e08 | ||
|
|
d18f37e026 | ||
|
|
9951542393 | ||
|
|
041b6cba61 | ||
|
|
63075118a5 | ||
|
|
531d7955fd | ||
|
|
bfa4a7f8b0 | ||
|
|
d0fece8d3c | ||
|
|
bdcd7693c8 | ||
|
|
43c2e8deae | ||
|
|
1692dc019d | ||
|
|
a9aea68fd5 | ||
|
|
261d809a47 | ||
|
|
d9cc5de9e5 | ||
|
|
b8940cd902 | ||
|
|
1942382246 | ||
|
|
eb9bd2d949 | ||
|
|
2d386d7038 | ||
|
|
4ac2823b3c | ||
|
|
22c7c5eb8f | ||
|
|
42c12c04f6 | ||
|
|
adb5b76ff5 | ||
|
|
3bcdf3664c | ||
|
|
9eeb03c0dd | ||
|
|
32937f3ea0 | ||
|
|
7b50769eb9 | ||
|
|
7693f24792 | ||
|
|
46a65c282f | ||
|
|
92b20713d7 | ||
|
|
da4ed08739 | ||
|
|
9060dc6b59 | ||
|
|
1fae1b3166 | ||
|
|
80b4119279 | ||
|
|
4011cf1c42 | ||
|
|
657298cebd | ||
|
|
fabb7acd45 | ||
|
|
23c639ff32 | ||
|
|
8be5284e91 | ||
|
|
503e4d3d52 | ||
|
|
00718ae7a9 | ||
|
|
0465560c1a | ||
|
|
61d05daab1 | ||
|
|
6ead27ddda | ||
|
|
50c87b8eed | ||
|
|
345995fcde | ||
|
|
62cebee8ee | ||
|
|
95cbfee8ae | ||
|
|
4ad8350607 | ||
|
|
6ea9cf58be | ||
|
|
f383d5a801 | ||
|
|
c95480963e | ||
|
|
069296dbb0 | ||
|
|
2d4d2bbae4 | ||
|
|
2f1348f339 | ||
|
|
69d4063651 | ||
|
|
5b02f33451 | ||
|
|
054aa0d58c | ||
|
|
3c4c229788 | ||
|
|
74aaacf82a | ||
|
|
29400b45b9 | ||
|
|
c28f1d16f0 | ||
|
|
265f30bd3f | ||
|
|
c9e62927f2 | ||
|
|
2366d28780 | ||
|
|
d89a9f7283 | ||
|
|
1aa11cf7ce | ||
|
|
0c1b7f843b | ||
|
|
4b46fbec5b | ||
|
|
1d7702833d | ||
|
|
6b69ddd17a | ||
|
|
d624e2a638 | ||
|
|
b1ca784aca | ||
|
|
4a9dc5b2f5 | ||
|
|
0ade2712d1 | ||
|
|
50f96f256f | ||
|
|
d2d61a8288 | ||
|
|
3e71d13acf | ||
|
|
e7a6edb0ee | ||
|
|
c27d6ad6b5 | ||
|
|
46daf2d200 | ||
|
|
3864b3a8e6 | ||
|
|
0618978238 | ||
|
|
09177f4f2e | ||
|
|
472be88674 | ||
|
|
a6e62cf6d0 | ||
|
|
12d381bd5d | ||
|
|
f8c30faf25 | ||
|
|
61cd5d9045 | ||
|
|
fb95035a65 | ||
|
|
4669def000 | ||
|
|
0337eaf321 | ||
|
|
884fb88e28 | ||
|
|
d76c058eea | ||
|
|
9927170787 | ||
|
|
109c8aafd2 | ||
|
|
b7788f80a3 | ||
|
|
c8ed9bd278 | ||
|
|
f2d90d5c02 | ||
|
|
845b0b2c97 | ||
|
|
c0036ced54 | ||
|
|
970a9b9d2b | ||
|
|
64991b0c8b | ||
|
|
e26a3d8d9e | ||
|
|
1319905d7a | ||
|
|
a9549fdce3 | ||
|
|
4ad8b45155 | ||
|
|
19167fd21f | ||
|
|
9c4ea42e79 | ||
|
|
74874ffda7 | ||
|
|
cd0864121b | ||
|
|
4932a7e2d9 | ||
|
|
0baf923153 | ||
|
|
9894da6a29 | ||
|
|
46d200a3a1 | ||
|
|
72443572bf | ||
|
|
a08bf11138 | ||
|
|
204132a998 | ||
|
|
f4c9ebbc34 | ||
|
|
45278eaa19 | ||
|
|
478e511db0 | ||
|
|
68c0603946 | ||
|
|
e3005d3ddb | ||
|
|
cc5d68f4c4 | ||
|
|
cc52f02d74 | ||
|
|
3151afee9e | ||
|
|
f41a9a1ffc | ||
|
|
1783c7ca92 | ||
|
|
0126ef7f3c | ||
|
|
d98edb548a | ||
|
|
9fbcf19188 | ||
|
|
073b891ec1 | ||
|
|
327ca883ec | ||
|
|
a1d4813a54 | ||
|
|
18f8247701 | ||
|
|
af27b84ff7 | ||
|
|
4a13ae7201 | ||
|
|
9182f87664 | ||
|
|
55e1bc8920 | ||
|
|
55fcf62e9c | ||
|
|
b96c133034 | ||
|
|
ce8b0b2868 | ||
|
|
252e6f6869 | ||
|
|
1ccaea5b92 | ||
|
|
0bc71103e1 | ||
|
|
f8b865264a | ||
|
|
5b8b1a43bd | ||
|
|
40cbd6b6ee | ||
|
|
4e49f52375 | ||
|
|
38432d8c25 | ||
|
|
42b7139dec | ||
|
|
1ef66cc3bd | ||
|
|
416a3e6c4f | ||
|
|
8558e1ec73 | ||
|
|
56f518d279 | ||
|
|
6f8e2d517e | ||
|
|
1c1d67dfef | ||
|
|
2c70849dc3 | ||
|
|
0a016b0525 | ||
|
|
e701aec2d1 | ||
|
|
412ece18e7 | ||
|
|
1c82fbd2eb | ||
|
|
2732be83d9 | ||
|
|
e4c4664d73 | ||
|
|
03c4f0ed67 | ||
|
|
f1acb9fd40 | ||
|
|
8a5be236e0 | ||
|
|
df75914791 | ||
|
|
b02e1006b9 | ||
|
|
f8152f2708 | ||
|
|
2f475bd5d5 | ||
|
|
a7b51f4539 | ||
|
|
288702170d | ||
|
|
f46eee838a | ||
|
|
a654f3fe49 | ||
|
|
44ccfa6258 | ||
|
|
04d1725752 | ||
|
|
1bac74b9ae | ||
|
|
ed83638668 | ||
|
|
7ac8a60c6f | ||
|
|
c253b14f6e | ||
|
|
bdcb23ca25 | ||
|
|
b2c2dc8940 | ||
|
|
869dc94cbb | ||
|
|
a218619626 | ||
|
|
b1e68add19 | ||
|
|
31e262e6b4 | ||
|
|
eede182df7 | ||
|
|
4e2f8b8722 | ||
|
|
c8c710eca7 | ||
|
|
149ed9f151 | ||
|
|
f7a79a37be | ||
|
|
6532b6e607 | ||
|
|
74270defda | ||
|
|
e1e5e53127 | ||
|
|
b3bda8a75f | ||
|
|
8a785c3006 | ||
|
|
191f7f09ce | ||
|
|
03eb4adc6e | ||
|
|
4bbf7156ef | ||
|
|
6d15401341 | ||
|
|
8c78414284 | ||
|
|
6c99491347 | ||
|
|
0eb61a3d16 | ||
|
|
a2c10d37d7 | ||
|
|
2e0d9219b9 | ||
|
|
f30d47c876 | ||
|
|
a16eaa0c33 | ||
|
|
f43063158a | ||
|
|
7c50e3b816 | ||
|
|
2808c040ef | ||
|
|
48b6ee2b67 | ||
|
|
bc41f0398f | ||
|
|
d3309933f5 | ||
|
|
b568c0231c | ||
|
|
3a7d7a3f22 | ||
|
|
3ba522bb23 | ||
|
|
6080830bef | ||
|
|
8b183781cb | ||
|
|
1f651ba6ec | ||
|
|
812a99100b | ||
|
|
1967650bc4 | ||
|
|
1ebff9736b | ||
|
|
24d21887ed | ||
|
|
db8d4e8dd6 | ||
|
|
2f9157b427 | ||
|
|
91c8f828e1 | ||
|
|
8db6832db8 | ||
|
|
117f35ac4a | ||
|
|
4eea5cf6c2 | ||
|
|
f96ab9d18d | ||
|
|
865398b4a9 | ||
|
|
e3417bbbe0 | ||
|
|
2492efb162 | ||
|
|
4a5990ff8f | ||
|
|
5e7a90316d | ||
|
|
231498ac45 | ||
|
|
fd4fa9097f | ||
|
|
0b1a8500a2 | ||
|
|
cb03fafdf1 | ||
|
|
bf5e54f255 | ||
|
|
94e1e58b4d | ||
|
|
ced39d019f | ||
|
|
16dcdedc8a | ||
|
|
83b554437e | ||
|
|
dfc46c6220 | ||
|
|
6ba2e3df4e | ||
|
|
427bcb7608 | ||
|
|
0ec346d942 | ||
|
|
1352ae2c07 | ||
|
|
4cd5fb13a3 | ||
|
|
ea1776f556 | ||
|
|
e1c0970c11 | ||
|
|
b8092fbc82 | ||
|
|
bc9e69e160 | ||
|
|
f2cf37518b | ||
|
|
04c7f3576e | ||
|
|
0268d40281 | ||
|
|
399b5add58 | ||
|
|
e6e130b9ba | ||
|
|
766bd8e880 | ||
|
|
ffad75bd62 | ||
|
|
a429515bdd | ||
|
|
8d761134c2 | ||
|
|
cf04cedf21 | ||
|
|
5b31afcbd1 | ||
|
|
6e91f14d09 | ||
|
|
ed26e4012b | ||
|
|
806f380a8b | ||
|
|
a19b739909 | ||
|
|
a5c72780e6 | ||
|
|
e19f794fee | ||
|
|
d5ff9effcf | ||
|
|
e845434028 | ||
|
|
4af32a2817 | ||
|
|
bc6cef823f | ||
|
|
1ec6fa98c9 | ||
|
|
25d2914fba | ||
|
|
cce5d057d3 | ||
|
|
6606f7c659 | ||
|
|
a971fa9d58 | ||
|
|
ded4128965 | ||
|
|
f9e12f79ca | ||
|
|
c756dfeb14 | ||
|
|
63677d1f47 | ||
|
|
32e14d8181 | ||
|
|
4847a9534d | ||
|
|
88cb06e996 | ||
|
|
d488463fa3 | ||
|
|
127fad17dd | ||
|
|
5a95cd4442 | ||
|
|
58d8339966 | ||
|
|
be7ead6946 | ||
|
|
3cbc286d06 | ||
|
|
3c741682e5 | ||
|
|
86fc9b617c | ||
|
|
90bcb86957 | ||
|
|
1bede47843 | ||
|
|
93937b2b31 | ||
|
|
c5365dee56 | ||
|
|
4103b1c470 | ||
|
|
4d5b098626 | ||
|
|
7ed2ec3061 | ||
|
|
ce797ad373 | ||
|
|
7e863c51e6 | ||
|
|
0f12772e32 | ||
|
|
d5d4281647 | ||
|
|
cda4a6f93f | ||
|
|
e2722f58ee | ||
|
|
a1665c5094 | ||
|
|
2ded344620 | ||
|
|
9707acfc40 | ||
|
|
8bf285e082 | ||
|
|
cf59d68b17 | ||
|
|
1280a47fc6 | ||
|
|
23d285ad57 | ||
|
|
8ad0f4912e | ||
|
|
6f9dea7483 | ||
|
|
22d7a59306 | ||
|
|
279a547a8b | ||
|
|
83f5125d52 | ||
|
|
a43b40449b | ||
|
|
9cef051ce2 | ||
|
|
0575840866 | ||
|
|
45131b2bca | ||
|
|
ccda401dbf | ||
|
|
5b89052d2f | ||
|
|
3887350e47 | ||
|
|
19234cc6c3 | ||
|
|
e8f1521605 | ||
|
|
605941ee26 | ||
|
|
5bc41fe9f8 | ||
|
|
638be5a6b9 | ||
|
|
830d07db82 | ||
|
|
65f5e4e3e4 | ||
|
|
07d4041709 | ||
|
|
c1b34af441 | ||
|
|
9a05795619 | ||
|
|
24d8134ac1 | ||
|
|
7f911ef4e3 | ||
|
|
d5e7e6b9b6 | ||
|
|
0775c62469 | ||
|
|
38928c6609 | ||
|
|
a2a93a4fa7 | ||
|
|
4fe95094d1 | ||
|
|
ae8ff92e05 | ||
|
|
49d6aa1394 | ||
|
|
0bfa78b39b | ||
|
|
6bc9edd8b2 | ||
|
|
05a35d62b6 | ||
|
|
8574bf62dc | ||
|
|
0af5f5efaf | ||
|
|
c8d3f6486d | ||
|
|
304111afd0 | ||
|
|
d0e444a648 | ||
|
|
65fd446b4d | ||
|
|
364c7f92b4 | ||
|
|
4eb6d66b45 | ||
|
|
6b59650753 | ||
|
|
41cd778d66 | ||
|
|
70a84f17f3 | ||
|
|
779f7b0f44 | ||
|
|
ef1e019840 | ||
|
|
5583e29513 | ||
|
|
c5bf0343e8 | ||
|
|
e24c32e6f3 | ||
|
|
e9c908ebc0 | ||
|
|
9236136f3a | ||
|
|
813e54bd5b | ||
|
|
80a620a83a | ||
|
|
9fa8bda099 | ||
|
|
f129ee1e18 | ||
|
|
09cbff174a | ||
|
|
d18e7779ca | ||
|
|
5e88a09a42 | ||
|
|
cf1fa59f4b | ||
|
|
3470cb36a8 | ||
|
|
c217504949 | ||
|
|
b59aa74556 | ||
|
|
d33ae65efc | ||
|
|
9f642a93ec | ||
|
|
e7887e37a8 | ||
|
|
af853a4cdb | ||
|
|
4891c4ff72 | ||
|
|
46183cc69f | ||
|
|
59bf16eddc | ||
|
|
9a506a191a | ||
|
|
8675ea03de | ||
|
|
8366fde82f | ||
|
|
3e420aebd8 | ||
|
|
ff1fa0fbf8 | ||
|
|
abcd03af02 | ||
|
|
5116946ae9 | ||
|
|
6f4f7e4e22 | ||
|
|
a32e876ef4 | ||
|
|
a198894bf7 | ||
|
|
5b999e206e | ||
|
|
32206dde3f | ||
|
|
4edcbcee3b | ||
|
|
953e40f9dc | ||
|
|
df4c12c762 | ||
|
|
c1a256cc4c | ||
|
|
f173d40a32 | ||
|
|
1b988b051b | ||
|
|
033a517feb | ||
|
|
9ba6487b3f | ||
|
|
d6b3ea75d4 | ||
|
|
7ab9f91a60 | ||
|
|
0e8f5095c7 | ||
|
|
ce2766d19c | ||
|
|
438a21c87b | ||
|
|
9aa0224cdf | ||
|
|
c7023f2155 | ||
|
|
0ba393924a | ||
|
|
f488293d96 | ||
|
|
1aa44939fc | ||
|
|
5a447098dd | ||
|
|
9e98f1022a | ||
|
|
9115421ace | ||
|
|
d19e79ecc9 | ||
|
|
ed008e85a8 | ||
|
|
6e7131f02f | ||
|
|
9a7f496298 | ||
|
|
78adccfaf4 | ||
|
|
d98660a60d | ||
|
|
d5272b1d2c | ||
|
|
278149f533 | ||
|
|
72d8406409 | ||
|
|
a63b4f7101 | ||
|
|
0f86312c4c | ||
|
|
b1022ed8b5 | ||
|
|
f6583796fe | ||
|
|
4848fdbf59 | ||
|
|
80cd08c190 | ||
|
|
9517f4da4d | ||
|
|
dc0c989ef4 | ||
|
|
ceb61daa70 | ||
|
|
fce0114005 | ||
|
|
7e282a53a5 | ||
|
|
91cb46191d | ||
|
|
87db64b839 | ||
|
|
cb8162d3d1 | ||
|
|
d288d273e1 | ||
|
|
d4f50f3ae5 | ||
|
|
455579ca90 | ||
|
|
0d0610870d | ||
|
|
532ebc4a82 | ||
|
|
56f2d31676 | ||
|
|
c178e4e6ca | ||
|
|
d7a0496f3e | ||
|
|
58ed393235 | ||
|
|
fae059cc18 | ||
|
|
b26d85c30f | ||
|
|
0dcb145c7e | ||
|
|
64cf1483e5 | ||
|
|
d028207a6e | ||
|
|
0a55a2b692 | ||
|
|
6cc046302f | ||
|
|
ed4d44d833 | ||
|
|
f88db7ac0b | ||
|
|
57976f646f | ||
|
|
bb24609158 | ||
|
|
89036579ed | ||
|
|
93978c5e2b | ||
|
|
1489521ee5 | ||
|
|
6d33f97703 | ||
|
|
7564dac8cb | ||
|
|
3f7a31d366 | ||
|
|
be170b1426 | ||
|
|
cd2539ab2a | ||
|
|
f0d6f724a2 | ||
|
|
6df319b6f0 | ||
|
|
f1d2b94e0b | ||
|
|
857810d2dd | ||
|
|
ac8eb0f319 | ||
|
|
d04fa1f712 | ||
|
|
c2c9471cba | ||
|
|
6279285b2a | ||
|
|
8bad40701b | ||
|
|
250e143084 | ||
|
|
b2e6ee5b43 | ||
|
|
c9c444f562 | ||
|
|
835e01fc70 | ||
|
|
f9232c7917 | ||
|
|
e7ce5d8b06 | ||
|
|
758d114cbc | ||
|
|
ea8590cf66 | ||
|
|
ab8229479b | ||
|
|
b677ff6692 | ||
|
|
c8032aec17 | ||
|
|
256fe08963 | ||
|
|
e731d30d90 | ||
|
|
a1abee013c | ||
|
|
98a3825614 | ||
|
|
7393c5ce4c | ||
|
|
598c47a108 | ||
|
|
9266cb0a22 | ||
|
|
bcfce93ccd | ||
|
|
dea236e4fa | ||
|
|
69135f59aa | ||
|
|
58367a9da2 | ||
|
|
58247c8b4b | ||
|
|
f55bd3f94b | ||
|
|
e90002ca1d | ||
|
|
bbb010a30f | ||
|
|
05a056a409 | ||
|
|
0eb7e6b9a8 | ||
|
|
128cf2daf7 | ||
|
|
b98b4c135d | ||
|
|
a2cdd11d4a | ||
|
|
e0214a263b | ||
|
|
e75fa8bbbf | ||
|
|
c782e893ec | ||
|
|
89ac1fa8ba | ||
|
|
2e4f0b2bd7 | ||
|
|
c1cdd7954d | ||
|
|
63cb7ece62 | ||
|
|
493e3fa0ca | ||
|
|
f1fbe3e09f | ||
|
|
642f725fd7 | ||
|
|
cbc0406be8 | ||
|
|
1748605c5d | ||
|
|
4d661ec0f3 | ||
|
|
0e847540c3 | ||
|
|
22b37b75db | ||
|
|
b0cf867319 | ||
|
|
0b96bb793e | ||
|
|
b3a0179d64 | ||
|
|
f9478e475b | ||
|
|
399689dcc7 | ||
|
|
fa319a5786 | ||
|
|
6d146e15df | ||
|
|
25187ab674 | ||
|
|
f52acf3b12 | ||
|
|
a99d6edc05 | ||
|
|
72625f2f4d | ||
|
|
e1a7e3564f | ||
|
|
094803cf82 | ||
|
|
e9c4b0d178 | ||
|
|
23ab0c68c2 | ||
|
|
849300bc73 | ||
|
|
8664599af7 | ||
|
|
e02cc249da | ||
|
|
59c448f074 | ||
|
|
d8caa5454d | ||
|
|
b0cdf097f4 | ||
|
|
ce8b5769f7 | ||
|
|
7d72e44eb9 | ||
|
|
c53ec53d80 | ||
|
|
9470412316 | ||
|
|
a594087f06 | ||
|
|
74bc42cfdd | ||
|
|
120b689284 | ||
|
|
e7420a3bef | ||
|
|
e07fc62833 | ||
|
|
5b6e11d560 | ||
|
|
211c14c391 | ||
|
|
ad5701f50f | ||
|
|
c92fdf88a3 | ||
|
|
d33a3b91c3 | ||
|
|
a7a28f85ae | ||
|
|
59a5f012cc | ||
|
|
099e4b88d8 | ||
|
|
cdb2e045ee | ||
|
|
465354ffde | ||
|
|
83b1e7fb3c | ||
|
|
04f8478aaa | ||
|
|
8916acbc13 | ||
|
|
abaf47bbb6 | ||
|
|
045afd6b61 | ||
|
|
98b867f7b7 | ||
|
|
db1fbc6c6f | ||
|
|
e84fe3599b | ||
|
|
c37eceeb9e | ||
|
|
b8a6692657 | ||
|
|
7e0bba555c | ||
|
|
04c9751f24 | ||
|
|
019422ebba | ||
|
|
b98cd03193 | ||
|
|
9fccb0df08 | ||
|
|
21fd84dcb8 | ||
|
|
6d74e46621 | ||
|
|
8e28db5cc9 | ||
|
|
0a60bbf4fa | ||
|
|
d5174065af | ||
|
|
1ead1caa18 | ||
|
|
f31e65ca8b | ||
|
|
1c2dcf762a | ||
|
|
1df3ccf7ee | ||
|
|
118c883429 | ||
|
|
406d32f8b5 | ||
|
|
34ce2ca62f | ||
|
|
4a6afa6abf | ||
|
|
01c099d9ef | ||
|
|
64345b7559 | ||
|
|
5d43eaed61 | ||
|
|
9ccccd4874 | ||
|
|
10766f1e93 | ||
|
|
2602ddc379 | ||
|
|
0354659f9d | ||
|
|
be9dafcd37 | ||
|
|
7d3491c741 | ||
|
|
a2c6f25190 | ||
|
|
96eda876a4 | ||
|
|
f260cb72cd | ||
|
|
e7d7152c3c | ||
|
|
b67765dccf | ||
|
|
2763587acd | ||
|
|
141ec04d19 | ||
|
|
5ecc768970 | ||
|
|
0fbfe1b08a | ||
|
|
369449827d | ||
|
|
c54773473f | ||
|
|
b102a87348 | ||
|
|
cf66ddc1b4 | ||
|
|
c06b45129c | ||
|
|
192e228a98 | ||
|
|
b1491dfd7c | ||
|
|
e49d6b1568 | ||
|
|
657a0d2568 | ||
|
|
3ce8540484 | ||
|
|
e780492ecf | ||
|
|
1487bba226 | ||
|
|
83d31144eb | ||
|
|
d516d68b29 | ||
|
|
130df8fb01 | ||
|
|
d79d91a4a7 | ||
|
|
5eab2549ab | ||
|
|
7644cb79b2 | ||
|
|
ba8ac996f9 | ||
|
|
a901ed16b5 | ||
|
|
0c838f9f5e | ||
|
|
773cb3b688 | ||
|
|
5b5c7a28d6 | ||
|
|
12bcf3d179 | ||
|
|
9708f49abf | ||
|
|
96fee64421 | ||
|
|
39aa968a76 | ||
|
|
6dfd8c73fc | ||
|
|
e319071191 | ||
|
|
9d9d39536b | ||
|
|
ae702d161a | ||
|
|
be09c23ff0 | ||
|
|
dc4b774f1e | ||
|
|
027fd1242c | ||
|
|
590b544f67 | ||
|
|
ed72fc3a50 | ||
|
|
1af1c45dc0 | ||
|
|
d56c01fff4 | ||
|
|
17d319a20d | ||
|
|
92b3dc3219 | ||
|
|
5681264faa | ||
|
|
f701197227 | ||
|
|
2a45f3d448 | ||
|
|
16dd87d848 | ||
|
|
5eefd1f618 | ||
|
|
b4c38738f4 | ||
|
|
640e53935d | ||
|
|
8c8354e85a | ||
|
|
c3530c3fb3 | ||
|
|
811355ccd0 | ||
|
|
82b34e813d | ||
|
|
84a4367657 | ||
|
|
abbee6b29b | ||
|
|
527e0c43a5 | ||
|
|
ede89ae3b4 | ||
|
|
a313e97873 | ||
|
|
da877aad15 | ||
|
|
8d33adfbbb | ||
|
|
6fab7bd2c1 | ||
|
|
09f9e8493c | ||
|
|
3c8bd7809c | ||
|
|
9f03553f48 | ||
|
|
b41dc68773 | ||
|
|
20436cdf75 | ||
|
|
2de5b14fe0 | ||
|
|
8486910b64 | ||
|
|
8ad024ea80 | ||
|
|
b2d2118476 | ||
|
|
fb7b6c4681 | ||
|
|
0a036944bd | ||
|
|
3fce185c77 | ||
|
|
e4f301e7a0 | ||
|
|
4195e55ccc | ||
|
|
c3c01641d2 | ||
|
|
210d3c5d72 | ||
|
|
3077cb2915 | ||
|
|
769f8b58e8 | ||
|
|
33f93d389e | ||
|
|
29481690c5 | ||
|
|
3f6b36d96e | ||
|
|
23d9bd1d74 | ||
|
|
9d9b230501 | ||
|
|
cb97ea3ec2 | ||
|
|
377ae369c1 | ||
|
|
b216b36892 | ||
|
|
3d73383d18 | ||
|
|
ebc4830666 | ||
|
|
2a6dedd7cc | ||
|
|
0554d07082 | ||
|
|
9dc9118e55 | ||
|
|
58ff066064 | ||
|
|
de190e49d5 | ||
|
|
127efeeb68 | ||
|
|
40c9896705 | ||
|
|
16b90764ad | ||
|
|
806a6c886a | ||
|
|
0ebd632d39 | ||
|
|
1cc77145d4 | ||
|
|
cfac3b7873 | ||
|
|
1959088156 | ||
|
|
f0995436e7 | ||
|
|
210ef79100 | ||
|
|
dcec7175dc | ||
|
|
93d90765c4 | ||
|
|
59362454dd | ||
|
|
92478e96d6 | ||
|
|
944003021b | ||
|
|
94fa334b01 | ||
|
|
29267cf9d7 | ||
|
|
978ce87c86 | ||
|
|
2c79c4dc7f | ||
|
|
2b8ca84296 | ||
|
|
2d20466f9a | ||
|
|
a025055643 | ||
|
|
255f989c7b | ||
|
|
e60353c4a0 | ||
|
|
4212e7a049 | ||
|
|
64c23352f9 | ||
|
|
4390a36b6e | ||
|
|
4a39c10eef | ||
|
|
1b4e3b7fa6 | ||
|
|
443ba4eecc | ||
|
|
c0aaf9fe76 | ||
|
|
082c88a4b2 | ||
|
|
2eeb8ec4fa | ||
|
|
9640510de2 | ||
|
|
f53fcbce97 | ||
|
|
27080698e7 | ||
|
|
74048bdd41 | ||
|
|
28d8614f48 | ||
|
|
bd84755e64 | ||
|
|
f30d4d5308 | ||
|
|
e36b18ad5b | ||
|
|
a09e59a698 | ||
|
|
e6363857d0 | ||
|
|
044d813ef7 | ||
|
|
357fba2c24 | ||
|
|
e76d485e29 | ||
|
|
0696dfd94b | ||
|
|
22399d3d8f | ||
|
|
852816befe | ||
|
|
4631b737fd | ||
|
|
e25e0f4da9 | ||
|
|
42b972bccd | ||
|
|
db215b7e00 | ||
|
|
3741c336ff | ||
|
|
596daf6e68 | ||
|
|
b33a4cd6cc | ||
|
|
a87c56c673 | ||
|
|
1f29fafc95 | ||
|
|
7c56210f20 | ||
|
|
7367ca42b5 | ||
|
|
2b45ca1541 | ||
|
|
dc0ee55110 | ||
|
|
0edfecc904 | ||
|
|
2bafeca270 | ||
|
|
e944b767d7 | ||
|
|
15e2d7e387 | ||
|
|
55022d6ca5 | ||
|
|
ebc3db295b | ||
|
|
077d200342 | ||
|
|
0ac2a79faa | ||
|
|
61959928bb | ||
|
|
5f4c28d313 | ||
|
|
0722f982d3 | ||
|
|
81163f822e | ||
|
|
894a89d99b | ||
|
|
939273c4b0 | ||
|
|
c3eb7dd9c5 | ||
|
|
8321e8a2e0 | ||
|
|
63c1f4fa98 | ||
|
|
b457f1677c | ||
|
|
faf4f67847 | ||
|
|
7025781df8 | ||
|
|
142f1263f6 | ||
|
|
6311ae8968 | ||
|
|
3f1871021e | ||
|
|
b6771037a6 | ||
|
|
5b753d472b | ||
|
|
1df8bad63e | ||
|
|
5358966a87 | ||
|
|
aa577df064 | ||
|
|
d122e215ff | ||
|
|
a7925259a1 | ||
|
|
7d304ae11c | ||
|
|
d4952e6849 | ||
|
|
446ef58992 | ||
|
|
cc3d3babb0 | ||
|
|
6375bd3e33 | ||
|
|
2462aacd77 | ||
|
|
b68e4a729f | ||
|
|
47d3ff4cf8 | ||
|
|
36e144091b | ||
|
|
b17bd31da0 | ||
|
|
5806d52423 | ||
|
|
87e9aeb914 | ||
|
|
7e9d59f3b4 | ||
|
|
cedad8fbd6 | ||
|
|
65ca713ff5 | ||
|
|
5e24471469 | ||
|
|
e482541e1d | ||
|
|
0db52d43fa | ||
|
|
859fbd4423 | ||
|
|
1be67eca8a | ||
|
|
2635d4e634 | ||
|
|
fe672a04f7 | ||
|
|
08f804208b | ||
|
|
ec847059f3 | ||
|
|
4fd176a41d | ||
|
|
d77912ff44 | ||
|
|
9371019133 | ||
|
|
649dc8a7e2 | ||
|
|
c8436b38a0 | ||
|
|
f91263b1e0 | ||
|
|
1177245e86 | ||
|
|
20e3172f38 | ||
|
|
58554fa658 | ||
|
|
2c29ed3e84 | ||
|
|
2b8f1a956c | ||
|
|
5025305fb2 | ||
|
|
1a989c436c | ||
|
|
964bb43fbe | ||
|
|
e7e20417ca | ||
|
|
8b919c00f3 | ||
|
|
676e8ee78a | ||
|
|
08e70231c9 | ||
|
|
0647e27a41 | ||
|
|
fa6c93bd26 | ||
|
|
c02da58a9d | ||
|
|
472734a8cc | ||
|
|
4de93001bf | ||
|
|
659ead082f | ||
|
|
c82e26ad4b | ||
|
|
47281f8fa4 | ||
|
|
02bfa889de | ||
|
|
c37e7e1774 | ||
|
|
c2b1dbd84c | ||
|
|
ea1d6c16cd | ||
|
|
72a4de2ce6 | ||
|
|
0194e71e99 | ||
|
|
baa5b9a975 | ||
|
|
bfffd2e108 | ||
|
|
2674aeb96a | ||
|
|
91fc5eef1d | ||
|
|
6138584651 | ||
|
|
a5ad6f862c | ||
|
|
8a59915d7d | ||
|
|
0421eb84ac | ||
|
|
6dd5c95841 | ||
|
|
b99a33f283 | ||
|
|
2b8903ce2f | ||
|
|
5f68529036 | ||
|
|
d502013c6e | ||
|
|
64def4f953 | ||
|
|
a78838c5ba | ||
|
|
8d5cce62ab | ||
|
|
650dc7f0f9 | ||
|
|
be26697b29 | ||
|
|
b11a6e1c3c | ||
|
|
0d872f5aa6 | ||
|
|
fa662b52d0 | ||
|
|
183b3d4e47 | ||
|
|
49eb11530c | ||
|
|
0546126cc5 | ||
|
|
2aa87305c0 | ||
|
|
e441c10a73 | ||
|
|
8c652a2b5f | ||
|
|
6375abcdac | ||
|
|
c09493d7aa | ||
|
|
74626a8de4 | ||
|
|
55e0916ffc | ||
|
|
f22646efcc | ||
|
|
16c6b860ac | ||
|
|
789251afa7 | ||
|
|
38df10b99e | ||
|
|
93d07c87dc | ||
|
|
5f6e6530d0 | ||
|
|
29805213d1 | ||
|
|
860b1b4841 | ||
|
|
58d848adc0 | ||
|
|
963256638d | ||
|
|
92d850fc87 | ||
|
|
a268c31737 | ||
|
|
48fbe79f71 | ||
|
|
6b186a57ba | ||
|
|
717687e1fc | ||
|
|
1ed836cc2b | ||
|
|
49fb1067ec | ||
|
|
df29666d3c | ||
|
|
b656081c99 | ||
|
|
959d77fc7a | ||
|
|
a566ed2f0e | ||
|
|
596728698f | ||
|
|
e76c24d6e6 | ||
|
|
28b468b292 | ||
|
|
2ad2b0dcca | ||
|
|
74f49b872e | ||
|
|
7fedc36ccc | ||
|
|
b7df941589 | ||
|
|
83d41f25d8 | ||
|
|
ff2a2ae56e | ||
|
|
8bbdf32849 | ||
|
|
2bf0e85f3d | ||
|
|
e9e54449f5 | ||
|
|
af89456c3c | ||
|
|
c52e8d395b | ||
|
|
021d93db11 | ||
|
|
54fdbc7e50 | ||
|
|
a793a0b810 | ||
|
|
42bc56dad3 | ||
|
|
9c24cff6ef | ||
|
|
7eef84a95b | ||
|
|
76935078d1 | ||
|
|
ed877d6585 | ||
|
|
ef276e8770 | ||
|
|
cb43fbeeb4 | ||
|
|
f2fdcb7c4b | ||
|
|
f518324426 | ||
|
|
b164e0896c | ||
|
|
7f47ba7383 | ||
|
|
41a9a76a99 | ||
|
|
45b56609ae | ||
|
|
7be0f6594e | ||
|
|
ddb816cf60 | ||
|
|
40f332e534 | ||
|
|
ddc25cf4e2 | ||
|
|
aff892ce79 | ||
|
|
f5a70e0d2e | ||
|
|
d8324d5a2b | ||
|
|
4ebbaf0d43 | ||
|
|
d4abeb8354 | ||
|
|
f42e29cf95 | ||
|
|
896253e085 | ||
|
|
14d413752b | ||
|
|
fd40d992ad | ||
|
|
8beb613916 | ||
|
|
c7783d6fee | ||
|
|
5758dafb4e | ||
|
|
6370cffbbf | ||
|
|
fb233dc40b | ||
|
|
05b961d7e3 | ||
|
|
dcf52469e8 | ||
|
|
8c83cc471b | ||
|
|
9978c5c103 | ||
|
|
ba63b4be5d | ||
|
|
eab141ee67 | ||
|
|
0e6b3e4e40 | ||
|
|
5e54365234 | ||
|
|
84a769cdb7 | ||
|
|
a9684730ac | ||
|
|
7ed971d9b2 | ||
|
|
eae0842bc1 | ||
|
|
c8e1da930d | ||
|
|
771892b314 | ||
|
|
b61a308b27 | ||
|
|
e8d4a31475 | ||
|
|
b085fac735 | ||
|
|
093e34e301 | ||
|
|
697ab75a34 | ||
|
|
f8abbae99f | ||
|
|
794fe2ca45 | ||
|
|
f88d3ee8ae | ||
|
|
fda4422bc9 | ||
|
|
d7c7efb691 | ||
|
|
91f0e41153 | ||
|
|
f91345bdb5 | ||
|
|
30595b466f | ||
|
|
c86ebe7673 | ||
|
|
2b042ad67f | ||
|
|
d19e2ed02f | ||
|
|
b90d377af4 | ||
|
|
8ce100c7b4 | ||
|
|
5c5f5c1f0e | ||
|
|
375eba6a18 | ||
|
|
0c4536da8f | ||
|
|
347b497db0 | ||
|
|
3a5ad7dbd5 | ||
|
|
d94f682a4c | ||
|
|
8f616684a3 | ||
|
|
0b725f5c4f | ||
|
|
bd2373277d | ||
|
|
a578251b48 | ||
|
|
53557fc532 | ||
|
|
f7cac2f7b6 | ||
|
|
76c5a5c2f6 | ||
|
|
c4ee4ce93e | ||
|
|
ef995e6946 | ||
|
|
66fde49f07 | ||
|
|
164f6b9256 | ||
|
|
75656712e3 | ||
|
|
784d714a3f | ||
|
|
ab3c897ce1 | ||
|
|
5a7dd05818 | ||
|
|
24cc6979fb | ||
|
|
ac3183caaa | ||
|
|
ecb0f78063 | ||
|
|
c2afc2ad90 | ||
|
|
8be07e0db4 | ||
|
|
b2b29efb75 | ||
|
|
37b6b880ef | ||
|
|
adc4310a73 | ||
|
|
0c0ae2e886 | ||
|
|
52eb5d6a09 | ||
|
|
582019f870 | ||
|
|
f02bf64d0e | ||
|
|
e117bc3fc5 | ||
|
|
34c39398fa | ||
|
|
03c25ebeae | ||
|
|
73a680b2a8 | ||
|
|
af613824e4 | ||
|
|
5bf318e9a6 | ||
|
|
b4886264a3 | ||
|
|
c4e3029d55 | ||
|
|
20db147ef3 | ||
|
|
55a186485c | ||
|
|
cc0532a4bf | ||
|
|
0cd66885e3 | ||
|
|
e890ce223c | ||
|
|
c78b5fb1f1 | ||
|
|
0995810273 | ||
|
|
c3ae8def75 | ||
|
|
e426df8e10 | ||
|
|
9f2573eea1 | ||
|
|
3737329d9b | ||
|
|
0227618d3c | ||
|
|
11e6b3d18b | ||
|
|
a3c6010718 | ||
|
|
cab4c73088 | ||
|
|
e9484d6a95 | ||
|
|
c20281ee33 | ||
|
|
a93fa42bce | ||
|
|
fc8bcc809d | ||
|
|
5b99b471b2 | ||
|
|
aaf50bf6f3 | ||
|
|
c163357f38 | ||
|
|
2df41aa138 | ||
|
|
f90782a658 | ||
|
|
951690e54d | ||
|
|
c71456117d | ||
|
|
4996398858 | ||
|
|
f08bd95880 | ||
|
|
8f5b858a1b | ||
|
|
e9c85a4d5a | ||
|
|
e1515c3e91 | ||
|
|
0613666d9c | ||
|
|
131e036402 | ||
|
|
51d63ac329 | ||
|
|
26a041541b | ||
|
|
bc658907f0 | ||
|
|
b932600653 | ||
|
|
f0c730252f | ||
|
|
6a7e168009 | ||
|
|
27091f146a | ||
|
|
a1a4960baf | ||
|
|
559a26b025 | ||
|
|
d60658c2db | ||
|
|
77e5ae22a9 | ||
|
|
19ebdc321d | ||
|
|
92c43e4a0e | ||
|
|
47b1e1491f | ||
|
|
3b5e8125eb | ||
|
|
f292ad4b2b | ||
|
|
543e84fe70 | ||
|
|
6de799422d | ||
|
|
8046df6efa | ||
|
|
6d3e4f4d0a | ||
|
|
96d4bf9012 | ||
|
|
aa8cce58bf | ||
|
|
d45e2302ed | ||
|
|
ae46f10fc5 | ||
|
|
2e77ba637a | ||
|
|
ce8bc642ae | ||
|
|
89f2e8fbdf | ||
|
|
95e2d2d36d | ||
|
|
650e32d455 | ||
|
|
ff78eded01 | ||
|
|
525a218b2b | ||
|
|
17753f0c20 | ||
|
|
03d415a6a2 | ||
|
|
f275ba49bb | ||
|
|
c0462dbf15 | ||
|
|
02be8da5e1 | ||
|
|
dc7bb70f22 | ||
|
|
3c39f42a05 | ||
|
|
7dd1c5c542 | ||
|
|
9a71add1c0 | ||
|
|
9bace3a367 | ||
|
|
8dae5c8108 | ||
|
|
7b810e136e | ||
|
|
0dd3aea319 | ||
|
|
94a5db9f4d | ||
|
|
6efd4d1649 | ||
|
|
77a076bd25 | ||
|
|
f2c039bfb9 | ||
|
|
fed29251d7 | ||
|
|
a060b47b13 | ||
|
|
3bd2841fdb | ||
|
|
197f3ea4ba | ||
|
|
06c34bfbae | ||
|
|
4ff2273b30 | ||
|
|
0f48e22ef6 | ||
|
|
51969f9e5f | ||
|
|
e7ca813dd4 | ||
|
|
09601255f5 | ||
|
|
9ff349a3cb | ||
|
|
1a2de0c5fe | ||
|
|
a2da04b8ab | ||
|
|
f3a4267757 | ||
|
|
4574b5a9e6 | ||
|
|
8c52e6e8a1 | ||
|
|
40c6fe1b81 | ||
|
|
1bb0528316 | ||
|
|
941f59101b | ||
|
|
f2eda123b7 | ||
|
|
038f5afb07 | ||
|
|
a006d168c5 | ||
|
|
22c1ffb0a0 | ||
|
|
c059c9fea5 | ||
|
|
6e856d7729 | ||
|
|
30ed0884fc | ||
|
|
898835d924 | ||
|
|
d8cf06e525 | ||
|
|
d3dd749044 | ||
|
|
c3979b236e | ||
|
|
b993555bf4 | ||
|
|
bcb8d2fe54 | ||
|
|
83c31735d0 | ||
|
|
3b33529dfd | ||
|
|
c934760014 | ||
|
|
365e007bee | ||
|
|
e9dfc4cfae | ||
|
|
0b354fcb84 | ||
|
|
fe10b882b7 | ||
|
|
4c0da49d7c | ||
|
|
68bd7dfbb7 | ||
|
|
9ccfdfcd7c | ||
|
|
166c2cd4f3 | ||
|
|
33cf48118f | ||
|
|
e709d61964 | ||
|
|
0b1cc7cc0b | ||
|
|
2cd29dbdd9 | ||
|
|
7d897f5bfc | ||
|
|
88391bcdc3 | ||
|
|
776ac820f9 | ||
|
|
b724a809c4 | ||
|
|
7a1e881665 | ||
|
|
b4b892f4a3 | ||
|
|
6dc92d3427 | ||
|
|
017dfaef4c | ||
|
|
1bd540ef79 | ||
|
|
9ec9d6f2cb | ||
|
|
4ffac34a64 | ||
|
|
9bfc8bf752 | ||
|
|
91015ad008 | ||
|
|
4f7fe63b6d | ||
|
|
fdd2ac495a | ||
|
|
8bc3066e0b | ||
|
|
471c47441d | ||
|
|
e97f756a05 | ||
|
|
2f4cb04f45 | ||
|
|
472cf532b7 | ||
|
|
322a047502 | ||
|
|
1251d017c1 | ||
|
|
3d7026e709 | ||
|
|
c515d37797 | ||
|
|
84b78c3b5f | ||
|
|
f7b84eb92a | ||
|
|
2aaedab203 | ||
|
|
e0b7c521cb | ||
|
|
875a481a1e | ||
|
|
7a9f6f083e | ||
|
|
76d7fd39cd | ||
|
|
8fe39a0311 | ||
|
|
a70a801184 | ||
|
|
4a67834bc8 | ||
|
|
c562f237f6 | ||
|
|
8498d348d8 | ||
|
|
e97de6d96a | ||
|
|
22dd1cde2d | ||
|
|
0adf3e5445 | ||
|
|
2c9e136d57 | ||
|
|
bd03947c05 | ||
|
|
2ebf795c0a | ||
|
|
0c2d245fdf | ||
|
|
823999716e | ||
|
|
c1d860870b | ||
|
|
c1c7b39827 | ||
|
|
fc946f3b8d | ||
|
|
0b16886397 | ||
|
|
1235f7f383 | ||
|
|
ece828a7b7 | ||
|
|
365a186729 | ||
|
|
7ceda8bf3d | ||
|
|
93ed31dda2 | ||
|
|
4bdfce30d7 | ||
|
|
e0d2c6889b | ||
|
|
78015948a7 | ||
|
|
4ad45f2582 | ||
|
|
722b65f461 | ||
|
|
cc42d3f907 | ||
|
|
4d9dd9bdc0 | ||
|
|
8e571cbed8 | ||
|
|
295322048d | ||
|
|
acb68a39e0 | ||
|
|
8b1dd9f57f | ||
|
|
9150a0d62e | ||
|
|
cf7c54ec93 | ||
|
|
33391db5f8 | ||
|
|
396a67a09a | ||
|
|
9d8f798a3f | ||
|
|
e4f50fa0aa | ||
|
|
e016f4043b | ||
|
|
38b27bd2cb | ||
|
|
5a3a15f5c1 | ||
|
|
c183cec8f6 | ||
|
|
83172487b0 | ||
|
|
5561a87920 | ||
|
|
777d9914b5 | ||
|
|
50de1eaad9 | ||
|
|
2a4fda7b88 | ||
|
|
3773759c0f | ||
|
|
e3e72b8c5c | ||
|
|
3dbce6f4a5 | ||
|
|
b9c442c85c | ||
|
|
1b4a164c02 | ||
|
|
b0b80074e0 | ||
|
|
d5bdf3c0c7 | ||
|
|
8552ed8df2 | ||
|
|
11634017f4 | ||
|
|
c81a19552f | ||
|
|
9c61556504 | ||
|
|
26c8fff19e | ||
|
|
3cca61e006 | ||
|
|
c18e551640 | ||
|
|
388581e087 | ||
|
|
c23e3db544 | ||
|
|
0ef5bfd6a9 | ||
|
|
6840e7cece | ||
|
|
60b143a52e | ||
|
|
c59bcabf0b | ||
|
|
fddc7a080a | ||
|
|
e78dd33292 | ||
|
|
93aac9bb7b | ||
|
|
445ad9941e | ||
|
|
6d485dd1c7 | ||
|
|
fb0928097a | ||
|
|
0cbb6b0f52 | ||
|
|
2cfdfee572 | ||
|
|
289a249874 | ||
|
|
3cb5b73c0d | ||
|
|
8807f4170e | ||
|
|
032f8d4ed3 | ||
|
|
d93ce29a86 | ||
|
|
6741c3dbd9 | ||
|
|
4fbf2328c2 | ||
|
|
30fbba168b | ||
|
|
dd3abbd61f | ||
|
|
6fde707add | ||
|
|
5f2665320f | ||
|
|
20c47383dc | ||
|
|
03149ad23a | ||
|
|
e1ca0f1396 | ||
|
|
6df6f5e084 | ||
|
|
ca7240a2f0 | ||
|
|
fb532d8425 | ||
|
|
c291a4d522 | ||
|
|
42876969b9 | ||
|
|
273b12729b | ||
|
|
e32ded7b3e | ||
|
|
b46fa8603e | ||
|
|
e020574d65 | ||
|
|
b19cf6a105 | ||
|
|
8398f19bce | ||
|
|
06cc147012 | ||
|
|
0c14a699bb | ||
|
|
54e513b4e6 | ||
|
|
fbeaeb8689 | ||
|
|
ec3719b583 | ||
|
|
92171f9dd1 | ||
|
|
a56008842b | ||
|
|
7331d34839 | ||
|
|
059651efa1 | ||
|
|
f7c4daa8f9 | ||
|
|
5eacaeb4a7 | ||
|
|
eba89f093f | ||
|
|
1d77969124 | ||
|
|
b1503112ce | ||
|
|
51449e0665 | ||
|
|
6efdc11cc8 | ||
|
|
05c7cba73a | ||
|
|
fa8e6ff900 | ||
|
|
f9958f3404 | ||
|
|
0484d7f6e9 | ||
|
|
57d2bfca3f | ||
|
|
39c1892b22 | ||
|
|
436513068d | ||
|
|
b481889117 | ||
|
|
69a75b7ebe | ||
|
|
3186c5bdbc | ||
|
|
9b6aaf2074 | ||
|
|
e5725eb3b9 | ||
|
|
7f6f3f9d62 | ||
|
|
0cfb4591a7 | ||
|
|
37b8a71f10 | ||
|
|
efac71d6ca | ||
|
|
8d7accb28f | ||
|
|
c92d64a6c3 | ||
|
|
d07dfe5392 | ||
|
|
14ff33bd93 | ||
|
|
7b88619241 | ||
|
|
7b814d3f7f | ||
|
|
2b1799883d | ||
|
|
e26340cee7 | ||
|
|
85419e1257 | ||
|
|
5f84ba8ea1 | ||
|
|
f21f9fa3c5 | ||
|
|
9b1e552b51 | ||
|
|
30a89d2fdb | ||
|
|
3b9cc882a5 | ||
|
|
bda5d7d14f | ||
|
|
e0bf18addf | ||
|
|
4be637cb12 | ||
|
|
f7cb604211 | ||
|
|
fc7a05c443 | ||
|
|
b3f66ea6fb | ||
|
|
d3e72b4d87 | ||
|
|
98e1080555 | ||
|
|
54c689c819 | ||
|
|
c4652d7772 | ||
|
|
6188c4f69c | ||
|
|
ada711504e | ||
|
|
1c06c48ce2 | ||
|
|
5759bec43c | ||
|
|
49fe31792b | ||
|
|
7dfd99f163 | ||
|
|
7256def8e4 | ||
|
|
f87586e661 | ||
|
|
bcd48b9636 | ||
|
|
6927b6b197 | ||
|
|
0c4b696727 | ||
|
|
3a243c53f4 | ||
|
|
cbb10879cb | ||
|
|
8a850573c9 | ||
|
|
673773b217 | ||
|
|
7ecb49ef25 | ||
|
|
5c6189ea3e | ||
|
|
ede491b4e0 | ||
|
|
dc93860619 | ||
|
|
22f00a09dd | ||
|
|
ca65a9d03e | ||
|
|
53584420a5 | ||
|
|
97c68c508d | ||
|
|
c2f9768740 | ||
|
|
73dd81ca62 | ||
|
|
b1b85753d7 | ||
|
|
1d2016b4a8 | ||
|
|
16bfabb9c5 | ||
|
|
d00cca11b9 | ||
|
|
58691680b8 | ||
|
|
7f058c5ff7 | ||
|
|
d3d0713de5 | ||
|
|
8907e143c1 | ||
|
|
f4ce61ed36 | ||
|
|
73315ce9de | ||
|
|
dbe71e670c | ||
|
|
b390bf39f2 | ||
|
|
6dcade97be | ||
|
|
5d5932d493 | ||
|
|
afb714f7be | ||
|
|
dc70d1fef8 | ||
|
|
42529cbced | ||
|
|
00e9c08609 | ||
|
|
3e85e52b3f | ||
|
|
5fed042640 | ||
|
|
2408c4b0a4 | ||
|
|
602684eac5 | ||
|
|
2bdee98269 | ||
|
|
2d2953cf5f | ||
|
|
2ca2dbc821 | ||
|
|
e3e2fc3255 | ||
|
|
2cb30767fa | ||
|
|
34a5fbe2b7 | ||
|
|
cf7e723808 | ||
|
|
c2e7c84e58 | ||
|
|
3891597eb3 | ||
|
|
fda63064fc | ||
|
|
895fcb377e | ||
|
|
c06a9063e1 | ||
|
|
70d0a453f3 | ||
|
|
38e3241eb7 | ||
|
|
b1a38c39ad | ||
|
|
1d3d37937d | ||
|
|
39585bf556 | ||
|
|
02ffbb20d0 | ||
|
|
9c804bc3fd | ||
|
|
67d8305aea | ||
|
|
db72a07ef5 | ||
|
|
968dc988f9 | ||
|
|
c43d898119 | ||
|
|
d8fcc4e00a | ||
|
|
bfb198a6eb | ||
|
|
28db5dde4c | ||
|
|
80e89772e2 | ||
|
|
63403aa7a5 | ||
|
|
9d0dcf2e3c | ||
|
|
7f83613733 | ||
|
|
b5924cae04 | ||
|
|
379a653ae3 | ||
|
|
edb557b2ad | ||
|
|
5940ec993b | ||
|
|
5720ab59e0 | ||
|
|
d44dd47fbf | ||
|
|
8ac9199f56 | ||
|
|
f3467d4646 | ||
|
|
5a0e687d5c | ||
|
|
c9d2cecac9 | ||
|
|
42507b0011 | ||
|
|
76e1565200 | ||
|
|
333836ff92 | ||
|
|
a09882de83 | ||
|
|
4c68460392 | ||
|
|
9cb4f75d53 | ||
|
|
9b8e348b15 | ||
|
|
cc7a267a85 | ||
|
|
bacaa215eb | ||
|
|
72d8d1265b | ||
|
|
89fc09c3d1 | ||
|
|
82be4457de | ||
|
|
a039e2544c | ||
|
|
6536161e2a | ||
|
|
1497e50649 | ||
|
|
5cf45c4319 | ||
|
|
dfa05f0cd6 | ||
|
|
36a2a877e2 | ||
|
|
d5ae67e67d | ||
|
|
fd9a8db7ea | ||
|
|
9e5545a6fa | ||
|
|
a01416cf21 | ||
|
|
f6da237c35 | ||
|
|
9bd07bed23 | ||
|
|
03a501456c | ||
|
|
52b2c6c9c7 | ||
|
|
8a12df8cf3 | ||
|
|
96707ed718 | ||
|
|
76ec154e95 | ||
|
|
bc2ec808f4 | ||
|
|
0529a7e2e9 | ||
|
|
b9f77d1ae1 | ||
|
|
5e23a19204 | ||
|
|
adb04b1e57 | ||
|
|
af1c7c7808 | ||
|
|
12819d5082 | ||
|
|
52d8519008 | ||
|
|
773de09774 | ||
|
|
98933e3db6 | ||
|
|
78edb47cc5 | ||
|
|
3c8c3bf3b7 | ||
|
|
3e26720e05 | ||
|
|
f4ea78e9e2 | ||
|
|
753126b8cc | ||
|
|
d7e8ea67b3 | ||
|
|
f0128f9600 | ||
|
|
96a5ba41f5 | ||
|
|
90d60e3fe4 | ||
|
|
af61c29527 | ||
|
|
0e93e01fcb | ||
|
|
407c299828 | ||
|
|
1eb319806b | ||
|
|
d90e586c85 | ||
|
|
24b5d01853 | ||
|
|
74ee4048c2 | ||
|
|
d61109f578 | ||
|
|
41ce544abe | ||
|
|
fead431c18 | ||
|
|
b56730bb6e | ||
|
|
afa953a293 | ||
|
|
0a6664493a | ||
|
|
4c7ad50f6e | ||
|
|
173264b656 | ||
|
|
fc7c5e9cd7 | ||
|
|
9728c305a3 | ||
|
|
f3788e3c78 | ||
|
|
88af58d41d | ||
|
|
bdc21e7282 | ||
|
|
7642d95d5e | ||
|
|
eb6aedf92c | ||
|
|
58f82e2e54 | ||
|
|
23465a30b6 | ||
|
|
ebf6c08a47 | ||
|
|
051b185811 | ||
|
|
74c3879760 |
22
.gitignore
vendored
22
.gitignore
vendored
@@ -26,17 +26,23 @@ htmlcov
|
|||||||
|
|
||||||
demo/*.db
|
demo/*.db
|
||||||
demo/*.log
|
demo/*.log
|
||||||
|
demo/*.log.*
|
||||||
demo/*.pid
|
demo/*.pid
|
||||||
|
demo/media_store.*
|
||||||
demo/etc
|
demo/etc
|
||||||
|
|
||||||
graph/*.svg
|
|
||||||
graph/*.png
|
|
||||||
graph/*.dot
|
|
||||||
|
|
||||||
**/webclient/config.js
|
|
||||||
**/webclient/test/coverage/
|
|
||||||
**/webclient/test/environment-protractor.js
|
|
||||||
|
|
||||||
uploads
|
uploads
|
||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
media_store/
|
||||||
|
|
||||||
|
*.tac
|
||||||
|
|
||||||
|
build/
|
||||||
|
|
||||||
|
localhost-800*/
|
||||||
|
static/client/register/register_config.js
|
||||||
|
.tox
|
||||||
|
|
||||||
|
env/
|
||||||
|
*.config
|
||||||
|
|||||||
47
AUTHORS.rst
Normal file
47
AUTHORS.rst
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
Erik Johnston <erik at matrix.org>
|
||||||
|
* HS core
|
||||||
|
* Federation API impl
|
||||||
|
|
||||||
|
Mark Haines <mark at matrix.org>
|
||||||
|
* HS core
|
||||||
|
* Crypto
|
||||||
|
* Content repository
|
||||||
|
* CS v2 API impl
|
||||||
|
|
||||||
|
Kegan Dougal <kegan at matrix.org>
|
||||||
|
* HS core
|
||||||
|
* CS v1 API impl
|
||||||
|
* AS API impl
|
||||||
|
|
||||||
|
Paul "LeoNerd" Evans <paul at matrix.org>
|
||||||
|
* HS core
|
||||||
|
* Presence
|
||||||
|
* Typing Notifications
|
||||||
|
* Performance metrics and caching layer
|
||||||
|
|
||||||
|
Dave Baker <dave at matrix.org>
|
||||||
|
* Push notifications
|
||||||
|
* Auth CS v2 impl
|
||||||
|
|
||||||
|
Matthew Hodgson <matthew at matrix.org>
|
||||||
|
* General doc & housekeeping
|
||||||
|
* Vertobot/vertobridge matrix<->verto PoC
|
||||||
|
|
||||||
|
Emmanuel Rohee <manu at matrix.org>
|
||||||
|
* Supporting iOS clients (testability and fallback registration)
|
||||||
|
|
||||||
|
Turned to Dust <dwinslow86 at gmail.com>
|
||||||
|
* ArchLinux installation instructions
|
||||||
|
|
||||||
|
Brabo <brabo at riseup.net>
|
||||||
|
* Installation instruction fixes
|
||||||
|
|
||||||
|
Ivan Shapovalov <intelfx100 at gmail.com>
|
||||||
|
* contrib/systemd: a sample systemd unit file and a logger configuration
|
||||||
|
|
||||||
|
Eric Myhre <hash at exultant.us>
|
||||||
|
* Fix bug where ``media_store_path`` config option was ignored by v0 content
|
||||||
|
repository API.
|
||||||
|
|
||||||
|
Muthu Subramanian <muthu.subramanian.karunanidhi at ericsson.com>
|
||||||
|
* Add SAML2 support for registration and logins.
|
||||||
409
CHANGES.rst
409
CHANGES.rst
@@ -1,46 +1,407 @@
|
|||||||
|
Changes in synapse v0.10.0 (2015-09-03)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
No change from release candidate.
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc6 (2015-09-02)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Remove some of the old database upgrade scripts.
|
||||||
|
* Fix database port script to work with newly created sqlite databases.
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc5 (2015-08-27)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Fix bug that broke downloading files with ascii filenames across federation.
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc4 (2015-08-27)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Allow UTF-8 filenames for upload. (PR #259)
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc3 (2015-08-25)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Add ``--keys-directory`` config option to specify where files such as
|
||||||
|
certs and signing keys should be stored in, when using ``--generate-config``
|
||||||
|
or ``--generate-keys``. (PR #250)
|
||||||
|
* Allow ``--config-path`` to specify a directory, causing synapse to use all
|
||||||
|
\*.yaml files in the directory as config files. (PR #249)
|
||||||
|
* Add ``web_client_location`` config option to specify static files to be
|
||||||
|
hosted by synapse under ``/_matrix/client``. (PR #245)
|
||||||
|
* Add helper utility to synapse to read and parse the config files and extract
|
||||||
|
the value of a given key. For example::
|
||||||
|
|
||||||
|
$ python -m synapse.config read server_name -c homeserver.yaml
|
||||||
|
localhost
|
||||||
|
|
||||||
|
(PR #246)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc2 (2015-08-24)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
* Fix bug where we incorrectly populated the ``event_forward_extremities``
|
||||||
|
table, resulting in problems joining large remote rooms (e.g.
|
||||||
|
``#matrix:matrix.org``)
|
||||||
|
* Reduce the number of times we wake up pushers by not listening for presence
|
||||||
|
or typing events, reducing the CPU cost of each pusher.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.10.0-rc1 (2015-08-21)
|
||||||
|
===========================================
|
||||||
|
|
||||||
|
Also see v0.9.4-rc1 changelog, which has been amalgamated into this release.
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Upgrade to Twisted 15 (PR #173)
|
||||||
|
* Add support for serving and fetching encryption keys over federation.
|
||||||
|
(PR #208)
|
||||||
|
* Add support for logging in with email address (PR #234)
|
||||||
|
* Add support for new ``m.room.canonical_alias`` event. (PR #233)
|
||||||
|
* Change synapse to treat user IDs case insensitively during registration and
|
||||||
|
login. (If two users already exist with case insensitive matching user ids,
|
||||||
|
synapse will continue to require them to specify their user ids exactly.)
|
||||||
|
* Error if a user tries to register with an email already in use. (PR #211)
|
||||||
|
* Add extra and improve existing caches (PR #212, #219, #226, #228)
|
||||||
|
* Batch various storage request (PR #226, #228)
|
||||||
|
* Fix bug where we didn't correctly log the entity that triggered the request
|
||||||
|
if the request came in via an application service (PR #230)
|
||||||
|
* Fix bug where we needlessly regenerated the full list of rooms an AS is
|
||||||
|
interested in. (PR #232)
|
||||||
|
* Add support for AS's to use v2_alpha registration API (PR #210)
|
||||||
|
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add ``--generate-keys`` that will generate any missing cert and key files in
|
||||||
|
the configuration files. This is equivalent to running ``--generate-config``
|
||||||
|
on an existing configuration file. (PR #220)
|
||||||
|
* ``--generate-config`` now no longer requires a ``--server-name`` parameter
|
||||||
|
when used on existing configuration files. (PR #220)
|
||||||
|
* Add ``--print-pidfile`` flag that controls the printing of the pid to stdout
|
||||||
|
of the demonised process. (PR #213)
|
||||||
|
|
||||||
|
Media Repository:
|
||||||
|
|
||||||
|
* Fix bug where we picked a lower resolution image than requested. (PR #205)
|
||||||
|
* Add support for specifying if a the media repository should dynamically
|
||||||
|
thumbnail images or not. (PR #206)
|
||||||
|
|
||||||
|
Metrics:
|
||||||
|
|
||||||
|
* Add statistics from the reactor to the metrics API. (PR #224, #225)
|
||||||
|
|
||||||
|
Demo Homeservers:
|
||||||
|
|
||||||
|
* Fix starting the demo homeservers without rate-limiting enabled. (PR #182)
|
||||||
|
* Fix enabling registration on demo homeservers (PR #223)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.9.4-rc1 (2015-07-21)
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Add basic implementation of receipts. (SPEC-99)
|
||||||
|
* Add support for configuration presets in room creation API. (PR #203)
|
||||||
|
* Add auth event that limits the visibility of history for new users.
|
||||||
|
(SPEC-134)
|
||||||
|
* Add SAML2 login/registration support. (PR #201. Thanks Muthu Subramanian!)
|
||||||
|
* Add client side key management APIs for end to end encryption. (PR #198)
|
||||||
|
* Change power level semantics so that you cannot kick, ban or change power
|
||||||
|
levels of users that have equal or greater power level than you. (SYN-192)
|
||||||
|
* Improve performance by bulk inserting events where possible. (PR #193)
|
||||||
|
* Improve performance by bulk verifying signatures where possible. (PR #194)
|
||||||
|
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add support for including TLS certificate chains.
|
||||||
|
|
||||||
|
Media Repository:
|
||||||
|
|
||||||
|
* Add Content-Disposition headers to content repository responses. (SYN-150)
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.9.3 (2015-07-01)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
No changes from v0.9.3 Release Candidate 1.
|
||||||
|
|
||||||
|
Changes in synapse v0.9.3-rc1 (2015-06-23)
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Fix a memory leak in the notifier. (SYN-412)
|
||||||
|
* Improve performance of room initial sync. (SYN-418)
|
||||||
|
* General improvements to logging.
|
||||||
|
* Remove ``access_token`` query params from ``INFO`` level logging.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add support for specifying and configuring multiple listeners. (SYN-389)
|
||||||
|
|
||||||
|
Application services:
|
||||||
|
|
||||||
|
* Fix bug where synapse failed to send user queries to application services.
|
||||||
|
|
||||||
|
Changes in synapse v0.9.2-r2 (2015-06-15)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
Fix packaging so that schema delta python files get included in the package.
|
||||||
|
|
||||||
|
Changes in synapse v0.9.2 (2015-06-12)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Use ultrajson for json (de)serialisation when a canonical encoding is not
|
||||||
|
required. Ultrajson is significantly faster than simplejson in certain
|
||||||
|
circumstances.
|
||||||
|
* Use connection pools for outgoing HTTP connections.
|
||||||
|
* Process thumbnails on separate threads.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add option, ``gzip_responses``, to disable HTTP response compression.
|
||||||
|
|
||||||
|
Federation:
|
||||||
|
|
||||||
|
* Improve resilience of backfill by ensuring we fetch any missing auth events.
|
||||||
|
* Improve performance of backfill and joining remote rooms by removing
|
||||||
|
unnecessary computations. This included handling events we'd previously
|
||||||
|
handled as well as attempting to compute the current state for outliers.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.9.1 (2015-05-26)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Add support for backfilling when a client paginates. This allows servers to
|
||||||
|
request history for a room from remote servers when a client tries to
|
||||||
|
paginate history the server does not have - SYN-36
|
||||||
|
* Fix bug where you couldn't disable non-default pushrules - SYN-378
|
||||||
|
* Fix ``register_new_user`` script - SYN-359
|
||||||
|
* Improve performance of fetching events from the database, this improves both
|
||||||
|
initialSync and sending of events.
|
||||||
|
* Improve performance of event streams, allowing synapse to handle more
|
||||||
|
simultaneous connected clients.
|
||||||
|
|
||||||
|
Federation:
|
||||||
|
|
||||||
|
* Fix bug with existing backfill implementation where it returned the wrong
|
||||||
|
selection of events in some circumstances.
|
||||||
|
* Improve performance of joining remote rooms.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add support for changing the bind host of the metrics listener via the
|
||||||
|
``metrics_bind_host`` option.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.9.0-r5 (2015-05-21)
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
* Add more database caches to reduce amount of work done for each pusher. This
|
||||||
|
radically reduces CPU usage when multiple pushers are set up in the same room.
|
||||||
|
|
||||||
|
Changes in synapse v0.9.0 (2015-05-07)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Add support for using a PostgreSQL database instead of SQLite. See
|
||||||
|
`docs/postgres.rst`_ for details.
|
||||||
|
* Add password change and reset APIs. See `Registration`_ in the spec.
|
||||||
|
* Fix memory leak due to not releasing stale notifiers - SYN-339.
|
||||||
|
* Fix race in caches that occasionally caused some presence updates to be
|
||||||
|
dropped - SYN-369.
|
||||||
|
* Check server name has not changed on restart.
|
||||||
|
* Add a sample systemd unit file and a logger configuration in
|
||||||
|
contrib/systemd. Contributed Ivan Shapovalov.
|
||||||
|
|
||||||
|
Federation:
|
||||||
|
|
||||||
|
* Add key distribution mechanisms for fetching public keys of unavailable
|
||||||
|
remote home servers. See `Retrieving Server Keys`_ in the spec.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Add support for multiple config files.
|
||||||
|
* Add support for dictionaries in config files.
|
||||||
|
* Remove support for specifying config options on the command line, except
|
||||||
|
for:
|
||||||
|
|
||||||
|
* ``--daemonize`` - Daemonize the home server.
|
||||||
|
* ``--manhole`` - Turn on the twisted telnet manhole service on the given
|
||||||
|
port.
|
||||||
|
* ``--database-path`` - The path to a sqlite database to use.
|
||||||
|
* ``--verbose`` - The verbosity level.
|
||||||
|
* ``--log-file`` - File to log to.
|
||||||
|
* ``--log-config`` - Python logging config file.
|
||||||
|
* ``--enable-registration`` - Enable registration for new users.
|
||||||
|
|
||||||
|
Application services:
|
||||||
|
|
||||||
|
* Reliably retry sending of events from Synapse to application services, as per
|
||||||
|
`Application Services`_ spec.
|
||||||
|
* Application services can no longer register via the ``/register`` API,
|
||||||
|
instead their configuration should be saved to a file and listed in the
|
||||||
|
synapse ``app_service_config_files`` config option. The AS configuration file
|
||||||
|
has the same format as the old ``/register`` request.
|
||||||
|
See `docs/application_services.rst`_ for more information.
|
||||||
|
|
||||||
|
.. _`docs/postgres.rst`: docs/postgres.rst
|
||||||
|
.. _`docs/application_services.rst`: docs/application_services.rst
|
||||||
|
.. _`Registration`: https://github.com/matrix-org/matrix-doc/blob/master/specification/10_client_server_api.rst#registration
|
||||||
|
.. _`Retrieving Server Keys`: https://github.com/matrix-org/matrix-doc/blob/6f2698/specification/30_server_server_api.rst#retrieving-server-keys
|
||||||
|
.. _`Application Services`: https://github.com/matrix-org/matrix-doc/blob/0c6bd9/specification/25_application_service_api.rst#home-server---application-service-api
|
||||||
|
|
||||||
|
Changes in synapse v0.8.1 (2015-03-18)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
* Disable registration by default. New users can be added using the command
|
||||||
|
``register_new_matrix_user`` or by enabling registration in the config.
|
||||||
|
* Add metrics to synapse. To enable metrics use config options
|
||||||
|
``enable_metrics`` and ``metrics_port``.
|
||||||
|
* Fix bug where banning only kicked the user.
|
||||||
|
|
||||||
|
Changes in synapse v0.8.0 (2015-03-06)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* Add support for registration fallback. This is a page hosted on the server
|
||||||
|
which allows a user to register for an account, regardless of what client
|
||||||
|
they are using (e.g. mobile devices).
|
||||||
|
|
||||||
|
* Added new default push rules and made them configurable by clients:
|
||||||
|
|
||||||
|
* Suppress all notice messages.
|
||||||
|
* Notify when invited to a new room.
|
||||||
|
* Notify for messages that don't match any rule.
|
||||||
|
* Notify on incoming call.
|
||||||
|
|
||||||
|
Federation:
|
||||||
|
|
||||||
|
* Added per host server side rate-limiting of incoming federation requests.
|
||||||
|
* Added a ``/get_missing_events/`` API to federation to reduce number of
|
||||||
|
``/events/`` requests.
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
* Added configuration option to disable registration:
|
||||||
|
``disable_registration``.
|
||||||
|
* Added configuration option to change soft limit of number of open file
|
||||||
|
descriptors: ``soft_file_limit``.
|
||||||
|
* Make ``tls_private_key_path`` optional when running with ``no_tls``.
|
||||||
|
|
||||||
|
Application services:
|
||||||
|
|
||||||
|
* Application services can now poll on the CS API ``/events`` for their events,
|
||||||
|
by providing their application service ``access_token``.
|
||||||
|
* Added exclusive namespace support to application services API.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in synapse v0.7.1 (2015-02-19)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
* Initial alpha implementation of parts of the Application Services API.
|
||||||
|
Including:
|
||||||
|
|
||||||
|
- AS Registration / Unregistration
|
||||||
|
- User Query API
|
||||||
|
- Room Alias Query API
|
||||||
|
- Push transport for receiving events.
|
||||||
|
- User/Alias namespace admin control
|
||||||
|
|
||||||
|
* Add cache when fetching events from remote servers to stop repeatedly
|
||||||
|
fetching events with bad signatures.
|
||||||
|
* Respect the per remote server retry scheme when fetching both events and
|
||||||
|
server keys to reduce the number of times we send requests to dead servers.
|
||||||
|
* Inform remote servers when the local server fails to handle a received event.
|
||||||
|
* Turn off python bytecode generation due to problems experienced when
|
||||||
|
upgrading from previous versions.
|
||||||
|
|
||||||
|
Changes in synapse v0.7.0 (2015-02-12)
|
||||||
|
======================================
|
||||||
|
|
||||||
|
* Add initial implementation of the query auth federation API, allowing
|
||||||
|
servers to agree on whether an event should be allowed or rejected.
|
||||||
|
* Persist events we have rejected from federation, fixing the bug where
|
||||||
|
servers would keep requesting the same events.
|
||||||
|
* Various federation performance improvements, including:
|
||||||
|
|
||||||
|
- Add in memory caches on queries such as:
|
||||||
|
|
||||||
|
* Computing the state of a room at a point in time, used for
|
||||||
|
authorization on federation requests.
|
||||||
|
* Fetching events from the database.
|
||||||
|
* User's room membership, used for authorizing presence updates.
|
||||||
|
|
||||||
|
- Upgraded JSON library to improve parsing and serialisation speeds.
|
||||||
|
|
||||||
|
* Add default avatars to new user accounts using pydenticon library.
|
||||||
|
* Correctly time out federation requests.
|
||||||
|
* Retry federation requests against different servers.
|
||||||
|
* Add support for push and push rules.
|
||||||
|
* Add alpha versions of proposed new CSv2 APIs, including ``/sync`` API.
|
||||||
|
|
||||||
|
Changes in synapse 0.6.1 (2015-01-07)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
* Major optimizations to improve performance of initial sync and event sending
|
||||||
|
in large rooms (by up to 10x)
|
||||||
|
* Media repository now includes a Content-Length header on media downloads.
|
||||||
|
* Improve quality of thumbnails by changing resizing algorithm.
|
||||||
|
|
||||||
Changes in synapse 0.6.0 (2014-12-16)
|
Changes in synapse 0.6.0 (2014-12-16)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
* Add new API for media upload and download that supports thumbnailing.
|
* Add new API for media upload and download that supports thumbnailing.
|
||||||
* Replicate media uploads over multiple homeservers so media is always served
|
* Replicate media uploads over multiple homeservers so media is always served
|
||||||
to clients from their local homeserver. This obsoletes the
|
to clients from their local homeserver. This obsoletes the
|
||||||
--content-addr parameter and confusion over accessing content directly
|
--content-addr parameter and confusion over accessing content directly
|
||||||
from remote homeservers.
|
from remote homeservers.
|
||||||
* Implement exponential backoff when retrying federation requests when
|
* Implement exponential backoff when retrying federation requests when
|
||||||
sending to remote homeservers which are offline.
|
sending to remote homeservers which are offline.
|
||||||
* Implement typing notifications.
|
* Implement typing notifications.
|
||||||
* Fix bugs where we sent events with invalid signatures due to bugs where
|
* Fix bugs where we sent events with invalid signatures due to bugs where
|
||||||
we incorrectly persisted events.
|
we incorrectly persisted events.
|
||||||
* Improve performance of database queries involving retrieving events.
|
* Improve performance of database queries involving retrieving events.
|
||||||
|
|
||||||
Changes in synapse 0.5.4a (2014-12-13)
|
Changes in synapse 0.5.4a (2014-12-13)
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
* Fix bug while generating the error message when a file path specified in
|
* Fix bug while generating the error message when a file path specified in
|
||||||
the config doesn't exist.
|
the config doesn't exist.
|
||||||
|
|
||||||
Changes in synapse 0.5.4 (2014-12-03)
|
Changes in synapse 0.5.4 (2014-12-03)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
* Fix presence bug where some rooms did not display presence updates for
|
* Fix presence bug where some rooms did not display presence updates for
|
||||||
remote users.
|
remote users.
|
||||||
* Do not log SQL timing log lines when started with "-v"
|
* Do not log SQL timing log lines when started with "-v"
|
||||||
* Fix potential memory leak.
|
* Fix potential memory leak.
|
||||||
|
|
||||||
Changes in synapse 0.5.3c (2014-12-02)
|
Changes in synapse 0.5.3c (2014-12-02)
|
||||||
======================================
|
======================================
|
||||||
|
|
||||||
* Change the default value for the `content_addr` option to use the HTTP
|
* Change the default value for the `content_addr` option to use the HTTP
|
||||||
listener, as by default the HTTPS listener will be using a self-signed
|
listener, as by default the HTTPS listener will be using a self-signed
|
||||||
certificate.
|
certificate.
|
||||||
|
|
||||||
Changes in synapse 0.5.3 (2014-11-27)
|
Changes in synapse 0.5.3 (2014-11-27)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
* Fix bug that caused joining a remote room to fail if a single event was not
|
* Fix bug that caused joining a remote room to fail if a single event was not
|
||||||
signed correctly.
|
signed correctly.
|
||||||
* Fix bug which caused servers to continuously try and fetch events from other
|
* Fix bug which caused servers to continuously try and fetch events from other
|
||||||
servers.
|
servers.
|
||||||
|
|
||||||
Changes in synapse 0.5.2 (2014-11-26)
|
Changes in synapse 0.5.2 (2014-11-26)
|
||||||
=====================================
|
=====================================
|
||||||
|
|||||||
118
CONTRIBUTING.rst
Normal file
118
CONTRIBUTING.rst
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
Contributing code to Matrix
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Everyone is welcome to contribute code to Matrix
|
||||||
|
(https://github.com/matrix-org), provided that they are willing to license
|
||||||
|
their contributions under the same license as the project itself. We follow a
|
||||||
|
simple 'inbound=outbound' model for contributions: the act of submitting an
|
||||||
|
'inbound' contribution means that the contributor agrees to license the code
|
||||||
|
under the same terms as the project's overall 'outbound' license - in our
|
||||||
|
case, this is almost always Apache Software License v2 (see LICENSE).
|
||||||
|
|
||||||
|
How to contribute
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The preferred and easiest way to contribute changes to Matrix is to fork the
|
||||||
|
relevant project on github, and then create a pull request to ask us to pull
|
||||||
|
your changes into our repo
|
||||||
|
(https://help.github.com/articles/using-pull-requests/)
|
||||||
|
|
||||||
|
**The single biggest thing you need to know is: please base your changes on
|
||||||
|
the develop branch - /not/ master.**
|
||||||
|
|
||||||
|
We use the master branch to track the most recent release, so that folks who
|
||||||
|
blindly clone the repo and automatically check out master get something that
|
||||||
|
works. Develop is the unstable branch where all the development actually
|
||||||
|
happens: the workflow is that contributors should fork the develop branch to
|
||||||
|
make a 'feature' branch for a particular contribution, and then make a pull
|
||||||
|
request to merge this back into the matrix.org 'official' develop branch. We
|
||||||
|
use github's pull request workflow to review the contribution, and either ask
|
||||||
|
you to make any refinements needed or merge it and make them ourselves. The
|
||||||
|
changes will then land on master when we next do a release.
|
||||||
|
|
||||||
|
We use Jenkins for continuous integration (http://matrix.org/jenkins), and
|
||||||
|
typically all pull requests get automatically tested Jenkins: if your change breaks the build, Jenkins will yell about it in #matrix-dev:matrix.org so please lurk there and keep an eye open.
|
||||||
|
|
||||||
|
Code style
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
All Matrix projects have a well-defined code-style - and sometimes we've even
|
||||||
|
got as far as documenting it... For instance, synapse's code style doc lives
|
||||||
|
at https://github.com/matrix-org/synapse/tree/master/docs/code_style.rst.
|
||||||
|
|
||||||
|
Please ensure your changes match the cosmetic style of the existing project,
|
||||||
|
and **never** mix cosmetic and functional changes in the same commit, as it
|
||||||
|
makes it horribly hard to review otherwise.
|
||||||
|
|
||||||
|
Attribution
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Everyone who contributes anything to Matrix is welcome to be listed in the
|
||||||
|
AUTHORS.rst file for the project in question. Please feel free to include a
|
||||||
|
change to AUTHORS.rst in your pull request to list yourself and a short
|
||||||
|
description of the area(s) you've worked on. Also, we sometimes have swag to
|
||||||
|
give away to contributors - if you feel that Matrix-branded apparel is missing
|
||||||
|
from your life, please mail us your shipping address to matrix at matrix.org and we'll try to fix it :)
|
||||||
|
|
||||||
|
Sign off
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
In order to have a concrete record that your contribution is intentional
|
||||||
|
and you agree to license it under the same terms as the project's license, we've adopted the
|
||||||
|
same lightweight approach that the Linux Kernel
|
||||||
|
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
|
||||||
|
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
||||||
|
projects use: the DCO (Developer Certificate of Origin:
|
||||||
|
http://developercertificate.org/). This is a simple declaration that you wrote
|
||||||
|
the contribution or otherwise have the right to contribute it to Matrix::
|
||||||
|
|
||||||
|
Developer Certificate of Origin
|
||||||
|
Version 1.1
|
||||||
|
|
||||||
|
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||||
|
660 York Street, Suite 102,
|
||||||
|
San Francisco, CA 94110 USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Developer's Certificate of Origin 1.1
|
||||||
|
|
||||||
|
By making a contribution to this project, I certify that:
|
||||||
|
|
||||||
|
(a) The contribution was created in whole or in part by me and I
|
||||||
|
have the right to submit it under the open source license
|
||||||
|
indicated in the file; or
|
||||||
|
|
||||||
|
(b) The contribution is based upon previous work that, to the best
|
||||||
|
of my knowledge, is covered under an appropriate open source
|
||||||
|
license and I have the right under that license to submit that
|
||||||
|
work with modifications, whether created in whole or in part
|
||||||
|
by me, under the same open source license (unless I am
|
||||||
|
permitted to submit under a different license), as indicated
|
||||||
|
in the file; or
|
||||||
|
|
||||||
|
(c) The contribution was provided directly to me by some other
|
||||||
|
person who certified (a), (b) or (c) and I have not modified
|
||||||
|
it.
|
||||||
|
|
||||||
|
(d) I understand and agree that this project and the contribution
|
||||||
|
are public and that a record of the contribution (including all
|
||||||
|
personal information I submit with it, including my sign-off) is
|
||||||
|
maintained indefinitely and may be redistributed consistent with
|
||||||
|
this project or the open source license(s) involved.
|
||||||
|
|
||||||
|
If you agree to this for your contribution, then all that's needed is to
|
||||||
|
include the line in your commit or pull request comment::
|
||||||
|
|
||||||
|
Signed-off-by: Your Name <your@email.example.org>
|
||||||
|
|
||||||
|
...using your real name; unfortunately pseudonyms and anonymous contributions
|
||||||
|
can't be accepted. Git makes this trivial - just use the -s flag when you do
|
||||||
|
``git commit``, having first set ``user.name`` and ``user.email`` git configs
|
||||||
|
(which you should have done anyway :)
|
||||||
|
|
||||||
|
Conclusion
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
That's it! Matrix is a very open and collaborative project as you might expect given our obsession with open communication. If we're going to successfully matrix together all the fragmented communication technologies out there we are reliant on contributions and collaboration from the community to do so. So please get involved - and we hope you have as much fun hacking on Matrix as we do!
|
||||||
24
MANIFEST.in
24
MANIFEST.in
@@ -1,4 +1,22 @@
|
|||||||
recursive-include docs *
|
include synctl
|
||||||
recursive-include tests *.py
|
include LICENSE
|
||||||
|
include VERSION
|
||||||
|
include *.rst
|
||||||
|
include demo/README
|
||||||
|
include demo/demo.tls.dh
|
||||||
|
include demo/*.py
|
||||||
|
include demo/*.sh
|
||||||
|
|
||||||
recursive-include synapse/storage/schema *.sql
|
recursive-include synapse/storage/schema *.sql
|
||||||
recursive-include syweb/webclient *
|
recursive-include synapse/storage/schema *.py
|
||||||
|
|
||||||
|
recursive-include docs *
|
||||||
|
recursive-include scripts *
|
||||||
|
recursive-include scripts-dev *
|
||||||
|
recursive-include tests *.py
|
||||||
|
|
||||||
|
recursive-include static *.css
|
||||||
|
recursive-include static *.html
|
||||||
|
recursive-include static *.js
|
||||||
|
|
||||||
|
prune demo/etc
|
||||||
|
|||||||
355
README.rst
355
README.rst
@@ -1,3 +1,5 @@
|
|||||||
|
.. contents::
|
||||||
|
|
||||||
Introduction
|
Introduction
|
||||||
============
|
============
|
||||||
|
|
||||||
@@ -5,8 +7,8 @@ Matrix is an ambitious new ecosystem for open federated Instant Messaging and
|
|||||||
VoIP. The basics you need to know to get up and running are:
|
VoIP. The basics you need to know to get up and running are:
|
||||||
|
|
||||||
- Everything in Matrix happens in a room. Rooms are distributed and do not
|
- Everything in Matrix happens in a room. Rooms are distributed and do not
|
||||||
exist on any single server. Rooms can be located using convenience aliases
|
exist on any single server. Rooms can be located using convenience aliases
|
||||||
like ``#matrix:matrix.org`` or ``#test:localhost:8008``.
|
like ``#matrix:matrix.org`` or ``#test:localhost:8448``.
|
||||||
|
|
||||||
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
- Matrix user IDs look like ``@matthew:matrix.org`` (although in the future
|
||||||
you will normally refer to yourself and others using a 3PID: email
|
you will normally refer to yourself and others using a 3PID: email
|
||||||
@@ -18,10 +20,10 @@ The overall architecture is::
|
|||||||
https://somewhere.org/_matrix https://elsewhere.net/_matrix
|
https://somewhere.org/_matrix https://elsewhere.net/_matrix
|
||||||
|
|
||||||
``#matrix:matrix.org`` is the official support room for Matrix, and can be
|
``#matrix:matrix.org`` is the official support room for Matrix, and can be
|
||||||
accessed by the web client at http://matrix.org/alpha or via an IRC bridge at
|
accessed by the web client at http://matrix.org/beta or via an IRC bridge at
|
||||||
irc://irc.freenode.net/matrix.
|
irc://irc.freenode.net/matrix.
|
||||||
|
|
||||||
Synapse is currently in rapid development, but as of version 0.5 we believe it
|
Synapse is currently in rapid development, but as of version 0.5 we believe it
|
||||||
is sufficiently stable to be run as an internet-facing service for real usage!
|
is sufficiently stable to be run as an internet-facing service for real usage!
|
||||||
|
|
||||||
About Matrix
|
About Matrix
|
||||||
@@ -67,25 +69,32 @@ Synapse ships with two basic demo Matrix clients: webclient (a basic group chat
|
|||||||
web client demo implemented in AngularJS) and cmdclient (a basic Python
|
web client demo implemented in AngularJS) and cmdclient (a basic Python
|
||||||
command line utility which lets you easily see what the JSON APIs are up to).
|
command line utility which lets you easily see what the JSON APIs are up to).
|
||||||
|
|
||||||
Meanwhile, iOS and Android SDKs and clients are currently in development and available from:
|
Meanwhile, iOS and Android SDKs and clients are available from:
|
||||||
|
|
||||||
- https://github.com/matrix-org/matrix-ios-sdk
|
- https://github.com/matrix-org/matrix-ios-sdk
|
||||||
|
- https://github.com/matrix-org/matrix-ios-kit
|
||||||
|
- https://github.com/matrix-org/matrix-ios-console
|
||||||
- https://github.com/matrix-org/matrix-android-sdk
|
- https://github.com/matrix-org/matrix-android-sdk
|
||||||
|
|
||||||
We'd like to invite you to join #matrix:matrix.org (via http://matrix.org/alpha), run a homeserver, take a look at the Matrix spec at
|
We'd like to invite you to join #matrix:matrix.org (via
|
||||||
http://matrix.org/docs/spec, experiment with the APIs and the demo
|
https://matrix.org/beta), run a homeserver, take a look at the Matrix spec at
|
||||||
clients, and report any bugs via http://matrix.org/jira.
|
https://matrix.org/docs/spec and API docs at https://matrix.org/docs/api,
|
||||||
|
experiment with the APIs and the demo clients, and report any bugs via
|
||||||
|
https://matrix.org/jira.
|
||||||
|
|
||||||
Thanks for using Matrix!
|
Thanks for using Matrix!
|
||||||
|
|
||||||
[1] End-to-end encryption is currently in development
|
[1] End-to-end encryption is currently in development
|
||||||
|
|
||||||
Homeserver Installation
|
Synapse Installation
|
||||||
=======================
|
====================
|
||||||
|
|
||||||
|
Synapse is the reference python/twisted Matrix homeserver implementation.
|
||||||
|
|
||||||
System requirements:
|
System requirements:
|
||||||
- POSIX-compliant system (tested on Linux & OSX)
|
- POSIX-compliant system (tested on Linux & OS X)
|
||||||
- Python 2.7
|
- Python 2.7
|
||||||
|
- At least 512 MB RAM.
|
||||||
|
|
||||||
Synapse is written in python but some of the libraries is uses are written in
|
Synapse is written in python but some of the libraries is uses are written in
|
||||||
C. So before we can install synapse itself we need a working C compiler and the
|
C. So before we can install synapse itself we need a working C compiler and the
|
||||||
@@ -93,62 +102,163 @@ header files for python C extensions.
|
|||||||
|
|
||||||
Installing prerequisites on Ubuntu or Debian::
|
Installing prerequisites on Ubuntu or Debian::
|
||||||
|
|
||||||
$ sudo apt-get install build-essential python2.7-dev libffi-dev \
|
sudo apt-get install build-essential python2.7-dev libffi-dev \
|
||||||
python-pip python-setuptools sqlite3 \
|
python-pip python-setuptools sqlite3 \
|
||||||
libssl-dev
|
libssl-dev python-virtualenv libjpeg-dev
|
||||||
|
|
||||||
|
Installing prerequisites on ArchLinux::
|
||||||
|
|
||||||
|
sudo pacman -S base-devel python2 python-pip \
|
||||||
|
python-setuptools python-virtualenv sqlite3
|
||||||
|
|
||||||
Installing prerequisites on Mac OS X::
|
Installing prerequisites on Mac OS X::
|
||||||
|
|
||||||
$ xcode-select --install
|
xcode-select --install
|
||||||
|
sudo easy_install pip
|
||||||
|
sudo pip install virtualenv
|
||||||
|
|
||||||
To install the synapse homeserver run::
|
To install the synapse homeserver run::
|
||||||
|
|
||||||
$ pip install --user --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
virtualenv -p python2.7 ~/.synapse
|
||||||
|
source ~/.synapse/bin/activate
|
||||||
|
pip install --upgrade setuptools
|
||||||
|
pip install --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
||||||
|
|
||||||
This installs synapse, along with the libraries it uses, into
|
This installs synapse, along with the libraries it uses, into a virtual
|
||||||
``$HOME/.local/lib/`` on Linux or ``$HOME/Library/Python/2.7/lib/`` on OSX.
|
environment under ``~/.synapse``. Feel free to pick a different directory
|
||||||
|
if you prefer.
|
||||||
|
|
||||||
Troubleshooting Installation
|
In case of problems, please see the _Troubleshooting section below.
|
||||||
----------------------------
|
|
||||||
|
|
||||||
Synapse requires pip 1.7 or later, so if your OS provides too old a version and
|
Alternatively, Silvio Fricke has contributed a Dockerfile to automate the
|
||||||
you get errors about ``error: no such option: --process-dependency-links`` you
|
above in Docker at https://registry.hub.docker.com/u/silviof/docker-matrix/.
|
||||||
may need to manually upgrade it::
|
|
||||||
|
|
||||||
$ sudo pip install --upgrade pip
|
To set up your homeserver, run (in your virtualenv, as before)::
|
||||||
|
|
||||||
If pip crashes mid-installation for reason (e.g. lost terminal), pip may
|
|
||||||
refuse to run until you remove the temporary installation directory it
|
|
||||||
created. To reset the installation::
|
|
||||||
|
|
||||||
$ rm -rf /tmp/pip_install_matrix
|
cd ~/.synapse
|
||||||
|
python -m synapse.app.homeserver \
|
||||||
pip seems to leak *lots* of memory during installation. For instance, a Linux
|
--server-name machine.my.domain.name \
|
||||||
host with 512MB of RAM may run out of memory whilst installing Twisted. If this
|
--config-path homeserver.yaml \
|
||||||
happens, you will have to individually install the dependencies which are
|
--generate-config
|
||||||
failing, e.g.::
|
|
||||||
|
|
||||||
$ pip install --user twisted
|
Substituting your host and domain name as appropriate.
|
||||||
|
|
||||||
On OSX, if you encounter clang: error: unknown argument: '-mno-fused-madd' you
|
This will generate you a config file that you can then customise, but it will
|
||||||
will need to export CFLAGS=-Qunused-arguments.
|
also generate a set of keys for you. These keys will allow your Home Server to
|
||||||
|
identify itself to other Home Servers, so don't lose or delete them. It would be
|
||||||
|
wise to back them up somewhere safe. If, for whatever reason, you do need to
|
||||||
|
change your Home Server's keys, you may find that other Home Servers have the
|
||||||
|
old key cached. If you update the signing key, you should change the name of the
|
||||||
|
key in the <server name>.signing.key file (the second word, which by default is
|
||||||
|
, 'auto') to something different.
|
||||||
|
|
||||||
|
By default, registration of new users is disabled. You can either enable
|
||||||
|
registration in the config by specifying ``enable_registration: true``
|
||||||
|
(it is then recommended to also set up CAPTCHA), or
|
||||||
|
you can use the command line to register new users::
|
||||||
|
|
||||||
|
$ source ~/.synapse/bin/activate
|
||||||
|
$ register_new_matrix_user -c homeserver.yaml https://localhost:8448
|
||||||
|
New user localpart: erikj
|
||||||
|
Password:
|
||||||
|
Confirm password:
|
||||||
|
Success!
|
||||||
|
|
||||||
|
For reliable VoIP calls to be routed via this homeserver, you MUST configure
|
||||||
|
a TURN server. See docs/turn-howto.rst for details.
|
||||||
|
|
||||||
|
Using PostgreSQL
|
||||||
|
================
|
||||||
|
|
||||||
|
As of Synapse 0.9, `PostgreSQL <http://www.postgresql.org>`_ is supported as an
|
||||||
|
alternative to the `SQLite <http://sqlite.org/>`_ database that Synapse has
|
||||||
|
traditionally used for convenience and simplicity.
|
||||||
|
|
||||||
|
The advantages of Postgres include:
|
||||||
|
|
||||||
|
* significant performance improvements due to the superior threading and
|
||||||
|
caching model, smarter query optimiser
|
||||||
|
* allowing the DB to be run on separate hardware
|
||||||
|
* allowing basic active/backup high-availability with a "hot spare" synapse
|
||||||
|
pointing at the same DB master, as well as enabling DB replication in
|
||||||
|
synapse itself.
|
||||||
|
|
||||||
|
The only disadvantage is that the code is relatively new as of April 2015 and
|
||||||
|
may have a few regressions relative to SQLite.
|
||||||
|
|
||||||
|
For information on how to install and use PostgreSQL, please see
|
||||||
|
`docs/postgres.rst <docs/postgres.rst>`_.
|
||||||
|
|
||||||
|
Running Synapse
|
||||||
|
===============
|
||||||
|
|
||||||
|
To actually run your new homeserver, pick a working directory for Synapse to
|
||||||
|
run (e.g. ``~/.synapse``), and::
|
||||||
|
|
||||||
|
cd ~/.synapse
|
||||||
|
source ./bin/activate
|
||||||
|
synctl start
|
||||||
|
|
||||||
|
Platform Specific Instructions
|
||||||
|
==============================
|
||||||
|
|
||||||
|
ArchLinux
|
||||||
|
---------
|
||||||
|
|
||||||
|
The quickest way to get up and running with ArchLinux is probably with Ivan
|
||||||
|
Shapovalov's AUR package from
|
||||||
|
https://aur.archlinux.org/packages/matrix-synapse/, which should pull in all
|
||||||
|
the necessary dependencies.
|
||||||
|
|
||||||
|
Alternatively, to install using pip a few changes may be needed as ArchLinux
|
||||||
|
defaults to python 3, but synapse currently assumes python 2.7 by default:
|
||||||
|
|
||||||
|
pip may be outdated (6.0.7-1 and needs to be upgraded to 6.0.8-1 )::
|
||||||
|
|
||||||
|
sudo pip2.7 install --upgrade pip
|
||||||
|
|
||||||
|
You also may need to explicitly specify python 2.7 again during the install
|
||||||
|
request::
|
||||||
|
|
||||||
|
pip2.7 install --process-dependency-links \
|
||||||
|
https://github.com/matrix-org/synapse/tarball/master
|
||||||
|
|
||||||
|
If you encounter an error with lib bcrypt causing an Wrong ELF Class:
|
||||||
|
ELFCLASS32 (x64 Systems), you may need to reinstall py-bcrypt to correctly
|
||||||
|
compile it under the right architecture. (This should not be needed if
|
||||||
|
installing under virtualenv)::
|
||||||
|
|
||||||
|
sudo pip2.7 uninstall py-bcrypt
|
||||||
|
sudo pip2.7 install py-bcrypt
|
||||||
|
|
||||||
|
During setup of Synapse you need to call python2.7 directly again::
|
||||||
|
|
||||||
|
cd ~/.synapse
|
||||||
|
python2.7 -m synapse.app.homeserver \
|
||||||
|
--server-name machine.my.domain.name \
|
||||||
|
--config-path homeserver.yaml \
|
||||||
|
--generate-config
|
||||||
|
|
||||||
|
...substituting your host and domain name as appropriate.
|
||||||
|
|
||||||
Windows Install
|
Windows Install
|
||||||
---------------
|
---------------
|
||||||
Synapse can be installed on Cygwin. It requires the following Cygwin packages:
|
Synapse can be installed on Cygwin. It requires the following Cygwin packages:
|
||||||
|
|
||||||
- gcc
|
- gcc
|
||||||
- git
|
- git
|
||||||
- libffi-devel
|
- libffi-devel
|
||||||
- openssl (and openssl-devel, python-openssl)
|
- openssl (and openssl-devel, python-openssl)
|
||||||
- python
|
- python
|
||||||
- python-setuptools
|
- python-setuptools
|
||||||
|
|
||||||
The content repository requires additional packages and will be unable to process
|
The content repository requires additional packages and will be unable to process
|
||||||
uploads without them:
|
uploads without them:
|
||||||
- libjpeg8
|
|
||||||
- libjpeg8-devel
|
- libjpeg8
|
||||||
- zlib
|
- libjpeg8-devel
|
||||||
|
- zlib
|
||||||
|
|
||||||
If you choose to install Synapse without these packages, you will need to reinstall
|
If you choose to install Synapse without these packages, you will need to reinstall
|
||||||
``pillow`` for changes to be applied, e.g. ``pip uninstall pillow`` ``pip install
|
``pillow`` for changes to be applied, e.g. ``pip uninstall pillow`` ``pip install
|
||||||
pillow --user``
|
pillow --user``
|
||||||
@@ -164,31 +274,44 @@ Troubleshooting:
|
|||||||
you do, you may need to create a symlink to ``libsodium.a`` so ``ld`` can find
|
you do, you may need to create a symlink to ``libsodium.a`` so ``ld`` can find
|
||||||
it: ``ln -s /usr/local/lib/libsodium.a /usr/lib/libsodium.a``
|
it: ``ln -s /usr/local/lib/libsodium.a /usr/lib/libsodium.a``
|
||||||
|
|
||||||
Running Your Homeserver
|
Troubleshooting
|
||||||
=======================
|
===============
|
||||||
|
|
||||||
To actually run your new homeserver, pick a working directory for Synapse to run
|
Troubleshooting Installation
|
||||||
(e.g. ``~/.synapse``), and::
|
----------------------------
|
||||||
|
|
||||||
$ mkdir ~/.synapse
|
Synapse requires pip 1.7 or later, so if your OS provides too old a version and
|
||||||
$ cd ~/.synapse
|
you get errors about ``error: no such option: --process-dependency-links`` you
|
||||||
|
may need to manually upgrade it::
|
||||||
$ # on Linux
|
|
||||||
$ ~/.local/bin/synctl start
|
sudo pip install --upgrade pip
|
||||||
|
|
||||||
$ # on OSX
|
Installing may fail with ``mock requires setuptools>=17.1. Aborting installation``.
|
||||||
$ ~/Library/Python/2.7/bin/synctl start
|
You can fix this by upgrading setuptools::
|
||||||
|
|
||||||
|
pip install --upgrade setuptools
|
||||||
|
|
||||||
|
If pip crashes mid-installation for reason (e.g. lost terminal), pip may
|
||||||
|
refuse to run until you remove the temporary installation directory it
|
||||||
|
created. To reset the installation::
|
||||||
|
|
||||||
|
rm -rf /tmp/pip_install_matrix
|
||||||
|
|
||||||
|
pip seems to leak *lots* of memory during installation. For instance, a Linux
|
||||||
|
host with 512MB of RAM may run out of memory whilst installing Twisted. If this
|
||||||
|
happens, you will have to individually install the dependencies which are
|
||||||
|
failing, e.g.::
|
||||||
|
|
||||||
|
pip install twisted
|
||||||
|
|
||||||
|
On OS X, if you encounter clang: error: unknown argument: '-mno-fused-madd' you
|
||||||
|
will need to export CFLAGS=-Qunused-arguments.
|
||||||
|
|
||||||
Troubleshooting Running
|
Troubleshooting Running
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
If ``synctl`` fails with ``pkg_resources.DistributionNotFound`` errors you may
|
If synapse fails with ``missing "sodium.h"`` crypto errors, you may need
|
||||||
need a newer version of setuptools than that provided by your OS.::
|
to manually upgrade PyNaCL, as synapse uses NaCl (http://nacl.cr.yp.to/) for
|
||||||
|
|
||||||
$ sudo pip install setuptools --upgrade
|
|
||||||
|
|
||||||
If synapse fails with ``missing "sodium.h"`` crypto errors, you may need
|
|
||||||
to manually upgrade PyNaCL, as synapse uses NaCl (http://nacl.cr.yp.to/) for
|
|
||||||
encryption and digital signatures.
|
encryption and digital signatures.
|
||||||
Unfortunately PyNACL currently has a few issues
|
Unfortunately PyNACL currently has a few issues
|
||||||
(https://github.com/pyca/pynacl/issues/53) and
|
(https://github.com/pyca/pynacl/issues/53) and
|
||||||
@@ -197,34 +320,46 @@ correctly, causing all tests to fail with errors about missing "sodium.h". To
|
|||||||
fix try re-installing from PyPI or directly from
|
fix try re-installing from PyPI or directly from
|
||||||
(https://github.com/pyca/pynacl)::
|
(https://github.com/pyca/pynacl)::
|
||||||
|
|
||||||
$ # Install from PyPI
|
# Install from PyPI
|
||||||
$ pip install --user --upgrade --force pynacl
|
pip install --user --upgrade --force pynacl
|
||||||
$ # Install from github
|
|
||||||
$ pip install --user https://github.com/pyca/pynacl/tarball/master
|
|
||||||
|
|
||||||
|
# Install from github
|
||||||
|
pip install --user https://github.com/pyca/pynacl/tarball/master
|
||||||
|
|
||||||
Homeserver Development
|
ArchLinux
|
||||||
======================
|
~~~~~~~~~
|
||||||
|
|
||||||
To check out a homeserver for development, clone the git repo into a working
|
If running `$ synctl start` fails with 'returned non-zero exit status 1',
|
||||||
|
you will need to explicitly call Python2.7 - either running as::
|
||||||
|
|
||||||
|
python2.7 -m synapse.app.homeserver --daemonize -c homeserver.yaml
|
||||||
|
|
||||||
|
...or by editing synctl with the correct python executable.
|
||||||
|
|
||||||
|
Synapse Development
|
||||||
|
===================
|
||||||
|
|
||||||
|
To check out a synapse for development, clone the git repo into a working
|
||||||
directory of your choice::
|
directory of your choice::
|
||||||
|
|
||||||
$ git clone https://github.com/matrix-org/synapse.git
|
git clone https://github.com/matrix-org/synapse.git
|
||||||
$ cd synapse
|
cd synapse
|
||||||
|
|
||||||
The homeserver has a number of external dependencies, that are easiest
|
Synapse has a number of external dependencies, that are easiest
|
||||||
to install by making setup.py do so, in --user mode::
|
to install using pip and a virtualenv::
|
||||||
|
|
||||||
$ python setup.py develop --user
|
virtualenv env
|
||||||
|
source env/bin/activate
|
||||||
|
python synapse/python_dependencies.py | xargs -n1 pip install
|
||||||
|
pip install setuptools_trial mock
|
||||||
|
|
||||||
This will run a process of downloading and installing into your
|
This will run a process of downloading and installing all the needed
|
||||||
user's .local/lib directory all of the required dependencies that are
|
dependencies into a virtual env.
|
||||||
missing.
|
|
||||||
|
|
||||||
Once this is done, you may wish to run the homeserver's unit tests, to
|
Once this is done, you may wish to run Synapse's unit tests, to
|
||||||
check that everything is installed as it should be::
|
check that everything is installed as it should be::
|
||||||
|
|
||||||
$ python setup.py test
|
python setup.py test
|
||||||
|
|
||||||
This should end with a 'PASSED' result::
|
This should end with a 'PASSED' result::
|
||||||
|
|
||||||
@@ -233,12 +368,14 @@ This should end with a 'PASSED' result::
|
|||||||
PASSED (successes=143)
|
PASSED (successes=143)
|
||||||
|
|
||||||
|
|
||||||
Upgrading an existing homeserver
|
Upgrading an existing Synapse
|
||||||
================================
|
=============================
|
||||||
|
|
||||||
IMPORTANT: Before upgrading an existing homeserver to a new version, please
|
The instructions for upgrading synapse are in `UPGRADE.rst`_.
|
||||||
refer to UPGRADE.rst for any additional instructions.
|
Please check these instructions as upgrading may require extra steps for some
|
||||||
|
versions of synapse.
|
||||||
|
|
||||||
|
.. _UPGRADE.rst: UPGRADE.rst
|
||||||
|
|
||||||
Setting up Federation
|
Setting up Federation
|
||||||
=====================
|
=====================
|
||||||
@@ -260,11 +397,11 @@ IDs:
|
|||||||
For the first form, simply pass the required hostname (of the machine) as the
|
For the first form, simply pass the required hostname (of the machine) as the
|
||||||
--server-name parameter::
|
--server-name parameter::
|
||||||
|
|
||||||
$ python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--server-name machine.my.domain.name \
|
--server-name machine.my.domain.name \
|
||||||
--config-path homeserver.config \
|
--config-path homeserver.yaml \
|
||||||
--generate-config
|
--generate-config
|
||||||
$ python -m synapse.app.homeserver --config-path homeserver.config
|
python -m synapse.app.homeserver --config-path homeserver.yaml
|
||||||
|
|
||||||
Alternatively, you can run ``synctl start`` to guide you through the process.
|
Alternatively, you can run ``synctl start`` to guide you through the process.
|
||||||
|
|
||||||
@@ -274,38 +411,33 @@ and port where the server is running. (At the current time synapse does not
|
|||||||
support clustering multiple servers into a single logical homeserver). The DNS
|
support clustering multiple servers into a single logical homeserver). The DNS
|
||||||
record would then look something like::
|
record would then look something like::
|
||||||
|
|
||||||
$ dig -t srv _matrix._tcp.machine.my.domaine.name
|
$ dig -t srv _matrix._tcp.machine.my.domain.name
|
||||||
_matrix._tcp IN SRV 10 0 8448 machine.my.domain.name.
|
_matrix._tcp IN SRV 10 0 8448 machine.my.domain.name.
|
||||||
|
|
||||||
|
|
||||||
At this point, you should then run the homeserver with the hostname of this
|
At this point, you should then run the homeserver with the hostname of this
|
||||||
SRV record, as that is the name other machines will expect it to have::
|
SRV record, as that is the name other machines will expect it to have::
|
||||||
|
|
||||||
$ python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--server-name YOURDOMAIN \
|
--server-name YOURDOMAIN \
|
||||||
--bind-port 8448 \
|
--config-path homeserver.yaml \
|
||||||
--config-path homeserver.config \
|
|
||||||
--generate-config
|
--generate-config
|
||||||
$ python -m synapse.app.homeserver --config-path homeserver.config
|
python -m synapse.app.homeserver --config-path homeserver.yaml
|
||||||
|
|
||||||
|
|
||||||
You may additionally want to pass one or more "-v" options, in order to
|
You may additionally want to pass one or more "-v" options, in order to
|
||||||
increase the verbosity of logging output; at least for initial testing.
|
increase the verbosity of logging output; at least for initial testing.
|
||||||
|
|
||||||
For the initial alpha release, the homeserver is not speaking TLS for
|
Running a Demo Federation of Synapses
|
||||||
either client-server or server-server traffic for ease of debugging. We have
|
-------------------------------------
|
||||||
also not spent any time yet getting the homeserver to run behind loadbalancers.
|
|
||||||
|
|
||||||
Running a Demo Federation of Homeservers
|
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
If you want to get up and running quickly with a trio of homeservers in a
|
If you want to get up and running quickly with a trio of homeservers in a
|
||||||
private federation (``localhost:8080``, ``localhost:8081`` and
|
private federation (``localhost:8080``, ``localhost:8081`` and
|
||||||
``localhost:8082``) which you can then access through the webclient running at
|
``localhost:8082``) which you can then access through the webclient running at
|
||||||
http://localhost:8080. Simply run::
|
http://localhost:8080. Simply run::
|
||||||
|
|
||||||
$ demo/start.sh
|
demo/start.sh
|
||||||
|
|
||||||
This is mainly useful just for development purposes.
|
This is mainly useful just for development purposes.
|
||||||
|
|
||||||
Running The Demo Web Client
|
Running The Demo Web Client
|
||||||
@@ -332,7 +464,10 @@ account. Your name will take the form of::
|
|||||||
Specify your desired localpart in the topmost box of the "Register for an
|
Specify your desired localpart in the topmost box of the "Register for an
|
||||||
account" form, and click the "Register" button. Hostnames can contain ports if
|
account" form, and click the "Register" button. Hostnames can contain ports if
|
||||||
required due to lack of SRV records (e.g. @matthew:localhost:8448 on an
|
required due to lack of SRV records (e.g. @matthew:localhost:8448 on an
|
||||||
internal synapse sandbox running on localhost)
|
internal synapse sandbox running on localhost).
|
||||||
|
|
||||||
|
If registration fails, you may need to enable it in the homeserver (see
|
||||||
|
`Synapse Installation`_ above)
|
||||||
|
|
||||||
|
|
||||||
Logging In To An Existing Account
|
Logging In To An Existing Account
|
||||||
@@ -358,14 +493,14 @@ track 3PID logins and publish end-user public keys.
|
|||||||
|
|
||||||
It's currently early days for identity servers as Matrix is not yet using 3PIDs
|
It's currently early days for identity servers as Matrix is not yet using 3PIDs
|
||||||
as the primary means of identity and E2E encryption is not complete. As such,
|
as the primary means of identity and E2E encryption is not complete. As such,
|
||||||
we are running a single identity server (http://matrix.org:8090) at the current
|
we are running a single identity server (https://matrix.org) at the current
|
||||||
time.
|
time.
|
||||||
|
|
||||||
|
|
||||||
Where's the spec?!
|
Where's the spec?!
|
||||||
==================
|
==================
|
||||||
|
|
||||||
The source of the matrix spec lives at https://github.com/matrix-org/matrix-doc.
|
The source of the matrix spec lives at https://github.com/matrix-org/matrix-doc.
|
||||||
A recent HTML snapshot of this lives at http://matrix.org/docs/spec
|
A recent HTML snapshot of this lives at http://matrix.org/docs/spec
|
||||||
|
|
||||||
|
|
||||||
@@ -375,10 +510,10 @@ Building Internal API Documentation
|
|||||||
Before building internal API documentation install sphinx and
|
Before building internal API documentation install sphinx and
|
||||||
sphinxcontrib-napoleon::
|
sphinxcontrib-napoleon::
|
||||||
|
|
||||||
$ pip install sphinx
|
pip install sphinx
|
||||||
$ pip install sphinxcontrib-napoleon
|
pip install sphinxcontrib-napoleon
|
||||||
|
|
||||||
Building internal API documentation::
|
Building internal API documentation::
|
||||||
|
|
||||||
$ python setup.py build_sphinx
|
python setup.py build_sphinx
|
||||||
|
|
||||||
|
|||||||
100
UPGRADE.rst
100
UPGRADE.rst
@@ -1,3 +1,99 @@
|
|||||||
|
Upgrading Synapse
|
||||||
|
=================
|
||||||
|
|
||||||
|
Before upgrading check if any special steps are required to upgrade from the
|
||||||
|
what you currently have installed to current version of synapse. The extra
|
||||||
|
instructions that may be required are listed later in this document.
|
||||||
|
|
||||||
|
If synapse was installed in a virtualenv then active that virtualenv before
|
||||||
|
upgrading. If synapse is installed in a virtualenv in ``~/.synapse/`` then run:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
source ~/.synapse/bin/activate
|
||||||
|
|
||||||
|
If synapse was installed using pip then upgrade to the latest version by
|
||||||
|
running:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
pip install --upgrade --process-dependency-links https://github.com/matrix-org/synapse/tarball/master
|
||||||
|
|
||||||
|
If synapse was installed using git then upgrade to the latest version by
|
||||||
|
running:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
# Pull the latest version of the master branch.
|
||||||
|
git pull
|
||||||
|
# Update the versions of synapse's python dependencies.
|
||||||
|
python synapse/python_dependencies.py | xargs -n1 pip install
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading to v0.9.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
Application services have had a breaking API change in this version.
|
||||||
|
|
||||||
|
They can no longer register themselves with a home server using the AS HTTP API. This
|
||||||
|
decision was made because a compromised application service with free reign to register
|
||||||
|
any regex in effect grants full read/write access to the home server if a regex of ``.*``
|
||||||
|
is used. An attack where a compromised AS re-registers itself with ``.*`` was deemed too
|
||||||
|
big of a security risk to ignore, and so the ability to register with the HS remotely has
|
||||||
|
been removed.
|
||||||
|
|
||||||
|
It has been replaced by specifying a list of application service registrations in
|
||||||
|
``homeserver.yaml``::
|
||||||
|
|
||||||
|
app_service_config_files: ["registration-01.yaml", "registration-02.yaml"]
|
||||||
|
|
||||||
|
Where ``registration-01.yaml`` looks like::
|
||||||
|
|
||||||
|
url: <String> # e.g. "https://my.application.service.com"
|
||||||
|
as_token: <String>
|
||||||
|
hs_token: <String>
|
||||||
|
sender_localpart: <String> # This is a new field which denotes the user_id localpart when using the AS token
|
||||||
|
namespaces:
|
||||||
|
users:
|
||||||
|
- exclusive: <Boolean>
|
||||||
|
regex: <String> # e.g. "@prefix_.*"
|
||||||
|
aliases:
|
||||||
|
- exclusive: <Boolean>
|
||||||
|
regex: <String>
|
||||||
|
rooms:
|
||||||
|
- exclusive: <Boolean>
|
||||||
|
regex: <String>
|
||||||
|
|
||||||
|
Upgrading to v0.8.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
Servers which use captchas will need to add their public key to::
|
||||||
|
|
||||||
|
static/client/register/register_config.js
|
||||||
|
|
||||||
|
window.matrixRegistrationConfig = {
|
||||||
|
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
||||||
|
};
|
||||||
|
|
||||||
|
This is required in order to support registration fallback (typically used on
|
||||||
|
mobile devices).
|
||||||
|
|
||||||
|
|
||||||
|
Upgrading to v0.7.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
New dependencies are:
|
||||||
|
|
||||||
|
- pydenticon
|
||||||
|
- simplejson
|
||||||
|
- syutil
|
||||||
|
- matrix-angular-sdk
|
||||||
|
|
||||||
|
To pull in these dependencies in a virtual env, run::
|
||||||
|
|
||||||
|
python synapse/python_dependencies.py | xargs -n 1 pip install
|
||||||
|
|
||||||
Upgrading to v0.6.0
|
Upgrading to v0.6.0
|
||||||
===================
|
===================
|
||||||
|
|
||||||
@@ -52,7 +148,7 @@ resulting conflicts during the upgrade process.
|
|||||||
Before running the command the homeserver should be first completely
|
Before running the command the homeserver should be first completely
|
||||||
shutdown. To run it, simply specify the location of the database, e.g.:
|
shutdown. To run it, simply specify the location of the database, e.g.:
|
||||||
|
|
||||||
./database-prepare-for-0.5.0.sh "homeserver.db"
|
./scripts/database-prepare-for-0.5.0.sh "homeserver.db"
|
||||||
|
|
||||||
Once this has successfully completed it will be safe to restart the
|
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
|
homeserver. You may notice that the homeserver takes a few seconds longer to
|
||||||
@@ -147,7 +243,7 @@ rooms the home server was a member of and room alias mappings.
|
|||||||
Before running the command the homeserver should be first completely
|
Before running the command the homeserver should be first completely
|
||||||
shutdown. To run it, simply specify the location of the database, e.g.:
|
shutdown. To run it, simply specify the location of the database, e.g.:
|
||||||
|
|
||||||
./database-prepare-for-0.0.1.sh "homeserver.db"
|
./scripts/database-prepare-for-0.0.1.sh "homeserver.db"
|
||||||
|
|
||||||
Once this has successfully completed it will be safe to restart the
|
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
|
homeserver. You may notice that the homeserver takes a few seconds longer to
|
||||||
|
|||||||
@@ -21,16 +21,30 @@ import datetime
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from synapse.events import FrozenEvent
|
from synapse.events import FrozenEvent
|
||||||
|
from synapse.util.frozenutils import unfreeze
|
||||||
|
|
||||||
|
|
||||||
def make_graph(db_name, room_id, file_prefix):
|
def make_graph(db_name, room_id, file_prefix, limit):
|
||||||
conn = sqlite3.connect(db_name)
|
conn = sqlite3.connect(db_name)
|
||||||
|
|
||||||
c = conn.execute(
|
sql = (
|
||||||
"SELECT json FROM event_json where room_id = ?",
|
"SELECT json FROM event_json as j "
|
||||||
(room_id,)
|
"INNER JOIN events as e ON e.event_id = j.event_id "
|
||||||
|
"WHERE j.room_id = ?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
args = [room_id]
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
sql += (
|
||||||
|
" ORDER BY topological_ordering DESC, stream_ordering DESC "
|
||||||
|
"LIMIT ?"
|
||||||
|
)
|
||||||
|
|
||||||
|
args.append(limit)
|
||||||
|
|
||||||
|
c = conn.execute(sql, args)
|
||||||
|
|
||||||
events = [FrozenEvent(json.loads(e[0])) for e in c.fetchall()]
|
events = [FrozenEvent(json.loads(e[0])) for e in c.fetchall()]
|
||||||
|
|
||||||
events.sort(key=lambda e: e.depth)
|
events.sort(key=lambda e: e.depth)
|
||||||
@@ -57,7 +71,7 @@ def make_graph(db_name, room_id, file_prefix):
|
|||||||
float(event.origin_server_ts) / 1000
|
float(event.origin_server_ts) / 1000
|
||||||
).strftime('%Y-%m-%d %H:%M:%S,%f')
|
).strftime('%Y-%m-%d %H:%M:%S,%f')
|
||||||
|
|
||||||
content = json.dumps(event.get_dict()["content"])
|
content = json.dumps(unfreeze(event.get_dict()["content"]))
|
||||||
|
|
||||||
label = (
|
label = (
|
||||||
"<"
|
"<"
|
||||||
@@ -128,11 +142,16 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-p", "--prefix", dest="prefix",
|
"-p", "--prefix", dest="prefix",
|
||||||
help="String to prefix output files with"
|
help="String to prefix output files with",
|
||||||
|
default="graph_output"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-l", "--limit",
|
||||||
|
help="Only retrieve the last N events.",
|
||||||
)
|
)
|
||||||
parser.add_argument('db')
|
parser.add_argument('db')
|
||||||
parser.add_argument('room')
|
parser.add_argument('room')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
make_graph(args.db, args.room, args.prefix)
|
make_graph(args.db, args.room, args.prefix, args.limit)
|
||||||
@@ -39,43 +39,43 @@ ROOMDOMAIN="meet.jit.si"
|
|||||||
#ROOMDOMAIN="conference.jitsi.vuc.me"
|
#ROOMDOMAIN="conference.jitsi.vuc.me"
|
||||||
|
|
||||||
class TrivialMatrixClient:
|
class TrivialMatrixClient:
|
||||||
def __init__(self, access_token):
|
def __init__(self, access_token):
|
||||||
self.token = None
|
self.token = None
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
|
|
||||||
def getEvent(self):
|
def getEvent(self):
|
||||||
while True:
|
while True:
|
||||||
url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
|
url = MATRIXBASE+'events?access_token='+self.access_token+"&timeout=60000"
|
||||||
if self.token:
|
if self.token:
|
||||||
url += "&from="+self.token
|
url += "&from="+self.token
|
||||||
req = grequests.get(url)
|
req = grequests.get(url)
|
||||||
resps = grequests.map([req])
|
resps = grequests.map([req])
|
||||||
obj = json.loads(resps[0].content)
|
obj = json.loads(resps[0].content)
|
||||||
print "incoming from matrix",obj
|
print "incoming from matrix",obj
|
||||||
if 'end' not in obj:
|
if 'end' not in obj:
|
||||||
continue
|
continue
|
||||||
self.token = obj['end']
|
self.token = obj['end']
|
||||||
if len(obj['chunk']):
|
if len(obj['chunk']):
|
||||||
return obj['chunk'][0]
|
return obj['chunk'][0]
|
||||||
|
|
||||||
def joinRoom(self, roomId):
|
def joinRoom(self, roomId):
|
||||||
url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
|
url = MATRIXBASE+'rooms/'+roomId+'/join?access_token='+self.access_token
|
||||||
print url
|
print url
|
||||||
headers={ 'Content-Type': 'application/json' }
|
headers={ 'Content-Type': 'application/json' }
|
||||||
req = grequests.post(url, headers=headers, data='{}')
|
req = grequests.post(url, headers=headers, data='{}')
|
||||||
resps = grequests.map([req])
|
resps = grequests.map([req])
|
||||||
obj = json.loads(resps[0].content)
|
obj = json.loads(resps[0].content)
|
||||||
print "response: ",obj
|
print "response: ",obj
|
||||||
|
|
||||||
def sendEvent(self, roomId, evType, event):
|
def sendEvent(self, roomId, evType, event):
|
||||||
url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
|
url = MATRIXBASE+'rooms/'+roomId+'/send/'+evType+'?access_token='+self.access_token
|
||||||
print url
|
print url
|
||||||
print json.dumps(event)
|
print json.dumps(event)
|
||||||
headers={ 'Content-Type': 'application/json' }
|
headers={ 'Content-Type': 'application/json' }
|
||||||
req = grequests.post(url, headers=headers, data=json.dumps(event))
|
req = grequests.post(url, headers=headers, data=json.dumps(event))
|
||||||
resps = grequests.map([req])
|
resps = grequests.map([req])
|
||||||
obj = json.loads(resps[0].content)
|
obj = json.loads(resps[0].content)
|
||||||
print "response: ",obj
|
print "response: ",obj
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -83,178 +83,178 @@ xmppClients = {}
|
|||||||
|
|
||||||
|
|
||||||
def matrixLoop():
|
def matrixLoop():
|
||||||
while True:
|
while True:
|
||||||
ev = matrixCli.getEvent()
|
ev = matrixCli.getEvent()
|
||||||
print ev
|
print ev
|
||||||
if ev['type'] == 'm.room.member':
|
if ev['type'] == 'm.room.member':
|
||||||
print 'membership event'
|
print 'membership event'
|
||||||
if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
|
if ev['membership'] == 'invite' and ev['state_key'] == MYUSERNAME:
|
||||||
roomId = ev['room_id']
|
roomId = ev['room_id']
|
||||||
print "joining room %s" % (roomId)
|
print "joining room %s" % (roomId)
|
||||||
matrixCli.joinRoom(roomId)
|
matrixCli.joinRoom(roomId)
|
||||||
elif ev['type'] == 'm.room.message':
|
elif ev['type'] == 'm.room.message':
|
||||||
if ev['room_id'] in xmppClients:
|
if ev['room_id'] in xmppClients:
|
||||||
print "already have a bridge for that user, ignoring"
|
print "already have a bridge for that user, ignoring"
|
||||||
continue
|
continue
|
||||||
print "got message, connecting"
|
print "got message, connecting"
|
||||||
xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
|
xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
|
||||||
gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
|
gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
|
||||||
elif ev['type'] == 'm.call.invite':
|
elif ev['type'] == 'm.call.invite':
|
||||||
print "Incoming call"
|
print "Incoming call"
|
||||||
#sdp = ev['content']['offer']['sdp']
|
#sdp = ev['content']['offer']['sdp']
|
||||||
#print "sdp: %s" % (sdp)
|
#print "sdp: %s" % (sdp)
|
||||||
#xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
|
#xmppClients[ev['room_id']] = TrivialXmppClient(ev['room_id'], ev['user_id'])
|
||||||
#gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
|
#gevent.spawn(xmppClients[ev['room_id']].xmppLoop)
|
||||||
elif ev['type'] == 'm.call.answer':
|
elif ev['type'] == 'm.call.answer':
|
||||||
print "Call answered"
|
print "Call answered"
|
||||||
sdp = ev['content']['answer']['sdp']
|
sdp = ev['content']['answer']['sdp']
|
||||||
if ev['room_id'] not in xmppClients:
|
if ev['room_id'] not in xmppClients:
|
||||||
print "We didn't have a call for that room"
|
print "We didn't have a call for that room"
|
||||||
continue
|
continue
|
||||||
# should probably check call ID too
|
# should probably check call ID too
|
||||||
xmppCli = xmppClients[ev['room_id']]
|
xmppCli = xmppClients[ev['room_id']]
|
||||||
xmppCli.sendAnswer(sdp)
|
xmppCli.sendAnswer(sdp)
|
||||||
elif ev['type'] == 'm.call.hangup':
|
elif ev['type'] == 'm.call.hangup':
|
||||||
if ev['room_id'] in xmppClients:
|
if ev['room_id'] in xmppClients:
|
||||||
xmppClients[ev['room_id']].stop()
|
xmppClients[ev['room_id']].stop()
|
||||||
del xmppClients[ev['room_id']]
|
del xmppClients[ev['room_id']]
|
||||||
|
|
||||||
class TrivialXmppClient:
|
class TrivialXmppClient:
|
||||||
def __init__(self, matrixRoom, userId):
|
def __init__(self, matrixRoom, userId):
|
||||||
self.rid = 0
|
self.rid = 0
|
||||||
self.matrixRoom = matrixRoom
|
self.matrixRoom = matrixRoom
|
||||||
self.userId = userId
|
self.userId = userId
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
def nextRid(self):
|
def nextRid(self):
|
||||||
self.rid += 1
|
self.rid += 1
|
||||||
return '%d' % (self.rid)
|
return '%d' % (self.rid)
|
||||||
|
|
||||||
def sendIq(self, xml):
|
def sendIq(self, xml):
|
||||||
fullXml = "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s'>%s</body>" % (self.nextRid(), self.sid, xml)
|
fullXml = "<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s'>%s</body>" % (self.nextRid(), self.sid, xml)
|
||||||
#print "\t>>>%s" % (fullXml)
|
#print "\t>>>%s" % (fullXml)
|
||||||
return self.xmppPoke(fullXml)
|
return self.xmppPoke(fullXml)
|
||||||
|
|
||||||
def xmppPoke(self, xml):
|
|
||||||
headers = {'Content-Type': 'application/xml'}
|
|
||||||
req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
|
|
||||||
resps = grequests.map([req])
|
|
||||||
obj = BeautifulSoup(resps[0].content)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def sendAnswer(self, answer):
|
def xmppPoke(self, xml):
|
||||||
print "sdp from matrix client",answer
|
headers = {'Content-Type': 'application/xml'}
|
||||||
p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--sdp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
req = grequests.post(HTTPBIND, verify=False, headers=headers, data=xml)
|
||||||
jingle, out_err = p.communicate(answer)
|
resps = grequests.map([req])
|
||||||
jingle = jingle % {
|
obj = BeautifulSoup(resps[0].content)
|
||||||
'tojid': self.callfrom,
|
return obj
|
||||||
'action': 'session-accept',
|
|
||||||
'initiator': self.callfrom,
|
|
||||||
'responder': self.jid,
|
|
||||||
'sid': self.callsid
|
|
||||||
}
|
|
||||||
print "answer jingle from sdp",jingle
|
|
||||||
res = self.sendIq(jingle)
|
|
||||||
print "reply from answer: ",res
|
|
||||||
|
|
||||||
self.ssrcs = {}
|
|
||||||
jingleSoup = BeautifulSoup(jingle)
|
|
||||||
for cont in jingleSoup.iq.jingle.findAll('content'):
|
|
||||||
if cont.description:
|
|
||||||
self.ssrcs[cont['name']] = cont.description['ssrc']
|
|
||||||
print "my ssrcs:",self.ssrcs
|
|
||||||
|
|
||||||
gevent.joinall([
|
def sendAnswer(self, answer):
|
||||||
gevent.spawn(self.advertiseSsrcs)
|
print "sdp from matrix client",answer
|
||||||
])
|
p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--sdp'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
jingle, out_err = p.communicate(answer)
|
||||||
def advertiseSsrcs(self):
|
jingle = jingle % {
|
||||||
|
'tojid': self.callfrom,
|
||||||
|
'action': 'session-accept',
|
||||||
|
'initiator': self.callfrom,
|
||||||
|
'responder': self.jid,
|
||||||
|
'sid': self.callsid
|
||||||
|
}
|
||||||
|
print "answer jingle from sdp",jingle
|
||||||
|
res = self.sendIq(jingle)
|
||||||
|
print "reply from answer: ",res
|
||||||
|
|
||||||
|
self.ssrcs = {}
|
||||||
|
jingleSoup = BeautifulSoup(jingle)
|
||||||
|
for cont in jingleSoup.iq.jingle.findAll('content'):
|
||||||
|
if cont.description:
|
||||||
|
self.ssrcs[cont['name']] = cont.description['ssrc']
|
||||||
|
print "my ssrcs:",self.ssrcs
|
||||||
|
|
||||||
|
gevent.joinall([
|
||||||
|
gevent.spawn(self.advertiseSsrcs)
|
||||||
|
])
|
||||||
|
|
||||||
|
def advertiseSsrcs(self):
|
||||||
time.sleep(7)
|
time.sleep(7)
|
||||||
print "SSRC spammer started"
|
print "SSRC spammer started"
|
||||||
while self.running:
|
while self.running:
|
||||||
ssrcMsg = "<presence to='%(tojid)s' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%(nick)s</nick><stats xmlns='http://jitsi.org/jitmeet/stats'><stat name='bitrate_download' value='175'/><stat name='bitrate_upload' value='176'/><stat name='packetLoss_total' value='0'/><stat name='packetLoss_download' value='0'/><stat name='packetLoss_upload' value='0'/></stats><media xmlns='http://estos.de/ns/mjs'><source type='audio' ssrc='%(assrc)s' direction='sendre'/><source type='video' ssrc='%(vssrc)s' direction='sendre'/></media></presence>" % { 'tojid': "%s@%s/%s" % (ROOMNAME, ROOMDOMAIN, self.shortJid), 'nick': self.userId, 'assrc': self.ssrcs['audio'], 'vssrc': self.ssrcs['video'] }
|
ssrcMsg = "<presence to='%(tojid)s' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%(nick)s</nick><stats xmlns='http://jitsi.org/jitmeet/stats'><stat name='bitrate_download' value='175'/><stat name='bitrate_upload' value='176'/><stat name='packetLoss_total' value='0'/><stat name='packetLoss_download' value='0'/><stat name='packetLoss_upload' value='0'/></stats><media xmlns='http://estos.de/ns/mjs'><source type='audio' ssrc='%(assrc)s' direction='sendre'/><source type='video' ssrc='%(vssrc)s' direction='sendre'/></media></presence>" % { 'tojid': "%s@%s/%s" % (ROOMNAME, ROOMDOMAIN, self.shortJid), 'nick': self.userId, 'assrc': self.ssrcs['audio'], 'vssrc': self.ssrcs['video'] }
|
||||||
res = self.sendIq(ssrcMsg)
|
res = self.sendIq(ssrcMsg)
|
||||||
print "reply from ssrc announce: ",res
|
print "reply from ssrc announce: ",res
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def xmppLoop(self):
|
|
||||||
self.matrixCallId = time.time()
|
|
||||||
res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' to='%s' xml:lang='en' wait='60' hold='1' content='text/xml; charset=utf-8' ver='1.6' xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), HOST))
|
|
||||||
|
|
||||||
print res
|
|
||||||
self.sid = res.body['sid']
|
|
||||||
print "sid %s" % (self.sid)
|
|
||||||
|
|
||||||
res = self.sendIq("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>")
|
def xmppLoop(self):
|
||||||
|
self.matrixCallId = time.time()
|
||||||
|
res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' to='%s' xml:lang='en' wait='60' hold='1' content='text/xml; charset=utf-8' ver='1.6' xmpp:version='1.0' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), HOST))
|
||||||
|
|
||||||
res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s' to='%s' xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), self.sid, HOST))
|
print res
|
||||||
|
self.sid = res.body['sid']
|
||||||
res = self.sendIq("<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>")
|
print "sid %s" % (self.sid)
|
||||||
print res
|
|
||||||
|
|
||||||
self.jid = res.body.iq.bind.jid.string
|
res = self.sendIq("<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='ANONYMOUS'/>")
|
||||||
print "jid: %s" % (self.jid)
|
|
||||||
self.shortJid = self.jid.split('-')[0]
|
|
||||||
|
|
||||||
res = self.sendIq("<iq type='set' id='_session_auth_2' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>")
|
res = self.xmppPoke("<body rid='%s' xmlns='http://jabber.org/protocol/httpbind' sid='%s' to='%s' xml:lang='en' xmpp:restart='true' xmlns:xmpp='urn:xmpp:xbosh'/>" % (self.nextRid(), self.sid, HOST))
|
||||||
|
|
||||||
#randomthing = res.body.iq['to']
|
res = self.sendIq("<iq type='set' id='_bind_auth_2' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>")
|
||||||
#whatsitpart = randomthing.split('-')[0]
|
print res
|
||||||
|
|
||||||
#print "other random bind thing: %s" % (randomthing)
|
self.jid = res.body.iq.bind.jid.string
|
||||||
|
print "jid: %s" % (self.jid)
|
||||||
|
self.shortJid = self.jid.split('-')[0]
|
||||||
|
|
||||||
# advertise preence to the jitsi room, with our nick
|
res = self.sendIq("<iq type='set' id='_session_auth_2' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>")
|
||||||
res = self.sendIq("<iq type='get' to='%s' xmlns='jabber:client' id='1:sendIQ'><services xmlns='urn:xmpp:extdisco:1'><service host='%s'/></services></iq><presence to='%s@%s/d98f6c40' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%s</nick></presence>" % (HOST, TURNSERVER, ROOMNAME, ROOMDOMAIN, self.userId))
|
|
||||||
self.muc = {'users': []}
|
|
||||||
for p in res.body.findAll('presence'):
|
|
||||||
u = {}
|
|
||||||
u['shortJid'] = p['from'].split('/')[1]
|
|
||||||
if p.c and p.c.nick:
|
|
||||||
u['nick'] = p.c.nick.string
|
|
||||||
self.muc['users'].append(u)
|
|
||||||
print "muc: ",self.muc
|
|
||||||
|
|
||||||
# wait for stuff
|
#randomthing = res.body.iq['to']
|
||||||
while True:
|
#whatsitpart = randomthing.split('-')[0]
|
||||||
print "waiting..."
|
|
||||||
res = self.sendIq("")
|
#print "other random bind thing: %s" % (randomthing)
|
||||||
print "got from stream: ",res
|
|
||||||
if res.body.iq:
|
# advertise preence to the jitsi room, with our nick
|
||||||
jingles = res.body.iq.findAll('jingle')
|
res = self.sendIq("<iq type='get' to='%s' xmlns='jabber:client' id='1:sendIQ'><services xmlns='urn:xmpp:extdisco:1'><service host='%s'/></services></iq><presence to='%s@%s/d98f6c40' xmlns='jabber:client'><x xmlns='http://jabber.org/protocol/muc'/><c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://jitsi.org/jitsimeet' ver='0WkSdhFnAUxrz4ImQQLdB80GFlE='/><nick xmlns='http://jabber.org/protocol/nick'>%s</nick></presence>" % (HOST, TURNSERVER, ROOMNAME, ROOMDOMAIN, self.userId))
|
||||||
if len(jingles):
|
self.muc = {'users': []}
|
||||||
self.callfrom = res.body.iq['from']
|
for p in res.body.findAll('presence'):
|
||||||
self.handleInvite(jingles[0])
|
u = {}
|
||||||
elif 'type' in res.body and res.body['type'] == 'terminate':
|
u['shortJid'] = p['from'].split('/')[1]
|
||||||
self.running = False
|
if p.c and p.c.nick:
|
||||||
del xmppClients[self.matrixRoom]
|
u['nick'] = p.c.nick.string
|
||||||
return
|
self.muc['users'].append(u)
|
||||||
|
print "muc: ",self.muc
|
||||||
|
|
||||||
|
# wait for stuff
|
||||||
|
while True:
|
||||||
|
print "waiting..."
|
||||||
|
res = self.sendIq("")
|
||||||
|
print "got from stream: ",res
|
||||||
|
if res.body.iq:
|
||||||
|
jingles = res.body.iq.findAll('jingle')
|
||||||
|
if len(jingles):
|
||||||
|
self.callfrom = res.body.iq['from']
|
||||||
|
self.handleInvite(jingles[0])
|
||||||
|
elif 'type' in res.body and res.body['type'] == 'terminate':
|
||||||
|
self.running = False
|
||||||
|
del xmppClients[self.matrixRoom]
|
||||||
|
return
|
||||||
|
|
||||||
|
def handleInvite(self, jingle):
|
||||||
|
self.initiator = jingle['initiator']
|
||||||
|
self.callsid = jingle['sid']
|
||||||
|
p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--jingle'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
print "raw jingle invite",str(jingle)
|
||||||
|
sdp, out_err = p.communicate(str(jingle))
|
||||||
|
print "transformed remote offer sdp",sdp
|
||||||
|
inviteEvent = {
|
||||||
|
'offer': {
|
||||||
|
'type': 'offer',
|
||||||
|
'sdp': sdp
|
||||||
|
},
|
||||||
|
'call_id': self.matrixCallId,
|
||||||
|
'version': 0,
|
||||||
|
'lifetime': 30000
|
||||||
|
}
|
||||||
|
matrixCli.sendEvent(self.matrixRoom, 'm.call.invite', inviteEvent)
|
||||||
|
|
||||||
def handleInvite(self, jingle):
|
|
||||||
self.initiator = jingle['initiator']
|
|
||||||
self.callsid = jingle['sid']
|
|
||||||
p = subprocess.Popen(['node', 'unjingle/unjingle.js', '--jingle'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
||||||
print "raw jingle invite",str(jingle)
|
|
||||||
sdp, out_err = p.communicate(str(jingle))
|
|
||||||
print "transformed remote offer sdp",sdp
|
|
||||||
inviteEvent = {
|
|
||||||
'offer': {
|
|
||||||
'type': 'offer',
|
|
||||||
'sdp': sdp
|
|
||||||
},
|
|
||||||
'call_id': self.matrixCallId,
|
|
||||||
'version': 0,
|
|
||||||
'lifetime': 30000
|
|
||||||
}
|
|
||||||
matrixCli.sendEvent(self.matrixRoom, 'm.call.invite', inviteEvent)
|
|
||||||
|
|
||||||
matrixCli = TrivialMatrixClient(ACCESS_TOKEN)
|
matrixCli = TrivialMatrixClient(ACCESS_TOKEN)
|
||||||
|
|
||||||
gevent.joinall([
|
gevent.joinall([
|
||||||
gevent.spawn(matrixLoop)
|
gevent.spawn(matrixLoop)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
93
contrib/scripts/kick_users.py
Executable file
93
contrib/scripts/kick_users.py
Executable file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
def _mkurl(template, kws):
|
||||||
|
for key in kws:
|
||||||
|
template = template.replace(key, kws[key])
|
||||||
|
return template
|
||||||
|
|
||||||
|
def main(hs, room_id, access_token, user_id_prefix, why):
|
||||||
|
if not why:
|
||||||
|
why = "Automated kick."
|
||||||
|
print "Kicking members on %s in room %s matching %s" % (hs, room_id, user_id_prefix)
|
||||||
|
room_state_url = _mkurl(
|
||||||
|
"$HS/_matrix/client/api/v1/rooms/$ROOM/state?access_token=$TOKEN",
|
||||||
|
{
|
||||||
|
"$HS": hs,
|
||||||
|
"$ROOM": room_id,
|
||||||
|
"$TOKEN": access_token
|
||||||
|
}
|
||||||
|
)
|
||||||
|
print "Getting room state => %s" % room_state_url
|
||||||
|
res = requests.get(room_state_url)
|
||||||
|
print "HTTP %s" % res.status_code
|
||||||
|
state_events = res.json()
|
||||||
|
if "error" in state_events:
|
||||||
|
print "FATAL"
|
||||||
|
print state_events
|
||||||
|
return
|
||||||
|
|
||||||
|
kick_list = []
|
||||||
|
room_name = room_id
|
||||||
|
for event in state_events:
|
||||||
|
if not event["type"] == "m.room.member":
|
||||||
|
if event["type"] == "m.room.name":
|
||||||
|
room_name = event["content"].get("name")
|
||||||
|
continue
|
||||||
|
if not event["content"].get("membership") == "join":
|
||||||
|
continue
|
||||||
|
if event["state_key"].startswith(user_id_prefix):
|
||||||
|
kick_list.append(event["state_key"])
|
||||||
|
|
||||||
|
if len(kick_list) == 0:
|
||||||
|
print "No user IDs match the prefix '%s'" % user_id_prefix
|
||||||
|
return
|
||||||
|
|
||||||
|
print "The following user IDs will be kicked from %s" % room_name
|
||||||
|
for uid in kick_list:
|
||||||
|
print uid
|
||||||
|
doit = raw_input("Continue? [Y]es\n")
|
||||||
|
if len(doit) > 0 and doit.lower() == 'y':
|
||||||
|
print "Kicking members..."
|
||||||
|
# encode them all
|
||||||
|
kick_list = [urllib.quote(uid) for uid in kick_list]
|
||||||
|
for uid in kick_list:
|
||||||
|
kick_url = _mkurl(
|
||||||
|
"$HS/_matrix/client/api/v1/rooms/$ROOM/state/m.room.member/$UID?access_token=$TOKEN",
|
||||||
|
{
|
||||||
|
"$HS": hs,
|
||||||
|
"$UID": uid,
|
||||||
|
"$ROOM": room_id,
|
||||||
|
"$TOKEN": access_token
|
||||||
|
}
|
||||||
|
)
|
||||||
|
kick_body = {
|
||||||
|
"membership": "leave",
|
||||||
|
"reason": why
|
||||||
|
}
|
||||||
|
print "Kicking %s" % uid
|
||||||
|
res = requests.put(kick_url, data=json.dumps(kick_body))
|
||||||
|
if res.status_code != 200:
|
||||||
|
print "ERROR: HTTP %s" % res.status_code
|
||||||
|
if res.json().get("error"):
|
||||||
|
print "ERROR: JSON %s" % res.json()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = ArgumentParser("Kick members in a room matching a certain user ID prefix.")
|
||||||
|
parser.add_argument("-u","--user-id",help="The user ID prefix e.g. '@irc_'")
|
||||||
|
parser.add_argument("-t","--token",help="Your access_token")
|
||||||
|
parser.add_argument("-r","--room",help="The room ID to kick members in")
|
||||||
|
parser.add_argument("-s","--homeserver",help="The base HS url e.g. http://matrix.org")
|
||||||
|
parser.add_argument("-w","--why",help="Reason for the kick. Optional.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if not args.room or not args.token or not args.user_id or not args.homeserver:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
main(args.homeserver, args.room, args.token, args.user_id, args.why)
|
||||||
25
contrib/systemd/log_config.yaml
Normal file
25
contrib/systemd/log_config.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
version: 1
|
||||||
|
|
||||||
|
# In systemd's journal, loglevel is implicitly stored, so let's omit it
|
||||||
|
# from the message text.
|
||||||
|
formatters:
|
||||||
|
journal_fmt:
|
||||||
|
format: '%(name)s: [%(request)s] %(message)s'
|
||||||
|
|
||||||
|
filters:
|
||||||
|
context:
|
||||||
|
(): synapse.util.logcontext.LoggingContextFilter
|
||||||
|
request: ""
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
journal:
|
||||||
|
class: systemd.journal.JournalHandler
|
||||||
|
formatter: journal_fmt
|
||||||
|
filters: [context]
|
||||||
|
SYSLOG_IDENTIFIER: synapse
|
||||||
|
|
||||||
|
root:
|
||||||
|
level: INFO
|
||||||
|
handlers: [journal]
|
||||||
|
|
||||||
|
disable_existing_loggers: False
|
||||||
16
contrib/systemd/synapse.service
Normal file
16
contrib/systemd/synapse.service
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# This assumes that Synapse has been installed as a system package
|
||||||
|
# (e.g. https://aur.archlinux.org/packages/matrix-synapse/ for ArchLinux)
|
||||||
|
# rather than in a user home directory or similar under virtualenv.
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Synapse Matrix homeserver
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=synapse
|
||||||
|
Group=synapse
|
||||||
|
WorkingDirectory=/var/lib/synapse
|
||||||
|
ExecStart=/usr/bin/python2.7 -m synapse.app.homeserver --config-path=/etc/synapse/homeserver.yaml --log-config=/etc/synapse/log_config.yaml
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -126,12 +126,26 @@ sub on_unknown_event
|
|||||||
if (!$bridgestate->{$room_id}->{gathered_candidates}) {
|
if (!$bridgestate->{$room_id}->{gathered_candidates}) {
|
||||||
$bridgestate->{$room_id}->{gathered_candidates} = 1;
|
$bridgestate->{$room_id}->{gathered_candidates} = 1;
|
||||||
my $offer = $bridgestate->{$room_id}->{offer};
|
my $offer = $bridgestate->{$room_id}->{offer};
|
||||||
my $candidate_block = "";
|
my $candidate_block = {
|
||||||
|
audio => '',
|
||||||
|
video => '',
|
||||||
|
};
|
||||||
foreach (@{$event->{content}->{candidates}}) {
|
foreach (@{$event->{content}->{candidates}}) {
|
||||||
$candidate_block .= "a=" . $_->{candidate} . "\r\n";
|
if ($_->{sdpMid}) {
|
||||||
|
$candidate_block->{$_->{sdpMid}} .= "a=" . $_->{candidate} . "\r\n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$candidate_block->{audio} .= "a=" . $_->{candidate} . "\r\n";
|
||||||
|
$candidate_block->{video} .= "a=" . $_->{candidate} . "\r\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
# XXX: collate using the right m= line - for now assume audio call
|
|
||||||
$offer =~ s/(a=rtcp.*[\r\n]+)/$1$candidate_block/;
|
# XXX: assumes audio comes first
|
||||||
|
#$offer =~ s/(a=rtcp-mux[\r\n]+)/$1$candidate_block->{audio}/;
|
||||||
|
#$offer =~ s/(a=rtcp-mux[\r\n]+)/$1$candidate_block->{video}/;
|
||||||
|
|
||||||
|
$offer =~ s/(m=video)/$candidate_block->{audio}$1/;
|
||||||
|
$offer =~ s/(.$)/$1\n$candidate_block->{video}$1/;
|
||||||
|
|
||||||
my $f = send_verto_json_request("verto.invite", {
|
my $f = send_verto_json_request("verto.invite", {
|
||||||
"sdp" => $offer,
|
"sdp" => $offer,
|
||||||
@@ -172,23 +186,18 @@ sub on_room_message
|
|||||||
warn "[Matrix] in $room_id: $from: " . $content->{body} . "\n";
|
warn "[Matrix] in $room_id: $from: " . $content->{body} . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
my $verto_connecting = $loop->new_future;
|
|
||||||
$bot_verto->connect(
|
|
||||||
%{ $CONFIG{"verto-bot"} },
|
|
||||||
on_connected => sub {
|
|
||||||
warn("[Verto] connected to websocket");
|
|
||||||
$verto_connecting->done($bot_verto) if not $verto_connecting->is_done;
|
|
||||||
},
|
|
||||||
on_connect_error => sub { die "Cannot connect to verto - $_[-1]" },
|
|
||||||
on_resolve_error => sub { die "Cannot resolve to verto - $_[-1]" },
|
|
||||||
);
|
|
||||||
|
|
||||||
Future->needs_all(
|
Future->needs_all(
|
||||||
$bot_matrix->login( %{ $CONFIG{"matrix-bot"} } )->then( sub {
|
$bot_matrix->login( %{ $CONFIG{"matrix-bot"} } )->then( sub {
|
||||||
$bot_matrix->start;
|
$bot_matrix->start;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
$verto_connecting,
|
$bot_verto->connect(
|
||||||
|
%{ $CONFIG{"verto-bot"} },
|
||||||
|
on_connect_error => sub { die "Cannot connect to verto - $_[-1]" },
|
||||||
|
on_resolve_error => sub { die "Cannot resolve to verto - $_[-1]" },
|
||||||
|
)->on_done( sub {
|
||||||
|
warn("[Verto] connected to websocket");
|
||||||
|
}),
|
||||||
)->get;
|
)->get;
|
||||||
|
|
||||||
$loop->attach_signal(
|
$loop->attach_signal(
|
||||||
|
|||||||
493
contrib/vertobot/bridge.pl
Executable file
493
contrib/vertobot/bridge.pl
Executable file
@@ -0,0 +1,493 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
use 5.010; # //
|
||||||
|
use IO::Socket::SSL qw(SSL_VERIFY_NONE);
|
||||||
|
use IO::Async::Loop;
|
||||||
|
use Net::Async::WebSocket::Client;
|
||||||
|
use Net::Async::HTTP;
|
||||||
|
use Net::Async::HTTP::Server;
|
||||||
|
use JSON;
|
||||||
|
use YAML;
|
||||||
|
use Data::UUID;
|
||||||
|
use Getopt::Long;
|
||||||
|
use Data::Dumper;
|
||||||
|
use URI::Encode qw(uri_encode uri_decode);
|
||||||
|
|
||||||
|
binmode STDOUT, ":encoding(UTF-8)";
|
||||||
|
binmode STDERR, ":encoding(UTF-8)";
|
||||||
|
|
||||||
|
my $msisdn_to_matrix = {
|
||||||
|
'447417892400' => '@matthew:matrix.org',
|
||||||
|
};
|
||||||
|
|
||||||
|
my $matrix_to_msisdn = {};
|
||||||
|
foreach (keys %$msisdn_to_matrix) {
|
||||||
|
$matrix_to_msisdn->{$msisdn_to_matrix->{$_}} = $_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
my $loop = IO::Async::Loop->new;
|
||||||
|
# Net::Async::HTTP + SSL + IO::Poll doesn't play well. See
|
||||||
|
# https://rt.cpan.org/Ticket/Display.html?id=93107
|
||||||
|
# ref $loop eq "IO::Async::Loop::Poll" and
|
||||||
|
# warn "Using SSL with IO::Poll causes known memory-leaks!!\n";
|
||||||
|
|
||||||
|
GetOptions(
|
||||||
|
'C|config=s' => \my $CONFIG,
|
||||||
|
'eval-from=s' => \my $EVAL_FROM,
|
||||||
|
) or exit 1;
|
||||||
|
|
||||||
|
if( defined $EVAL_FROM ) {
|
||||||
|
# An emergency 'eval() this file' hack
|
||||||
|
$SIG{HUP} = sub {
|
||||||
|
my $code = do {
|
||||||
|
open my $fh, "<", $EVAL_FROM or warn( "Cannot read - $!" ), return;
|
||||||
|
local $/; <$fh>
|
||||||
|
};
|
||||||
|
|
||||||
|
eval $code or warn "Cannot eval() - $@";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
defined $CONFIG or die "Must supply --config\n";
|
||||||
|
|
||||||
|
my %CONFIG = %{ YAML::LoadFile( $CONFIG ) };
|
||||||
|
|
||||||
|
my %MATRIX_CONFIG = %{ $CONFIG{matrix} };
|
||||||
|
# No harm in always applying this
|
||||||
|
$MATRIX_CONFIG{SSL_verify_mode} = SSL_VERIFY_NONE;
|
||||||
|
|
||||||
|
my $bridgestate = {};
|
||||||
|
my $roomid_by_callid = {};
|
||||||
|
|
||||||
|
my $sessid = lc new Data::UUID->create_str();
|
||||||
|
my $as_token = $CONFIG{"matrix-bot"}->{as_token};
|
||||||
|
my $hs_domain = $CONFIG{"matrix-bot"}->{domain};
|
||||||
|
|
||||||
|
my $http = Net::Async::HTTP->new();
|
||||||
|
$loop->add( $http );
|
||||||
|
|
||||||
|
sub create_virtual_user
|
||||||
|
{
|
||||||
|
my ($localpart) = @_;
|
||||||
|
my ( $response ) = $http->do_request(
|
||||||
|
method => "POST",
|
||||||
|
uri => URI->new(
|
||||||
|
$CONFIG{"matrix"}->{server}.
|
||||||
|
"/_matrix/client/api/v1/register?".
|
||||||
|
"access_token=$as_token&user_id=$localpart"
|
||||||
|
),
|
||||||
|
content_type => "application/json",
|
||||||
|
content => <<EOT
|
||||||
|
{
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"user": "$localpart"
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
)->get;
|
||||||
|
warn $response->as_string if ($response->code != 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
my $http_server = Net::Async::HTTP::Server->new(
|
||||||
|
on_request => sub {
|
||||||
|
my $self = shift;
|
||||||
|
my ( $req ) = @_;
|
||||||
|
|
||||||
|
my $response;
|
||||||
|
my $path = uri_decode($req->path);
|
||||||
|
warn("request: $path");
|
||||||
|
if ($path =~ m#/users/\@(\+.*)#) {
|
||||||
|
# when queried about virtual users, auto-create them in the HS
|
||||||
|
my $localpart = $1;
|
||||||
|
create_virtual_user($localpart);
|
||||||
|
$response = HTTP::Response->new( 200 );
|
||||||
|
$response->add_content('{}');
|
||||||
|
$response->content_type( "application/json" );
|
||||||
|
}
|
||||||
|
elsif ($path =~ m#/transactions/(.*)#) {
|
||||||
|
my $event = JSON->new->decode($req->body);
|
||||||
|
print Dumper($event);
|
||||||
|
|
||||||
|
my $room_id = $event->{room_id};
|
||||||
|
my %dp = %{$CONFIG{'verto-dialog-params'}};
|
||||||
|
$dp{callID} = $bridgestate->{$room_id}->{callid};
|
||||||
|
|
||||||
|
if ($event->{type} eq 'm.room.membership') {
|
||||||
|
my $membership = $event->{content}->{membership};
|
||||||
|
my $state_key = $event->{state_key};
|
||||||
|
my $room_id = $event->{state_id};
|
||||||
|
|
||||||
|
if ($membership eq 'invite') {
|
||||||
|
# autojoin invites
|
||||||
|
my ( $response ) = $http->do_request(
|
||||||
|
method => "POST",
|
||||||
|
uri => URI->new(
|
||||||
|
$CONFIG{"matrix"}->{server}.
|
||||||
|
"/_matrix/client/api/v1/rooms/$room_id/join?".
|
||||||
|
"access_token=$as_token&user_id=$state_key"
|
||||||
|
),
|
||||||
|
content_type => "application/json",
|
||||||
|
content => "{}",
|
||||||
|
)->get;
|
||||||
|
warn $response->as_string if ($response->code != 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elsif ($event->{type} eq 'm.call.invite') {
|
||||||
|
my $room_id = $event->{room_id};
|
||||||
|
$bridgestate->{$room_id}->{matrix_callid} = $event->{content}->{call_id};
|
||||||
|
$bridgestate->{$room_id}->{callid} = lc new Data::UUID->create_str();
|
||||||
|
$bridgestate->{$room_id}->{sessid} = $sessid;
|
||||||
|
# $bridgestate->{$room_id}->{offer} = $event->{content}->{offer}->{sdp};
|
||||||
|
my $offer = $event->{content}->{offer}->{sdp};
|
||||||
|
# $bridgestate->{$room_id}->{gathered_candidates} = 0;
|
||||||
|
$roomid_by_callid->{ $bridgestate->{$room_id}->{callid} } = $room_id;
|
||||||
|
# no trickle ICE in verto apparently
|
||||||
|
|
||||||
|
my $f = send_verto_json_request("verto.invite", {
|
||||||
|
"sdp" => $offer,
|
||||||
|
"dialogParams" => \%dp,
|
||||||
|
"sessid" => $bridgestate->{$room_id}->{sessid},
|
||||||
|
});
|
||||||
|
$self->adopt_future($f);
|
||||||
|
}
|
||||||
|
# elsif ($event->{type} eq 'm.call.candidates') {
|
||||||
|
# # XXX: this could fire for both matrix->verto and verto->matrix calls
|
||||||
|
# # and races as it collects candidates. much better to just turn off
|
||||||
|
# # candidate gathering in the webclient entirely for now
|
||||||
|
#
|
||||||
|
# my $room_id = $event->{room_id};
|
||||||
|
# # XXX: compare call IDs
|
||||||
|
# if (!$bridgestate->{$room_id}->{gathered_candidates}) {
|
||||||
|
# $bridgestate->{$room_id}->{gathered_candidates} = 1;
|
||||||
|
# my $offer = $bridgestate->{$room_id}->{offer};
|
||||||
|
# my $candidate_block = "";
|
||||||
|
# foreach (@{$event->{content}->{candidates}}) {
|
||||||
|
# $candidate_block .= "a=" . $_->{candidate} . "\r\n";
|
||||||
|
# }
|
||||||
|
# # XXX: collate using the right m= line - for now assume audio call
|
||||||
|
# $offer =~ s/(a=rtcp.*[\r\n]+)/$1$candidate_block/;
|
||||||
|
#
|
||||||
|
# my $f = send_verto_json_request("verto.invite", {
|
||||||
|
# "sdp" => $offer,
|
||||||
|
# "dialogParams" => \%dp,
|
||||||
|
# "sessid" => $bridgestate->{$room_id}->{sessid},
|
||||||
|
# });
|
||||||
|
# $self->adopt_future($f);
|
||||||
|
# }
|
||||||
|
# else {
|
||||||
|
# # ignore them, as no trickle ICE, although we might as well
|
||||||
|
# # batch them up
|
||||||
|
# # foreach (@{$event->{content}->{candidates}}) {
|
||||||
|
# # push @{$bridgestate->{$room_id}->{candidates}}, $_;
|
||||||
|
# # }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
elsif ($event->{type} eq 'm.call.answer') {
|
||||||
|
# grab the answer and relay it to verto as a verto.answer
|
||||||
|
my $room_id = $event->{room_id};
|
||||||
|
|
||||||
|
my $answer = $event->{content}->{answer}->{sdp};
|
||||||
|
my $f = send_verto_json_request("verto.answer", {
|
||||||
|
"sdp" => $answer,
|
||||||
|
"dialogParams" => \%dp,
|
||||||
|
"sessid" => $bridgestate->{$room_id}->{sessid},
|
||||||
|
});
|
||||||
|
$self->adopt_future($f);
|
||||||
|
}
|
||||||
|
elsif ($event->{type} eq 'm.call.hangup') {
|
||||||
|
my $room_id = $event->{room_id};
|
||||||
|
if ($bridgestate->{$room_id}->{matrix_callid} eq $event->{content}->{call_id}) {
|
||||||
|
my $f = send_verto_json_request("verto.bye", {
|
||||||
|
"dialogParams" => \%dp,
|
||||||
|
"sessid" => $bridgestate->{$room_id}->{sessid},
|
||||||
|
});
|
||||||
|
$self->adopt_future($f);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
warn "Ignoring unrecognised callid: ".$event->{content}->{call_id};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
warn "Unhandled event: $event->{type}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = HTTP::Response->new( 200 );
|
||||||
|
$response->add_content('{}');
|
||||||
|
$response->content_type( "application/json" );
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
warn "Unhandled path: $path";
|
||||||
|
$response = HTTP::Response->new( 404 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$req->respond( $response );
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$loop->add( $http_server );
|
||||||
|
|
||||||
|
$http_server->listen(
|
||||||
|
addr => { family => "inet", socktype => "stream", port => 8009 },
|
||||||
|
on_listen_error => sub { die "Cannot listen - $_[-1]\n" },
|
||||||
|
);
|
||||||
|
|
||||||
|
my $bot_verto = Net::Async::WebSocket::Client->new(
|
||||||
|
on_frame => sub {
|
||||||
|
my ( $self, $frame ) = @_;
|
||||||
|
warn "[Verto] receiving $frame";
|
||||||
|
on_verto_json($frame);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
$loop->add( $bot_verto );
|
||||||
|
|
||||||
|
my $verto_connecting = $loop->new_future;
|
||||||
|
$bot_verto->connect(
|
||||||
|
%{ $CONFIG{"verto-bot"} },
|
||||||
|
on_connected => sub {
|
||||||
|
warn("[Verto] connected to websocket");
|
||||||
|
if (not $verto_connecting->is_done) {
|
||||||
|
$verto_connecting->done($bot_verto);
|
||||||
|
|
||||||
|
send_verto_json_request("login", {
|
||||||
|
'login' => $CONFIG{'verto-dialog-params'}{'login'},
|
||||||
|
'passwd' => $CONFIG{'verto-config'}{'passwd'},
|
||||||
|
'sessid' => $sessid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
on_connect_error => sub { die "Cannot connect to verto - $_[-1]" },
|
||||||
|
on_resolve_error => sub { die "Cannot resolve to verto - $_[-1]" },
|
||||||
|
);
|
||||||
|
|
||||||
|
# die Dumper($verto_connecting);
|
||||||
|
|
||||||
|
my $as_url = $CONFIG{"matrix-bot"}->{as_url};
|
||||||
|
|
||||||
|
Future->needs_all(
|
||||||
|
$http->do_request(
|
||||||
|
method => "POST",
|
||||||
|
uri => URI->new( $CONFIG{"matrix"}->{server}."/_matrix/appservice/v1/register" ),
|
||||||
|
content_type => "application/json",
|
||||||
|
content => <<EOT
|
||||||
|
{
|
||||||
|
"as_token": "$as_token",
|
||||||
|
"url": "$as_url",
|
||||||
|
"namespaces": { "users": [ { "regex": "\@\\\\+.*", "exclusive": false } ] }
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
)->then( sub{
|
||||||
|
my ($response) = (@_);
|
||||||
|
warn $response->as_string if ($response->code != 200);
|
||||||
|
return Future->done;
|
||||||
|
}),
|
||||||
|
$verto_connecting,
|
||||||
|
)->get;
|
||||||
|
|
||||||
|
$loop->attach_signal(
|
||||||
|
PIPE => sub { warn "pipe\n" }
|
||||||
|
);
|
||||||
|
$loop->attach_signal(
|
||||||
|
INT => sub { $loop->stop },
|
||||||
|
);
|
||||||
|
$loop->attach_signal(
|
||||||
|
TERM => sub { $loop->stop },
|
||||||
|
);
|
||||||
|
|
||||||
|
eval {
|
||||||
|
$loop->run;
|
||||||
|
} or my $e = $@;
|
||||||
|
|
||||||
|
die $e if $e;
|
||||||
|
|
||||||
|
exit 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
my $json_id;
|
||||||
|
my $requests;
|
||||||
|
|
||||||
|
sub send_verto_json_request
|
||||||
|
{
|
||||||
|
$json_id ||= 1;
|
||||||
|
|
||||||
|
my ($method, $params) = @_;
|
||||||
|
my $json = {
|
||||||
|
jsonrpc => "2.0",
|
||||||
|
method => $method,
|
||||||
|
params => $params,
|
||||||
|
id => $json_id,
|
||||||
|
};
|
||||||
|
my $text = JSON->new->encode( $json );
|
||||||
|
warn "[Verto] sending $text";
|
||||||
|
$bot_verto->send_frame ( $text );
|
||||||
|
my $request = $loop->new_future;
|
||||||
|
$requests->{$json_id} = $request;
|
||||||
|
$json_id++;
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub send_verto_json_response
|
||||||
|
{
|
||||||
|
my ($result, $id) = @_;
|
||||||
|
my $json = {
|
||||||
|
jsonrpc => "2.0",
|
||||||
|
result => $result,
|
||||||
|
id => $id,
|
||||||
|
};
|
||||||
|
my $text = JSON->new->encode( $json );
|
||||||
|
warn "[Verto] sending $text";
|
||||||
|
$bot_verto->send_frame ( $text );
|
||||||
|
}
|
||||||
|
|
||||||
|
sub on_verto_json
|
||||||
|
{
|
||||||
|
my $json = JSON->new->decode( $_[0] );
|
||||||
|
if ($json->{method}) {
|
||||||
|
if (($json->{method} eq 'verto.answer' && $json->{params}->{sdp}) ||
|
||||||
|
$json->{method} eq 'verto.media') {
|
||||||
|
|
||||||
|
my $caller = $json->{dialogParams}->{caller_id_number};
|
||||||
|
my $callee = $json->{dialogParams}->{destination_number};
|
||||||
|
my $caller_user = '@+' . $caller . ':' . $hs_domain;
|
||||||
|
my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";
|
||||||
|
my $room_id = $roomid_by_callid->{$json->{params}->{callID}};
|
||||||
|
|
||||||
|
if ($json->{params}->{sdp}) {
|
||||||
|
$http->do_request(
|
||||||
|
method => "POST",
|
||||||
|
uri => URI->new(
|
||||||
|
$CONFIG{"matrix"}->{server}.
|
||||||
|
"/_matrix/client/api/v1/send/m.call.answer?".
|
||||||
|
"access_token=$as_token&user_id=$caller_user"
|
||||||
|
),
|
||||||
|
content_type => "application/json",
|
||||||
|
content => JSON->new->encode({
|
||||||
|
call_id => $bridgestate->{$room_id}->{matrix_callid},
|
||||||
|
version => 0,
|
||||||
|
answer => {
|
||||||
|
sdp => $json->{params}->{sdp},
|
||||||
|
type => "answer",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)->then( sub {
|
||||||
|
send_verto_json_response( {
|
||||||
|
method => $json->{method},
|
||||||
|
}, $json->{id});
|
||||||
|
})->get;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elsif ($json->{method} eq 'verto.invite') {
|
||||||
|
my $caller = $json->{dialogParams}->{caller_id_number};
|
||||||
|
my $callee = $json->{dialogParams}->{destination_number};
|
||||||
|
my $caller_user = '@+' . $caller . ':' . $hs_domain;
|
||||||
|
my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";
|
||||||
|
|
||||||
|
my $alias = ($caller lt $callee) ? ($caller.'-'.$callee) : ($callee.'-'.$caller);
|
||||||
|
my $room_id;
|
||||||
|
|
||||||
|
# create a virtual user for the caller if needed.
|
||||||
|
create_virtual_user($caller);
|
||||||
|
|
||||||
|
# create a room of form #peer-peer and invite the callee
|
||||||
|
$http->do_request(
|
||||||
|
method => "POST",
|
||||||
|
uri => URI->new(
|
||||||
|
$CONFIG{"matrix"}->{server}.
|
||||||
|
"/_matrix/client/api/v1/createRoom?".
|
||||||
|
"access_token=$as_token&user_id=$caller_user"
|
||||||
|
),
|
||||||
|
content_type => "application/json",
|
||||||
|
content => JSON->new->encode({
|
||||||
|
room_alias_name => $alias,
|
||||||
|
invite => [ $callee_user ],
|
||||||
|
}),
|
||||||
|
)->then( sub {
|
||||||
|
my ( $response ) = @_;
|
||||||
|
my $resp = JSON->new->decode($response->content);
|
||||||
|
$room_id = $resp->{room_id};
|
||||||
|
$roomid_by_callid->{$json->{params}->{callID}} = $room_id;
|
||||||
|
})->get;
|
||||||
|
|
||||||
|
# join it
|
||||||
|
my ($response) = $http->do_request(
|
||||||
|
method => "POST",
|
||||||
|
uri => URI->new(
|
||||||
|
$CONFIG{"matrix"}->{server}.
|
||||||
|
"/_matrix/client/api/v1/join/$room_id?".
|
||||||
|
"access_token=$as_token&user_id=$caller_user"
|
||||||
|
),
|
||||||
|
content_type => "application/json",
|
||||||
|
content => '{}',
|
||||||
|
)->get;
|
||||||
|
|
||||||
|
$bridgestate->{$room_id}->{matrix_callid} = lc new Data::UUID->create_str();
|
||||||
|
$bridgestate->{$room_id}->{callid} = $json->{dialogParams}->{callID};
|
||||||
|
$bridgestate->{$room_id}->{sessid} = $sessid;
|
||||||
|
|
||||||
|
# put the m.call.invite in there
|
||||||
|
$http->do_request(
|
||||||
|
method => "POST",
|
||||||
|
uri => URI->new(
|
||||||
|
$CONFIG{"matrix"}->{server}.
|
||||||
|
"/_matrix/client/api/v1/send/m.call.invite?".
|
||||||
|
"access_token=$as_token&user_id=$caller_user"
|
||||||
|
),
|
||||||
|
content_type => "application/json",
|
||||||
|
content => JSON->new->encode({
|
||||||
|
call_id => $bridgestate->{$room_id}->{matrix_callid},
|
||||||
|
version => 0,
|
||||||
|
answer => {
|
||||||
|
sdp => $json->{params}->{sdp},
|
||||||
|
type => "offer",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)->then( sub {
|
||||||
|
# acknowledge the verto
|
||||||
|
send_verto_json_response( {
|
||||||
|
method => $json->{method},
|
||||||
|
}, $json->{id});
|
||||||
|
})->get;
|
||||||
|
}
|
||||||
|
elsif ($json->{method} eq 'verto.bye') {
|
||||||
|
my $caller = $json->{dialogParams}->{caller_id_number};
|
||||||
|
my $callee = $json->{dialogParams}->{destination_number};
|
||||||
|
my $caller_user = '@+' . $caller . ':' . $hs_domain;
|
||||||
|
my $callee_user = $msisdn_to_matrix->{$callee} || warn "unrecogised callee: $callee";
|
||||||
|
my $room_id = $roomid_by_callid->{$json->{params}->{callID}};
|
||||||
|
|
||||||
|
# put the m.call.hangup into the room
|
||||||
|
$http->do_request(
|
||||||
|
method => "POST",
|
||||||
|
uri => URI->new(
|
||||||
|
$CONFIG{"matrix"}->{server}.
|
||||||
|
"/_matrix/client/api/v1/send/m.call.hangup?".
|
||||||
|
"access_token=$as_token&user_id=$caller_user"
|
||||||
|
),
|
||||||
|
content_type => "application/json",
|
||||||
|
content => JSON->new->encode({
|
||||||
|
call_id => $bridgestate->{$room_id}->{matrix_callid},
|
||||||
|
version => 0,
|
||||||
|
}),
|
||||||
|
)->then( sub {
|
||||||
|
# acknowledge the verto
|
||||||
|
send_verto_json_response( {
|
||||||
|
method => $json->{method},
|
||||||
|
}, $json->{id});
|
||||||
|
})->get;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
warn ("[Verto] unhandled method: " . $json->{method});
|
||||||
|
send_verto_json_response( {
|
||||||
|
method => $json->{method},
|
||||||
|
}, $json->{id});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elsif ($json->{result}) {
|
||||||
|
$requests->{$json->{id}}->done($json->{result});
|
||||||
|
}
|
||||||
|
elsif ($json->{error}) {
|
||||||
|
$requests->{$json->{id}}->fail($json->{error}->{message}, $json->{error});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,9 @@ matrix:
|
|||||||
matrix-bot:
|
matrix-bot:
|
||||||
user_id: '@vertobot:matrix.org'
|
user_id: '@vertobot:matrix.org'
|
||||||
password: ''
|
password: ''
|
||||||
|
domain: 'matrix.org"
|
||||||
|
as_url: 'http://localhost:8009'
|
||||||
|
as_token: 'vertobot123'
|
||||||
|
|
||||||
verto-bot:
|
verto-bot:
|
||||||
host: webrtc.freeswitch.org
|
host: webrtc.freeswitch.org
|
||||||
|
|||||||
@@ -11,7 +11,4 @@ requires 'YAML', 0;
|
|||||||
requires 'JSON', 0;
|
requires 'JSON', 0;
|
||||||
requires 'Getopt::Long', 0;
|
requires 'Getopt::Long', 0;
|
||||||
|
|
||||||
on 'test' => sub {
|
|
||||||
requires 'Test::More', '>= 0.98';
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# This is will prepare a synapse database for running with v0.5.0 of synapse.
|
|
||||||
# It will store all the user information, but will *delete* all messages and
|
|
||||||
# room data.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cp "$1" "$1.bak"
|
|
||||||
|
|
||||||
DUMP=$(sqlite3 "$1" << 'EOF'
|
|
||||||
.dump users
|
|
||||||
.dump access_tokens
|
|
||||||
.dump presence
|
|
||||||
.dump profiles
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
|
|
||||||
rm "$1"
|
|
||||||
|
|
||||||
sqlite3 "$1" <<< "$DUMP"
|
|
||||||
@@ -11,7 +11,9 @@ if [ -f $PID_FILE ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$DIR" -name "*.log" -delete
|
for port in 8080 8081 8082; do
|
||||||
find "$DIR" -name "*.db" -delete
|
rm -rf $DIR/$port
|
||||||
|
rm -rf $DIR/media_store.$port
|
||||||
|
done
|
||||||
|
|
||||||
rm -rf $DIR/etc
|
rm -rf $DIR/etc
|
||||||
|
|||||||
@@ -8,36 +8,41 @@ cd "$DIR/.."
|
|||||||
|
|
||||||
mkdir -p demo/etc
|
mkdir -p demo/etc
|
||||||
|
|
||||||
# Check the --no-rate-limit param
|
export PYTHONPATH=$(readlink -f $(pwd))
|
||||||
PARAMS=""
|
|
||||||
if [ $# -eq 1 ]; then
|
|
||||||
if [ $1 = "--no-rate-limit" ]; then
|
echo $PYTHONPATH
|
||||||
PARAMS="--rc-messages-per-second 1000 --rc-message-burst-count 1000"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
for port in 8080 8081 8082; do
|
for port in 8080 8081 8082; do
|
||||||
echo "Starting server on port $port... "
|
echo "Starting server on port $port... "
|
||||||
|
|
||||||
https_port=$((port + 400))
|
https_port=$((port + 400))
|
||||||
|
mkdir -p demo/$port
|
||||||
|
pushd demo/$port
|
||||||
|
|
||||||
|
#rm $DIR/etc/$port.config
|
||||||
python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--generate-config \
|
--generate-config \
|
||||||
--config-path "demo/etc/$port.config" \
|
|
||||||
-p "$https_port" \
|
|
||||||
--unsecure-port "$port" \
|
|
||||||
-H "localhost:$https_port" \
|
-H "localhost:$https_port" \
|
||||||
-f "$DIR/$port.log" \
|
--config-path "$DIR/etc/$port.config" \
|
||||||
-d "$DIR/$port.db" \
|
|
||||||
-D --pid-file "$DIR/$port.pid" \
|
# Check script parameters
|
||||||
--manhole $((port + 1000)) \
|
if [ $# -eq 1 ]; then
|
||||||
--tls-dh-params-path "demo/demo.tls.dh" \
|
if [ $1 = "--no-rate-limit" ]; then
|
||||||
$PARAMS $SYNAPSE_PARAMS
|
# Set high limits in config file to disable rate limiting
|
||||||
|
perl -p -i -e 's/rc_messages_per_second.*/rc_messages_per_second: 1000/g' $DIR/etc/$port.config
|
||||||
|
perl -p -i -e 's/rc_message_burst_count.*/rc_message_burst_count: 1000/g' $DIR/etc/$port.config
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
perl -p -i -e 's/^enable_registration:.*/enable_registration: true/g' $DIR/etc/$port.config
|
||||||
|
|
||||||
python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
--config-path "demo/etc/$port.config" \
|
--config-path "$DIR/etc/$port.config" \
|
||||||
|
-D \
|
||||||
-vv \
|
-vv \
|
||||||
|
|
||||||
|
popd
|
||||||
done
|
done
|
||||||
|
|
||||||
cd "$CWD"
|
cd "$CWD"
|
||||||
|
|||||||
31
docs/CAPTCHA_SETUP
Normal file
31
docs/CAPTCHA_SETUP
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Captcha can be enabled for this home server. This file explains how to do that.
|
||||||
|
The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google.
|
||||||
|
|
||||||
|
Getting keys
|
||||||
|
------------
|
||||||
|
Requires a public/private key pair from:
|
||||||
|
|
||||||
|
https://developers.google.com/recaptcha/
|
||||||
|
|
||||||
|
|
||||||
|
Setting ReCaptcha Keys
|
||||||
|
----------------------
|
||||||
|
The keys are a config option on the home server config. If they are not
|
||||||
|
visible, you can generate them via --generate-config. Set the following value:
|
||||||
|
|
||||||
|
recaptcha_public_key: YOUR_PUBLIC_KEY
|
||||||
|
recaptcha_private_key: YOUR_PRIVATE_KEY
|
||||||
|
|
||||||
|
In addition, you MUST enable captchas via:
|
||||||
|
|
||||||
|
enable_registration_captcha: true
|
||||||
|
|
||||||
|
Configuring IP used for auth
|
||||||
|
----------------------------
|
||||||
|
The ReCaptcha API requires that the IP address of the user who solved the
|
||||||
|
captcha is sent. If the client is connecting through a proxy or load balancer,
|
||||||
|
it may be required to use the X-Forwarded-For (XFF) header instead of the origin
|
||||||
|
IP address. This can be configured as an option on the home server like so:
|
||||||
|
|
||||||
|
captcha_ip_origin_is_x_forwarded: true
|
||||||
|
|
||||||
36
docs/application_services.rst
Normal file
36
docs/application_services.rst
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
Registering an Application Service
|
||||||
|
==================================
|
||||||
|
|
||||||
|
The registration of new application services depends on the homeserver used.
|
||||||
|
In synapse, you need to create a new configuration file for your AS and add it
|
||||||
|
to the list specified under the ``app_service_config_files`` config
|
||||||
|
option in your synapse config.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
app_service_config_files:
|
||||||
|
- /home/matrix/.synapse/<your-AS>.yaml
|
||||||
|
|
||||||
|
|
||||||
|
The format of the AS configuration file is as follows:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
url: <base url of AS>
|
||||||
|
as_token: <token AS will add to requests to HS>
|
||||||
|
hs_token: <token HS will add to requests to AS>
|
||||||
|
sender_localpart: <localpart of AS user>
|
||||||
|
namespaces:
|
||||||
|
users: # List of users we're interested in
|
||||||
|
- exclusive: <bool>
|
||||||
|
regex: <regex>
|
||||||
|
- ...
|
||||||
|
aliases: [] # List of aliases we're interested in
|
||||||
|
rooms: [] # List of room ids we're interested in
|
||||||
|
|
||||||
|
See the spec_ for further details on how application services work.
|
||||||
|
|
||||||
|
.. _spec: https://github.com/matrix-org/matrix-doc/blob/master/specification/25_application_service_api.rst#application-service-api
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
Media Repository
|
Media Repository
|
||||||
================
|
================
|
||||||
|
|
||||||
|
*Synapse implementation-specific details for the media repository*
|
||||||
|
|
||||||
The media repository is where attachments and avatar photos are stored.
|
The media repository is where attachments and avatar photos are stored.
|
||||||
It stores attachment content and thumbnails for media uploaded by local users.
|
It stores attachment content and thumbnails for media uploaded by local users.
|
||||||
It caches attachment content and thumbnails for media uploaded by remote users.
|
It caches attachment content and thumbnails for media uploaded by remote users.
|
||||||
|
|||||||
50
docs/metrics-howto.rst
Normal file
50
docs/metrics-howto.rst
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
How to monitor Synapse metrics using Prometheus
|
||||||
|
===============================================
|
||||||
|
|
||||||
|
1: Install prometheus:
|
||||||
|
Follow instructions at http://prometheus.io/docs/introduction/install/
|
||||||
|
|
||||||
|
2: Enable synapse metrics:
|
||||||
|
Simply setting a (local) port number will enable it. Pick a port.
|
||||||
|
prometheus itself defaults to 9090, so starting just above that for
|
||||||
|
locally monitored services seems reasonable. E.g. 9092:
|
||||||
|
|
||||||
|
Add to homeserver.yaml
|
||||||
|
|
||||||
|
metrics_port: 9092
|
||||||
|
|
||||||
|
Restart synapse
|
||||||
|
|
||||||
|
3: Check out synapse-prometheus-config
|
||||||
|
https://github.com/matrix-org/synapse-prometheus-config
|
||||||
|
|
||||||
|
4: Add ``synapse.html`` and ``synapse.rules``
|
||||||
|
The ``.html`` file needs to appear in prometheus's ``consoles`` directory,
|
||||||
|
and the ``.rules`` file needs to be invoked somewhere in the main config
|
||||||
|
file. A symlink to each from the git checkout into the prometheus directory
|
||||||
|
might be easiest to ensure ``git pull`` keeps it updated.
|
||||||
|
|
||||||
|
5: Add a prometheus target for synapse
|
||||||
|
This is easiest if prometheus runs on the same machine as synapse, as it can
|
||||||
|
then just use localhost::
|
||||||
|
|
||||||
|
global: {
|
||||||
|
rule_file: "synapse.rules"
|
||||||
|
}
|
||||||
|
|
||||||
|
job: {
|
||||||
|
name: "synapse"
|
||||||
|
|
||||||
|
target_group: {
|
||||||
|
target: "http://localhost:9092/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
6: Start prometheus::
|
||||||
|
|
||||||
|
./prometheus -config.file=prometheus.conf
|
||||||
|
|
||||||
|
7: Wait a few seconds for it to start and perform the first scrape,
|
||||||
|
then visit the console:
|
||||||
|
|
||||||
|
http://server-where-prometheus-runs:9090/consoles/synapse.html
|
||||||
107
docs/postgres.rst
Normal file
107
docs/postgres.rst
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
Using Postgres
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Set up database
|
||||||
|
===============
|
||||||
|
|
||||||
|
The PostgreSQL database used *must* have the correct encoding set, otherwise
|
||||||
|
would not be able to store UTF8 strings. To create a database with the correct
|
||||||
|
encoding use, e.g.::
|
||||||
|
|
||||||
|
CREATE DATABASE synapse
|
||||||
|
ENCODING 'UTF8'
|
||||||
|
LC_COLLATE='C'
|
||||||
|
LC_CTYPE='C'
|
||||||
|
template=template0
|
||||||
|
OWNER synapse_user;
|
||||||
|
|
||||||
|
This would create an appropriate database named ``synapse`` owned by the
|
||||||
|
``synapse_user`` user (which must already exist).
|
||||||
|
|
||||||
|
Set up client
|
||||||
|
=============
|
||||||
|
|
||||||
|
Postgres support depends on the postgres python connector ``psycopg2``. In the
|
||||||
|
virtual env::
|
||||||
|
|
||||||
|
sudo apt-get install libpq-dev
|
||||||
|
pip install psycopg2
|
||||||
|
|
||||||
|
|
||||||
|
Synapse config
|
||||||
|
==============
|
||||||
|
|
||||||
|
When you are ready to start using PostgreSQL, add the following line to your
|
||||||
|
config file::
|
||||||
|
|
||||||
|
database:
|
||||||
|
name: psycopg2
|
||||||
|
args:
|
||||||
|
user: <user>
|
||||||
|
password: <pass>
|
||||||
|
database: <db>
|
||||||
|
host: <host>
|
||||||
|
cp_min: 5
|
||||||
|
cp_max: 10
|
||||||
|
|
||||||
|
All key, values in ``args`` are passed to the ``psycopg2.connect(..)``
|
||||||
|
function, except keys beginning with ``cp_``, which are consumed by the twisted
|
||||||
|
adbapi connection pool.
|
||||||
|
|
||||||
|
|
||||||
|
Porting from SQLite
|
||||||
|
===================
|
||||||
|
|
||||||
|
Overview
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
The script ``synapse_port_db`` allows porting an existing synapse server
|
||||||
|
backed by SQLite to using PostgreSQL. This is done in as a two phase process:
|
||||||
|
|
||||||
|
1. Copy the existing SQLite database to a separate location (while the server
|
||||||
|
is down) and running the port script against that offline database.
|
||||||
|
2. Shut down the server. Rerun the port script to port any data that has come
|
||||||
|
in since taking the first snapshot. Restart server against the PostgreSQL
|
||||||
|
database.
|
||||||
|
|
||||||
|
The port script is designed to be run repeatedly against newer snapshots of the
|
||||||
|
SQLite database file. This makes it safe to repeat step 1 if there was a delay
|
||||||
|
between taking the previous snapshot and being ready to do step 2.
|
||||||
|
|
||||||
|
It is safe to at any time kill the port script and restart it.
|
||||||
|
|
||||||
|
Using the port script
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Firstly, shut down the currently running synapse server and copy its database
|
||||||
|
file (typically ``homeserver.db``) to another location. Once the copy is
|
||||||
|
complete, restart synapse. For instance::
|
||||||
|
|
||||||
|
./synctl stop
|
||||||
|
cp homeserver.db homeserver.db.snapshot
|
||||||
|
./synctl start
|
||||||
|
|
||||||
|
Assuming your new config file (as described in the section *Synapse config*)
|
||||||
|
is named ``homeserver-postgres.yaml`` and the SQLite snapshot is at
|
||||||
|
``homeserver.db.snapshot`` then simply run::
|
||||||
|
|
||||||
|
synapse_port_db --sqlite-database homeserver.db.snapshot \
|
||||||
|
--postgres-config homeserver-postgres.yaml
|
||||||
|
|
||||||
|
The flag ``--curses`` displays a coloured curses progress UI.
|
||||||
|
|
||||||
|
If the script took a long time to complete, or time has otherwise passed since
|
||||||
|
the original snapshot was taken, repeat the previous steps with a newer
|
||||||
|
snapshot.
|
||||||
|
|
||||||
|
To complete the conversion shut down the synapse server and run the port
|
||||||
|
script one last time, e.g. if the SQLite database is at ``homeserver.db``
|
||||||
|
run::
|
||||||
|
|
||||||
|
synapse_port_db --sqlite-database homeserver.db \
|
||||||
|
--postgres-config database_config.yaml
|
||||||
|
|
||||||
|
Once that has completed, change the synapse config to point at the PostgreSQL
|
||||||
|
database configuration file using the ``database_config`` parameter (see
|
||||||
|
`Synapse Config`_) and restart synapse. Synapse should now be running against
|
||||||
|
PostgreSQL.
|
||||||
@@ -81,7 +81,7 @@ Your home server configuration file needs the following extra keys:
|
|||||||
As an example, here is the relevant section of the config file for
|
As an example, here is the relevant section of the config file for
|
||||||
matrix.org::
|
matrix.org::
|
||||||
|
|
||||||
turn_uris: turn:turn.matrix.org:3478?transport=udp,turn:turn.matrix.org:3478?transport=tcp
|
turn_uris: [ "turn:turn.matrix.org:3478?transport=udp", "turn:turn.matrix.org:3478?transport=tcp" ]
|
||||||
turn_shared_secret: n0t4ctuAllymatr1Xd0TorgSshar3d5ecret4obvIousreAsons
|
turn_shared_secret: n0t4ctuAllymatr1Xd0TorgSshar3d5ecret4obvIousreAsons
|
||||||
turn_user_lifetime: 86400000
|
turn_user_lifetime: 86400000
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
.loggedin {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
table
|
|
||||||
{
|
|
||||||
border-spacing:5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,td
|
|
||||||
{
|
|
||||||
padding:5px;
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<div>
|
|
||||||
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8008</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>
|
|
||||||
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
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:8008/_matrix/client/api/v1/login",
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
showLoggedIn(data);
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
var errMsg = "To try this, you need a home server running!";
|
|
||||||
var errJson = $.parseJSON(err.responseText);
|
|
||||||
if (errJson) {
|
|
||||||
errMsg = JSON.stringify(errJson);
|
|
||||||
}
|
|
||||||
alert(errMsg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var getCurrentRoomList = function() {
|
|
||||||
var url = "http://localhost:8008/_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:8008/_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:8008/_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)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
.loggedin {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
table
|
|
||||||
{
|
|
||||||
border-spacing:5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,td
|
|
||||||
{
|
|
||||||
padding:5px;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<div>
|
|
||||||
<p>This event stream demo requires a home server to be running on http://localhost:8008</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>
|
|
||||||
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
var accountInfo = {};
|
|
||||||
|
|
||||||
var eventStreamInfo = {
|
|
||||||
from: "END"
|
|
||||||
};
|
|
||||||
|
|
||||||
var roomInfo = [];
|
|
||||||
|
|
||||||
var longpollEventStream = function() {
|
|
||||||
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
|
||||||
url = url.replace("$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:8008/_matrix/client/api/v1/login",
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
$("#rooms").find("tr:gt(0)").remove();
|
|
||||||
showLoggedIn(data);
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
var errMsg = "To try this, you need a home server running!";
|
|
||||||
var errJson = $.parseJSON(err.responseText);
|
|
||||||
if (errJson) {
|
|
||||||
errMsg = JSON.stringify(errJson);
|
|
||||||
}
|
|
||||||
alert(errMsg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var getCurrentRoomList = function() {
|
|
||||||
$("#roomId").val("");
|
|
||||||
var url = "http://localhost:8008/_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:8008/_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);
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
name: Example Matrix Client
|
|
||||||
description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists.
|
|
||||||
authors:
|
|
||||||
- matrix.org
|
|
||||||
resources:
|
|
||||||
- http://matrix.org
|
|
||||||
normalize_css: no
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<div class="signUp">
|
|
||||||
<p>Matrix example application: Requires a local home server running at http://localhost:8008</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>
|
|
||||||
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
var accountInfo = {};
|
|
||||||
|
|
||||||
var eventStreamInfo = {
|
|
||||||
from: "END"
|
|
||||||
};
|
|
||||||
|
|
||||||
var roomInfo = [];
|
|
||||||
var memberInfo = [];
|
|
||||||
var viewingRoomId;
|
|
||||||
|
|
||||||
// ************** Event Streaming **************
|
|
||||||
var longpollEventStream = function() {
|
|
||||||
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
|
||||||
url = url.replace("$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:8008/_matrix/client/api/v1/login",
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
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:8008/_matrix/client/api/v1/register",
|
|
||||||
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) {
|
|
||||||
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:8008/_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:8008/_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:8008/_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:8008/_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:8008/_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:8008/_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 === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (data.content.membership === "invite") {
|
|
||||||
msg = "<em>invited " + data.state_key + " to the room</em>";
|
|
||||||
}
|
|
||||||
else if (data.content.membership === "join") {
|
|
||||||
msg = "<em>joined the room</em>";
|
|
||||||
}
|
|
||||||
else if (data.content.membership === "leave") {
|
|
||||||
msg = "<em>left the room</em>";
|
|
||||||
}
|
|
||||||
else if (data.content.membership === "ban") {
|
|
||||||
msg = "<em>was banned from the room</em>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (msg === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var row = "<tr>" +
|
|
||||||
"<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);
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
.loggedin {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<div>
|
|
||||||
<p>This registration/login demo requires a home server to be running on http://localhost:8008</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>
|
|
||||||
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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:8008/_matrix/client/api/v1/register",
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
showLoggedIn(data);
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
var errMsg = "To try this, you need a home server running!";
|
|
||||||
var errJson = $.parseJSON(err.responseText);
|
|
||||||
if (errJson) {
|
|
||||||
errMsg = JSON.stringify(errJson);
|
|
||||||
}
|
|
||||||
alert(errMsg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var login = function(user, password) {
|
|
||||||
$.ajax({
|
|
||||||
url: "http://localhost:8008/_matrix/client/api/v1/login",
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
showLoggedIn(data);
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
var errMsg = "To try this, you need a home server running!";
|
|
||||||
var errJson = $.parseJSON(err.responseText);
|
|
||||||
if (errJson) {
|
|
||||||
errMsg = JSON.stringify(errJson);
|
|
||||||
}
|
|
||||||
alert(errMsg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$('.login').live('click', function() {
|
|
||||||
var user = $("#userLogin").val();
|
|
||||||
var password = $("#passwordLogin").val();
|
|
||||||
$.getJSON("http://localhost:8008/_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:8008/_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)));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
.loggedin {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
table
|
|
||||||
{
|
|
||||||
border-spacing:5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,td
|
|
||||||
{
|
|
||||||
padding:5px;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<div>
|
|
||||||
<p>This room membership demo requires a home server to be running on http://localhost:8008</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>
|
|
||||||
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
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:8008/_matrix/client/api/v1/login",
|
|
||||||
type: "POST",
|
|
||||||
contentType: "application/json; charset=utf-8",
|
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
$("#rooms").find("tr:gt(0)").remove();
|
|
||||||
showLoggedIn(data);
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
var errMsg = "To try this, you need a home server running!";
|
|
||||||
var errJson = $.parseJSON(err.responseText);
|
|
||||||
if (errJson) {
|
|
||||||
errMsg = JSON.stringify(errJson);
|
|
||||||
}
|
|
||||||
alert(errMsg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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:8008/_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:8008/_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:8008/_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:8008/_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)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
64
scripts-dev/check_auth.py
Normal file
64
scripts-dev/check_auth.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from synapse.events import FrozenEvent
|
||||||
|
from synapse.api.auth import Auth
|
||||||
|
|
||||||
|
from mock import Mock
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def check_auth(auth, auth_chain, events):
|
||||||
|
auth_chain.sort(key=lambda e: e.depth)
|
||||||
|
|
||||||
|
auth_map = {
|
||||||
|
e.event_id: e
|
||||||
|
for e in auth_chain
|
||||||
|
}
|
||||||
|
|
||||||
|
create_events = {}
|
||||||
|
for e in auth_chain:
|
||||||
|
if e.type == "m.room.create":
|
||||||
|
create_events[e.room_id] = e
|
||||||
|
|
||||||
|
for e in itertools.chain(auth_chain, events):
|
||||||
|
auth_events_list = [auth_map[i] for i, _ in e.auth_events]
|
||||||
|
|
||||||
|
auth_events = {
|
||||||
|
(e.type, e.state_key): e
|
||||||
|
for e in auth_events_list
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_events[("m.room.create", "")] = create_events[e.room_id]
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth.check(e, auth_events=auth_events)
|
||||||
|
except Exception as ex:
|
||||||
|
print "Failed:", e.event_id, e.type, e.state_key
|
||||||
|
print "Auth_events:", auth_events
|
||||||
|
print ex
|
||||||
|
print json.dumps(e.get_dict(), sort_keys=True, indent=4)
|
||||||
|
# raise
|
||||||
|
print "Success:", e.event_id, e.type, e.state_key
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'json',
|
||||||
|
nargs='?',
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
default=sys.stdin,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
js = json.load(args.json)
|
||||||
|
|
||||||
|
auth = Auth(Mock())
|
||||||
|
check_auth(
|
||||||
|
auth,
|
||||||
|
[FrozenEvent(d) for d in js["auth_chain"]],
|
||||||
|
[FrozenEvent(d) for d in js.get("pdus", [])],
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from synapse.crypto.event_signing import *
|
from synapse.crypto.event_signing import *
|
||||||
from syutil.base64util import encode_base64
|
from unpaddedbase64 import encode_base64
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
|
|
||||||
from syutil.crypto.jsonsign import verify_signed_json
|
from signedjson.sign import verify_signed_json
|
||||||
from syutil.crypto.signing_key import (
|
from signedjson.key import decode_verify_key_bytes, write_signing_keys
|
||||||
decode_verify_key_bytes, write_signing_keys
|
from unpaddedbase64 import decode_base64
|
||||||
)
|
|
||||||
from syutil.base64util import decode_base64
|
|
||||||
|
|
||||||
import urllib2
|
import urllib2
|
||||||
import json
|
import json
|
||||||
116
scripts-dev/convert_server_keys.py
Normal file
116
scripts-dev/convert_server_keys.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import psycopg2
|
||||||
|
import yaml
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
from unpaddedbase64 import encode_base64
|
||||||
|
from signedjson.key import read_signing_keys
|
||||||
|
from signedjson.sign import sign_json
|
||||||
|
from canonicaljson import encode_canonical_json
|
||||||
|
|
||||||
|
|
||||||
|
def select_v1_keys(connection):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("SELECT server_name, key_id, verify_key FROM server_signature_keys")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
results = {}
|
||||||
|
for server_name, key_id, verify_key in rows:
|
||||||
|
results.setdefault(server_name, {})[key_id] = encode_base64(verify_key)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def select_v1_certs(connection):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("SELECT server_name, tls_certificate FROM server_tls_certificates")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
results = {}
|
||||||
|
for server_name, tls_certificate in rows:
|
||||||
|
results[server_name] = tls_certificate
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def select_v2_json(connection):
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.execute("SELECT server_name, key_id, key_json FROM server_keys_json")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
cursor.close()
|
||||||
|
results = {}
|
||||||
|
for server_name, key_id, key_json in rows:
|
||||||
|
results.setdefault(server_name, {})[key_id] = json.loads(str(key_json).decode("utf-8"))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def convert_v1_to_v2(server_name, valid_until, keys, certificate):
|
||||||
|
return {
|
||||||
|
"old_verify_keys": {},
|
||||||
|
"server_name": server_name,
|
||||||
|
"verify_keys": {
|
||||||
|
key_id: {"key": key}
|
||||||
|
for key_id, key in keys.items()
|
||||||
|
},
|
||||||
|
"valid_until_ts": valid_until,
|
||||||
|
"tls_fingerprints": [fingerprint(certificate)],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def fingerprint(certificate):
|
||||||
|
finger = hashlib.sha256(certificate)
|
||||||
|
return {"sha256": encode_base64(finger.digest())}
|
||||||
|
|
||||||
|
|
||||||
|
def rows_v2(server, json):
|
||||||
|
valid_until = json["valid_until_ts"]
|
||||||
|
key_json = encode_canonical_json(json)
|
||||||
|
for key_id in json["verify_keys"]:
|
||||||
|
yield (server, key_id, "-", valid_until, valid_until, buffer(key_json))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
config = yaml.load(open(sys.argv[1]))
|
||||||
|
valid_until = int(time.time() / (3600 * 24)) * 1000 * 3600 * 24
|
||||||
|
|
||||||
|
server_name = config["server_name"]
|
||||||
|
signing_key = read_signing_keys(open(config["signing_key_path"]))[0]
|
||||||
|
|
||||||
|
database = config["database"]
|
||||||
|
assert database["name"] == "psycopg2", "Can only convert for postgresql"
|
||||||
|
args = database["args"]
|
||||||
|
args.pop("cp_max")
|
||||||
|
args.pop("cp_min")
|
||||||
|
connection = psycopg2.connect(**args)
|
||||||
|
keys = select_v1_keys(connection)
|
||||||
|
certificates = select_v1_certs(connection)
|
||||||
|
json = select_v2_json(connection)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for server in keys:
|
||||||
|
if not server in json:
|
||||||
|
v2_json = convert_v1_to_v2(
|
||||||
|
server, valid_until, keys[server], certificates[server]
|
||||||
|
)
|
||||||
|
v2_json = sign_json(v2_json, server_name, signing_key)
|
||||||
|
result[server] = v2_json
|
||||||
|
|
||||||
|
yaml.safe_dump(result, sys.stdout, default_flow_style=False)
|
||||||
|
|
||||||
|
rows = list(
|
||||||
|
row for server, json in result.items()
|
||||||
|
for row in rows_v2(server, json)
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
cursor.executemany(
|
||||||
|
"INSERT INTO server_keys_json ("
|
||||||
|
" server_name, key_id, from_server,"
|
||||||
|
" ts_added_ms, ts_valid_until_ms, key_json"
|
||||||
|
") VALUES (%s, %s, %s, %s, %s, %s)",
|
||||||
|
rows
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
33
scripts-dev/copyrighter-sql.pl
Executable file
33
scripts-dev/copyrighter-sql.pl
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/perl -pi
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
$copyright = <<EOT;
|
||||||
|
/* Copyright 2015 OpenMarket Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
EOT
|
||||||
|
|
||||||
|
s/^(# -\*- coding: utf-8 -\*-\n)?/$1$copyright/ if ($. == 1);
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
$copyright = <<EOT;
|
$copyright = <<EOT;
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -97,8 +97,11 @@ def lookup(destination, path):
|
|||||||
if ":" in destination:
|
if ":" in destination:
|
||||||
return "https://%s%s" % (destination, path)
|
return "https://%s%s" % (destination, path)
|
||||||
else:
|
else:
|
||||||
srv = srvlookup.lookup("matrix", "tcp", destination)[0]
|
try:
|
||||||
return "https://%s:%d%s" % (srv.host, srv.port, path)
|
srv = srvlookup.lookup("matrix", "tcp", destination)[0]
|
||||||
|
return "https://%s:%d%s" % (srv.host, srv.port, path)
|
||||||
|
except:
|
||||||
|
return "https://%s:%d%s" % (destination, 8448, path)
|
||||||
|
|
||||||
def get_json(origin_name, origin_key, destination, path):
|
def get_json(origin_name, origin_key, destination, path):
|
||||||
request_json = {
|
request_json = {
|
||||||
@@ -6,8 +6,8 @@ from synapse.crypto.event_signing import (
|
|||||||
add_event_pdu_content_hash, compute_pdu_event_reference_hash
|
add_event_pdu_content_hash, compute_pdu_event_reference_hash
|
||||||
)
|
)
|
||||||
from synapse.api.events.utils import prune_pdu
|
from synapse.api.events.utils import prune_pdu
|
||||||
from syutil.base64util import encode_base64, decode_base64
|
from unpaddedbase64 import encode_base64, decode_base64
|
||||||
from syutil.jsonutil import encode_canonical_json
|
from canonicaljson import encode_canonical_json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
39
scripts-dev/make_identicons.pl
Executable file
39
scripts-dev/make_identicons.pl
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
use DBI;
|
||||||
|
use DBD::SQLite;
|
||||||
|
use JSON;
|
||||||
|
use Getopt::Long;
|
||||||
|
|
||||||
|
my $db; # = "homeserver.db";
|
||||||
|
my $server = "http://localhost:8008";
|
||||||
|
my $size = 320;
|
||||||
|
|
||||||
|
GetOptions("db|d=s", \$db,
|
||||||
|
"server|s=s", \$server,
|
||||||
|
"width|w=i", \$size) or usage();
|
||||||
|
|
||||||
|
usage() unless $db;
|
||||||
|
|
||||||
|
my $dbh = DBI->connect("dbi:SQLite:dbname=$db","","") || die $DBI::errstr;
|
||||||
|
|
||||||
|
my $res = $dbh->selectall_arrayref("select token, name from access_tokens, users where access_tokens.user_id = users.id group by user_id") || die $DBI::errstr;
|
||||||
|
|
||||||
|
foreach (@$res) {
|
||||||
|
my ($token, $mxid) = ($_->[0], $_->[1]);
|
||||||
|
my ($user_id) = ($mxid =~ m/@(.*):/);
|
||||||
|
my ($url) = $dbh->selectrow_array("select avatar_url from profiles where user_id=?", undef, $user_id);
|
||||||
|
if (!$url || $url =~ /#auto$/) {
|
||||||
|
`curl -s -o tmp.png "$server/_matrix/media/v1/identicon?name=${mxid}&width=$size&height=$size"`;
|
||||||
|
my $json = `curl -s -X POST -H "Content-Type: image/png" -T "tmp.png" $server/_matrix/media/v1/upload?access_token=$token`;
|
||||||
|
my $content_uri = from_json($json)->{content_uri};
|
||||||
|
`curl -X PUT -H "Content-Type: application/json" --data '{ "avatar_url": "${content_uri}#auto"}' $server/_matrix/client/api/v1/profile/${mxid}/avatar_url?access_token=$token`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sub usage {
|
||||||
|
die "usage: ./make-identicons.pl\n\t-d database [e.g. homeserver.db]\n\t-s homeserver (default: http://localhost:8008)\n\t-w identicon size in pixels (default 320)";
|
||||||
|
}
|
||||||
154
scripts/register_new_matrix_user
Executable file
154
scripts/register_new_matrix_user
Executable file
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib2
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
def request_registration(user, password, server_location, shared_secret):
|
||||||
|
mac = hmac.new(
|
||||||
|
key=shared_secret,
|
||||||
|
msg=user,
|
||||||
|
digestmod=hashlib.sha1,
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"user": user,
|
||||||
|
"password": password,
|
||||||
|
"mac": mac,
|
||||||
|
"type": "org.matrix.login.shared_secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
server_location = server_location.rstrip("/")
|
||||||
|
|
||||||
|
print "Sending registration request..."
|
||||||
|
|
||||||
|
req = urllib2.Request(
|
||||||
|
"%s/_matrix/client/api/v1/register" % (server_location,),
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers={'Content-Type': 'application/json'}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if sys.version_info[:3] >= (2, 7, 9):
|
||||||
|
# As of version 2.7.9, urllib2 now checks SSL certs
|
||||||
|
import ssl
|
||||||
|
f = urllib2.urlopen(req, context=ssl.SSLContext(ssl.PROTOCOL_SSLv23))
|
||||||
|
else:
|
||||||
|
f = urllib2.urlopen(req)
|
||||||
|
f.read()
|
||||||
|
f.close()
|
||||||
|
print "Success."
|
||||||
|
except urllib2.HTTPError as e:
|
||||||
|
print "ERROR! Received %d %s" % (e.code, e.reason,)
|
||||||
|
if 400 <= e.code < 500:
|
||||||
|
if e.info().type == "application/json":
|
||||||
|
resp = json.load(e)
|
||||||
|
if "error" in resp:
|
||||||
|
print resp["error"]
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def register_new_user(user, password, server_location, shared_secret):
|
||||||
|
if not user:
|
||||||
|
try:
|
||||||
|
default_user = getpass.getuser()
|
||||||
|
except:
|
||||||
|
default_user = None
|
||||||
|
|
||||||
|
if default_user:
|
||||||
|
user = raw_input("New user localpart [%s]: " % (default_user,))
|
||||||
|
if not user:
|
||||||
|
user = default_user
|
||||||
|
else:
|
||||||
|
user = raw_input("New user localpart: ")
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
print "Invalid user name"
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
password = getpass.getpass("Password: ")
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
print "Password cannot be blank."
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
confirm_password = getpass.getpass("Confirm password: ")
|
||||||
|
|
||||||
|
if password != confirm_password:
|
||||||
|
print "Passwords do not match"
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
request_registration(user, password, server_location, shared_secret)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Used to register new users with a given home server when"
|
||||||
|
" registration has been disabled. The home server must be"
|
||||||
|
" configured with the 'registration_shared_secret' option"
|
||||||
|
" set.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-u", "--user",
|
||||||
|
default=None,
|
||||||
|
help="Local part of the new user. Will prompt if omitted.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--password",
|
||||||
|
default=None,
|
||||||
|
help="New password for user. Will prompt if omitted.",
|
||||||
|
)
|
||||||
|
|
||||||
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument(
|
||||||
|
"-c", "--config",
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
help="Path to server config file. Used to read in shared secret.",
|
||||||
|
)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"-k", "--shared-secret",
|
||||||
|
help="Shared secret as defined in server config file.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"server_url",
|
||||||
|
default="https://localhost:8448",
|
||||||
|
nargs='?',
|
||||||
|
help="URL to use to talk to the home server. Defaults to "
|
||||||
|
" 'https://localhost:8448'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if "config" in args and args.config:
|
||||||
|
config = yaml.safe_load(args.config)
|
||||||
|
secret = config.get("registration_shared_secret", None)
|
||||||
|
if not secret:
|
||||||
|
print "No 'registration_shared_secret' defined in config."
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
secret = args.shared_secret
|
||||||
|
|
||||||
|
register_new_user(args.user, args.password, args.server_url, secret)
|
||||||
763
scripts/synapse_port_db
Executable file
763
scripts/synapse_port_db
Executable file
@@ -0,0 +1,763 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import defer, reactor
|
||||||
|
from twisted.enterprise import adbapi
|
||||||
|
|
||||||
|
from synapse.storage._base import LoggingTransaction, SQLBaseStore
|
||||||
|
from synapse.storage.engines import create_engine
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import curses
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("synapse_port_db")
|
||||||
|
|
||||||
|
|
||||||
|
BOOLEAN_COLUMNS = {
|
||||||
|
"events": ["processed", "outlier"],
|
||||||
|
"rooms": ["is_public"],
|
||||||
|
"event_edges": ["is_state"],
|
||||||
|
"presence_list": ["accepted"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
APPEND_ONLY_TABLES = [
|
||||||
|
"event_content_hashes",
|
||||||
|
"event_reference_hashes",
|
||||||
|
"event_signatures",
|
||||||
|
"event_edge_hashes",
|
||||||
|
"events",
|
||||||
|
"event_json",
|
||||||
|
"state_events",
|
||||||
|
"room_memberships",
|
||||||
|
"feedback",
|
||||||
|
"topics",
|
||||||
|
"room_names",
|
||||||
|
"rooms",
|
||||||
|
"local_media_repository",
|
||||||
|
"local_media_repository_thumbnails",
|
||||||
|
"remote_media_cache",
|
||||||
|
"remote_media_cache_thumbnails",
|
||||||
|
"redactions",
|
||||||
|
"event_edges",
|
||||||
|
"event_auth",
|
||||||
|
"received_transactions",
|
||||||
|
"sent_transactions",
|
||||||
|
"transaction_id_to_pdu",
|
||||||
|
"users",
|
||||||
|
"state_groups",
|
||||||
|
"state_groups_state",
|
||||||
|
"event_to_state_groups",
|
||||||
|
"rejections",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
end_error_exec_info = None
|
||||||
|
|
||||||
|
|
||||||
|
class Store(object):
|
||||||
|
"""This object is used to pull out some of the convenience API from the
|
||||||
|
Storage layer.
|
||||||
|
|
||||||
|
*All* database interactions should go through this object.
|
||||||
|
"""
|
||||||
|
def __init__(self, db_pool, engine):
|
||||||
|
self.db_pool = db_pool
|
||||||
|
self.database_engine = engine
|
||||||
|
|
||||||
|
_simple_insert_txn = SQLBaseStore.__dict__["_simple_insert_txn"]
|
||||||
|
_simple_insert = SQLBaseStore.__dict__["_simple_insert"]
|
||||||
|
|
||||||
|
_simple_select_onecol_txn = SQLBaseStore.__dict__["_simple_select_onecol_txn"]
|
||||||
|
_simple_select_onecol = SQLBaseStore.__dict__["_simple_select_onecol"]
|
||||||
|
_simple_select_one_onecol = SQLBaseStore.__dict__["_simple_select_one_onecol"]
|
||||||
|
_simple_select_one_onecol_txn = SQLBaseStore.__dict__["_simple_select_one_onecol_txn"]
|
||||||
|
|
||||||
|
_simple_update_one = SQLBaseStore.__dict__["_simple_update_one"]
|
||||||
|
_simple_update_one_txn = SQLBaseStore.__dict__["_simple_update_one_txn"]
|
||||||
|
|
||||||
|
_execute_and_decode = SQLBaseStore.__dict__["_execute_and_decode"]
|
||||||
|
|
||||||
|
def runInteraction(self, desc, func, *args, **kwargs):
|
||||||
|
def r(conn):
|
||||||
|
try:
|
||||||
|
i = 0
|
||||||
|
N = 5
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
txn = conn.cursor()
|
||||||
|
return func(
|
||||||
|
LoggingTransaction(txn, desc, self.database_engine, []),
|
||||||
|
*args, **kwargs
|
||||||
|
)
|
||||||
|
except self.database_engine.module.DatabaseError as e:
|
||||||
|
if self.database_engine.is_deadlock(e):
|
||||||
|
logger.warn("[TXN DEADLOCK] {%s} %d/%d", desc, i, N)
|
||||||
|
if i < N:
|
||||||
|
i += 1
|
||||||
|
conn.rollback()
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug("[TXN FAIL] {%s} %s", desc, e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
return self.db_pool.runWithConnection(r)
|
||||||
|
|
||||||
|
def execute(self, f, *args, **kwargs):
|
||||||
|
return self.runInteraction(f.__name__, f, *args, **kwargs)
|
||||||
|
|
||||||
|
def execute_sql(self, sql, *args):
|
||||||
|
def r(txn):
|
||||||
|
txn.execute(sql, args)
|
||||||
|
return txn.fetchall()
|
||||||
|
return self.runInteraction("execute_sql", r)
|
||||||
|
|
||||||
|
def insert_many_txn(self, txn, table, headers, rows):
|
||||||
|
sql = "INSERT INTO %s (%s) VALUES (%s)" % (
|
||||||
|
table,
|
||||||
|
", ".join(k for k in headers),
|
||||||
|
", ".join("%s" for _ in headers)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
txn.executemany(sql, rows)
|
||||||
|
except:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to insert: %s",
|
||||||
|
table,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class Porter(object):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def setup_table(self, table):
|
||||||
|
if table in APPEND_ONLY_TABLES:
|
||||||
|
# It's safe to just carry on inserting.
|
||||||
|
next_chunk = yield self.postgres_store._simple_select_one_onecol(
|
||||||
|
table="port_from_sqlite3",
|
||||||
|
keyvalues={"table_name": table},
|
||||||
|
retcol="rowid",
|
||||||
|
allow_none=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_to_port = None
|
||||||
|
if next_chunk is None:
|
||||||
|
if table == "sent_transactions":
|
||||||
|
next_chunk, already_ported, total_to_port = (
|
||||||
|
yield self._setup_sent_transactions()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
yield self.postgres_store._simple_insert(
|
||||||
|
table="port_from_sqlite3",
|
||||||
|
values={"table_name": table, "rowid": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
next_chunk = 1
|
||||||
|
already_ported = 0
|
||||||
|
|
||||||
|
if total_to_port is None:
|
||||||
|
already_ported, total_to_port = yield self._get_total_count_to_port(
|
||||||
|
table, next_chunk
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
def delete_all(txn):
|
||||||
|
txn.execute(
|
||||||
|
"DELETE FROM port_from_sqlite3 WHERE table_name = %s",
|
||||||
|
(table,)
|
||||||
|
)
|
||||||
|
txn.execute("TRUNCATE %s CASCADE" % (table,))
|
||||||
|
|
||||||
|
yield self.postgres_store.execute(delete_all)
|
||||||
|
|
||||||
|
yield self.postgres_store._simple_insert(
|
||||||
|
table="port_from_sqlite3",
|
||||||
|
values={"table_name": table, "rowid": 0}
|
||||||
|
)
|
||||||
|
|
||||||
|
next_chunk = 1
|
||||||
|
|
||||||
|
already_ported, total_to_port = yield self._get_total_count_to_port(
|
||||||
|
table, next_chunk
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((table, already_ported, total_to_port, next_chunk))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def handle_table(self, table, postgres_size, table_size, next_chunk):
|
||||||
|
if not table_size:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.progress.add_table(table, postgres_size, table_size)
|
||||||
|
|
||||||
|
select = (
|
||||||
|
"SELECT rowid, * FROM %s WHERE rowid >= ? ORDER BY rowid LIMIT ?"
|
||||||
|
% (table,)
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
def r(txn):
|
||||||
|
txn.execute(select, (next_chunk, self.batch_size,))
|
||||||
|
rows = txn.fetchall()
|
||||||
|
headers = [column[0] for column in txn.description]
|
||||||
|
|
||||||
|
return headers, rows
|
||||||
|
|
||||||
|
headers, rows = yield self.sqlite_store.runInteraction("select", r)
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
next_chunk = rows[-1][0] + 1
|
||||||
|
|
||||||
|
self._convert_rows(table, headers, rows)
|
||||||
|
|
||||||
|
def insert(txn):
|
||||||
|
self.postgres_store.insert_many_txn(
|
||||||
|
txn, table, headers[1:], rows
|
||||||
|
)
|
||||||
|
|
||||||
|
self.postgres_store._simple_update_one_txn(
|
||||||
|
txn,
|
||||||
|
table="port_from_sqlite3",
|
||||||
|
keyvalues={"table_name": table},
|
||||||
|
updatevalues={"rowid": next_chunk},
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.postgres_store.execute(insert)
|
||||||
|
|
||||||
|
postgres_size += len(rows)
|
||||||
|
|
||||||
|
self.progress.update(table, postgres_size)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
def setup_db(self, db_config, database_engine):
|
||||||
|
db_conn = database_engine.module.connect(
|
||||||
|
**{
|
||||||
|
k: v for k, v in db_config.get("args", {}).items()
|
||||||
|
if not k.startswith("cp_")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
database_engine.prepare_database(db_conn)
|
||||||
|
|
||||||
|
db_conn.commit()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
sqlite_db_pool = adbapi.ConnectionPool(
|
||||||
|
self.sqlite_config["name"],
|
||||||
|
**self.sqlite_config["args"]
|
||||||
|
)
|
||||||
|
|
||||||
|
postgres_db_pool = adbapi.ConnectionPool(
|
||||||
|
self.postgres_config["name"],
|
||||||
|
**self.postgres_config["args"]
|
||||||
|
)
|
||||||
|
|
||||||
|
sqlite_engine = create_engine("sqlite3")
|
||||||
|
postgres_engine = create_engine("psycopg2")
|
||||||
|
|
||||||
|
self.sqlite_store = Store(sqlite_db_pool, sqlite_engine)
|
||||||
|
self.postgres_store = Store(postgres_db_pool, postgres_engine)
|
||||||
|
|
||||||
|
yield self.postgres_store.execute(
|
||||||
|
postgres_engine.check_database
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 1. Set up databases.
|
||||||
|
self.progress.set_state("Preparing SQLite3")
|
||||||
|
self.setup_db(sqlite_config, sqlite_engine)
|
||||||
|
|
||||||
|
self.progress.set_state("Preparing PostgreSQL")
|
||||||
|
self.setup_db(postgres_config, postgres_engine)
|
||||||
|
|
||||||
|
# Step 2. Get tables.
|
||||||
|
self.progress.set_state("Fetching tables")
|
||||||
|
sqlite_tables = yield self.sqlite_store._simple_select_onecol(
|
||||||
|
table="sqlite_master",
|
||||||
|
keyvalues={
|
||||||
|
"type": "table",
|
||||||
|
},
|
||||||
|
retcol="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
postgres_tables = yield self.postgres_store._simple_select_onecol(
|
||||||
|
table="information_schema.tables",
|
||||||
|
keyvalues={
|
||||||
|
"table_schema": "public",
|
||||||
|
},
|
||||||
|
retcol="distinct table_name",
|
||||||
|
)
|
||||||
|
|
||||||
|
tables = set(sqlite_tables) & set(postgres_tables)
|
||||||
|
|
||||||
|
self.progress.set_state("Creating tables")
|
||||||
|
|
||||||
|
logger.info("Found %d tables", len(tables))
|
||||||
|
|
||||||
|
def create_port_table(txn):
|
||||||
|
txn.execute(
|
||||||
|
"CREATE TABLE port_from_sqlite3 ("
|
||||||
|
" table_name varchar(100) NOT NULL UNIQUE,"
|
||||||
|
" rowid bigint NOT NULL"
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield self.postgres_store.runInteraction(
|
||||||
|
"create_port_table", create_port_table
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Failed to create port table: %s", e)
|
||||||
|
|
||||||
|
self.progress.set_state("Setting up")
|
||||||
|
|
||||||
|
# Set up tables.
|
||||||
|
setup_res = yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
self.setup_table(table)
|
||||||
|
for table in tables
|
||||||
|
if table not in ["schema_version", "applied_schema_deltas"]
|
||||||
|
and not table.startswith("sqlite_")
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process tables.
|
||||||
|
yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
self.handle_table(*res)
|
||||||
|
for res in setup_res
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.progress.done()
|
||||||
|
except:
|
||||||
|
global end_error_exec_info
|
||||||
|
end_error_exec_info = sys.exc_info()
|
||||||
|
logger.exception("")
|
||||||
|
finally:
|
||||||
|
reactor.stop()
|
||||||
|
|
||||||
|
def _convert_rows(self, table, headers, rows):
|
||||||
|
bool_col_names = BOOLEAN_COLUMNS.get(table, [])
|
||||||
|
|
||||||
|
bool_cols = [
|
||||||
|
i for i, h in enumerate(headers) if h in bool_col_names
|
||||||
|
]
|
||||||
|
|
||||||
|
def conv(j, col):
|
||||||
|
if j in bool_cols:
|
||||||
|
return bool(col)
|
||||||
|
return col
|
||||||
|
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
rows[i] = tuple(
|
||||||
|
conv(j, col)
|
||||||
|
for j, col in enumerate(row)
|
||||||
|
if j > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _setup_sent_transactions(self):
|
||||||
|
# Only save things from the last day
|
||||||
|
yesterday = int(time.time()*1000) - 86400000
|
||||||
|
|
||||||
|
# And save the max transaction id from each destination
|
||||||
|
select = (
|
||||||
|
"SELECT rowid, * FROM sent_transactions WHERE rowid IN ("
|
||||||
|
"SELECT max(rowid) FROM sent_transactions"
|
||||||
|
" GROUP BY destination"
|
||||||
|
")"
|
||||||
|
)
|
||||||
|
|
||||||
|
def r(txn):
|
||||||
|
txn.execute(select)
|
||||||
|
rows = txn.fetchall()
|
||||||
|
headers = [column[0] for column in txn.description]
|
||||||
|
|
||||||
|
ts_ind = headers.index('ts')
|
||||||
|
|
||||||
|
return headers, [r for r in rows if r[ts_ind] < yesterday]
|
||||||
|
|
||||||
|
headers, rows = yield self.sqlite_store.runInteraction(
|
||||||
|
"select", r,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._convert_rows("sent_transactions", headers, rows)
|
||||||
|
|
||||||
|
inserted_rows = len(rows)
|
||||||
|
if inserted_rows:
|
||||||
|
max_inserted_rowid = max(r[0] for r in rows)
|
||||||
|
|
||||||
|
def insert(txn):
|
||||||
|
self.postgres_store.insert_many_txn(
|
||||||
|
txn, "sent_transactions", headers[1:], rows
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.postgres_store.execute(insert)
|
||||||
|
else:
|
||||||
|
max_inserted_rowid = 0
|
||||||
|
|
||||||
|
def get_start_id(txn):
|
||||||
|
txn.execute(
|
||||||
|
"SELECT rowid FROM sent_transactions WHERE ts >= ?"
|
||||||
|
" ORDER BY rowid ASC LIMIT 1",
|
||||||
|
(yesterday,)
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = txn.fetchall()
|
||||||
|
if rows:
|
||||||
|
return rows[0][0]
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
next_chunk = yield self.sqlite_store.execute(get_start_id)
|
||||||
|
next_chunk = max(max_inserted_rowid + 1, next_chunk)
|
||||||
|
|
||||||
|
yield self.postgres_store._simple_insert(
|
||||||
|
table="port_from_sqlite3",
|
||||||
|
values={"table_name": "sent_transactions", "rowid": next_chunk}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_sent_table_size(txn):
|
||||||
|
txn.execute(
|
||||||
|
"SELECT count(*) FROM sent_transactions"
|
||||||
|
" WHERE ts >= ?",
|
||||||
|
(yesterday,)
|
||||||
|
)
|
||||||
|
size, = txn.fetchone()
|
||||||
|
return int(size)
|
||||||
|
|
||||||
|
remaining_count = yield self.sqlite_store.execute(
|
||||||
|
get_sent_table_size
|
||||||
|
)
|
||||||
|
|
||||||
|
total_count = remaining_count + inserted_rows
|
||||||
|
|
||||||
|
defer.returnValue((next_chunk, inserted_rows, total_count))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _get_remaining_count_to_port(self, table, next_chunk):
|
||||||
|
rows = yield self.sqlite_store.execute_sql(
|
||||||
|
"SELECT count(*) FROM %s WHERE rowid >= ?" % (table,),
|
||||||
|
next_chunk,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(rows[0][0])
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _get_already_ported_count(self, table):
|
||||||
|
rows = yield self.postgres_store.execute_sql(
|
||||||
|
"SELECT count(*) FROM %s" % (table,),
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(rows[0][0])
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _get_total_count_to_port(self, table, next_chunk):
|
||||||
|
remaining, done = yield defer.gatherResults(
|
||||||
|
[
|
||||||
|
self._get_remaining_count_to_port(table, next_chunk),
|
||||||
|
self._get_already_ported_count(table),
|
||||||
|
],
|
||||||
|
consumeErrors=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
remaining = int(remaining) if remaining else 0
|
||||||
|
done = int(done) if done else 0
|
||||||
|
|
||||||
|
defer.returnValue((done, remaining + done))
|
||||||
|
|
||||||
|
|
||||||
|
##############################################
|
||||||
|
###### The following is simply UI stuff ######
|
||||||
|
##############################################
|
||||||
|
|
||||||
|
|
||||||
|
class Progress(object):
|
||||||
|
"""Used to report progress of the port
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.tables = {}
|
||||||
|
|
||||||
|
self.start_time = int(time.time())
|
||||||
|
|
||||||
|
def add_table(self, table, cur, size):
|
||||||
|
self.tables[table] = {
|
||||||
|
"start": cur,
|
||||||
|
"num_done": cur,
|
||||||
|
"total": size,
|
||||||
|
"perc": int(cur * 100 / size),
|
||||||
|
}
|
||||||
|
|
||||||
|
def update(self, table, num_done):
|
||||||
|
data = self.tables[table]
|
||||||
|
data["num_done"] = num_done
|
||||||
|
data["perc"] = int(num_done * 100 / data["total"])
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CursesProgress(Progress):
|
||||||
|
"""Reports progress to a curses window
|
||||||
|
"""
|
||||||
|
def __init__(self, stdscr):
|
||||||
|
self.stdscr = stdscr
|
||||||
|
|
||||||
|
curses.use_default_colors()
|
||||||
|
curses.curs_set(0)
|
||||||
|
|
||||||
|
curses.init_pair(1, curses.COLOR_RED, -1)
|
||||||
|
curses.init_pair(2, curses.COLOR_GREEN, -1)
|
||||||
|
|
||||||
|
self.last_update = 0
|
||||||
|
|
||||||
|
self.finished = False
|
||||||
|
|
||||||
|
self.total_processed = 0
|
||||||
|
self.total_remaining = 0
|
||||||
|
|
||||||
|
super(CursesProgress, self).__init__()
|
||||||
|
|
||||||
|
def update(self, table, num_done):
|
||||||
|
super(CursesProgress, self).update(table, num_done)
|
||||||
|
|
||||||
|
self.total_processed = 0
|
||||||
|
self.total_remaining = 0
|
||||||
|
for table, data in self.tables.items():
|
||||||
|
self.total_processed += data["num_done"] - data["start"]
|
||||||
|
self.total_remaining += data["total"] - data["num_done"]
|
||||||
|
|
||||||
|
self.render()
|
||||||
|
|
||||||
|
def render(self, force=False):
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
if not force and now - self.last_update < 0.2:
|
||||||
|
# reactor.callLater(1, self.render)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stdscr.clear()
|
||||||
|
|
||||||
|
rows, cols = self.stdscr.getmaxyx()
|
||||||
|
|
||||||
|
duration = int(now) - int(self.start_time)
|
||||||
|
|
||||||
|
minutes, seconds = divmod(duration, 60)
|
||||||
|
duration_str = '%02dm %02ds' % (minutes, seconds,)
|
||||||
|
|
||||||
|
if self.finished:
|
||||||
|
status = "Time spent: %s (Done!)" % (duration_str,)
|
||||||
|
else:
|
||||||
|
|
||||||
|
if self.total_processed > 0:
|
||||||
|
left = float(self.total_remaining) / self.total_processed
|
||||||
|
|
||||||
|
est_remaining = (int(now) - self.start_time) * left
|
||||||
|
est_remaining_str = '%02dm %02ds remaining' % divmod(est_remaining, 60)
|
||||||
|
else:
|
||||||
|
est_remaining_str = "Unknown"
|
||||||
|
status = (
|
||||||
|
"Time spent: %s (est. remaining: %s)"
|
||||||
|
% (duration_str, est_remaining_str,)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdscr.addstr(
|
||||||
|
0, 0,
|
||||||
|
status,
|
||||||
|
curses.A_BOLD,
|
||||||
|
)
|
||||||
|
|
||||||
|
max_len = max([len(t) for t in self.tables.keys()])
|
||||||
|
|
||||||
|
left_margin = 5
|
||||||
|
middle_space = 1
|
||||||
|
|
||||||
|
items = self.tables.items()
|
||||||
|
items.sort(
|
||||||
|
key=lambda i: (i[1]["perc"], i[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, (table, data) in enumerate(items):
|
||||||
|
if i + 2 >= rows:
|
||||||
|
break
|
||||||
|
|
||||||
|
perc = data["perc"]
|
||||||
|
|
||||||
|
color = curses.color_pair(2) if perc == 100 else curses.color_pair(1)
|
||||||
|
|
||||||
|
self.stdscr.addstr(
|
||||||
|
i+2, left_margin + max_len - len(table),
|
||||||
|
table,
|
||||||
|
curses.A_BOLD | color,
|
||||||
|
)
|
||||||
|
|
||||||
|
size = 20
|
||||||
|
|
||||||
|
progress = "[%s%s]" % (
|
||||||
|
"#" * int(perc*size/100),
|
||||||
|
" " * (size - int(perc*size/100)),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdscr.addstr(
|
||||||
|
i+2, left_margin + max_len + middle_space,
|
||||||
|
"%s %3d%% (%d/%d)" % (progress, perc, data["num_done"], data["total"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.finished:
|
||||||
|
self.stdscr.addstr(
|
||||||
|
rows-1, 0,
|
||||||
|
"Press any key to exit...",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdscr.refresh()
|
||||||
|
self.last_update = time.time()
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
self.finished = True
|
||||||
|
self.render(True)
|
||||||
|
self.stdscr.getch()
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
self.stdscr.clear()
|
||||||
|
self.stdscr.addstr(
|
||||||
|
0, 0,
|
||||||
|
state + "...",
|
||||||
|
curses.A_BOLD,
|
||||||
|
)
|
||||||
|
self.stdscr.refresh()
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalProgress(Progress):
|
||||||
|
"""Just prints progress to the terminal
|
||||||
|
"""
|
||||||
|
def update(self, table, num_done):
|
||||||
|
super(TerminalProgress, self).update(table, num_done)
|
||||||
|
|
||||||
|
data = self.tables[table]
|
||||||
|
|
||||||
|
print "%s: %d%% (%d/%d)" % (
|
||||||
|
table, data["perc"],
|
||||||
|
data["num_done"], data["total"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
print state + "..."
|
||||||
|
|
||||||
|
|
||||||
|
##############################################
|
||||||
|
##############################################
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="A script to port an existing synapse SQLite database to"
|
||||||
|
" a new PostgreSQL database."
|
||||||
|
)
|
||||||
|
parser.add_argument("-v", action='store_true')
|
||||||
|
parser.add_argument(
|
||||||
|
"--sqlite-database", required=True,
|
||||||
|
help="The snapshot of the SQLite database file. This must not be"
|
||||||
|
" currently used by a running synapse server"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--postgres-config", type=argparse.FileType('r'), required=True,
|
||||||
|
help="The database config file for the PostgreSQL database"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--curses", action='store_true',
|
||||||
|
help="display a curses based progress UI"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--batch-size", type=int, default=1000,
|
||||||
|
help="The number of rows to select from the SQLite table each"
|
||||||
|
" iteration [default=1000]",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logging_config = {
|
||||||
|
"level": logging.DEBUG if args.v else logging.INFO,
|
||||||
|
"format": "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s"
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.curses:
|
||||||
|
logging_config["filename"] = "port-synapse.log"
|
||||||
|
|
||||||
|
logging.basicConfig(**logging_config)
|
||||||
|
|
||||||
|
sqlite_config = {
|
||||||
|
"name": "sqlite3",
|
||||||
|
"args": {
|
||||||
|
"database": args.sqlite_database,
|
||||||
|
"cp_min": 1,
|
||||||
|
"cp_max": 1,
|
||||||
|
"check_same_thread": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
postgres_config = yaml.safe_load(args.postgres_config)
|
||||||
|
|
||||||
|
if "database" in postgres_config:
|
||||||
|
postgres_config = postgres_config["database"]
|
||||||
|
|
||||||
|
if "name" not in postgres_config:
|
||||||
|
sys.stderr.write("Malformed database config: no 'name'")
|
||||||
|
sys.exit(2)
|
||||||
|
if postgres_config["name"] != "psycopg2":
|
||||||
|
sys.stderr.write("Database must use 'psycopg2' connector.")
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
def start(stdscr=None):
|
||||||
|
if stdscr:
|
||||||
|
progress = CursesProgress(stdscr)
|
||||||
|
else:
|
||||||
|
progress = TerminalProgress()
|
||||||
|
|
||||||
|
porter = Porter(
|
||||||
|
sqlite_config=sqlite_config,
|
||||||
|
postgres_config=postgres_config,
|
||||||
|
progress=progress,
|
||||||
|
batch_size=args.batch_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
reactor.callWhenRunning(porter.run)
|
||||||
|
|
||||||
|
reactor.run()
|
||||||
|
|
||||||
|
if args.curses:
|
||||||
|
curses.wrapper(start)
|
||||||
|
else:
|
||||||
|
start()
|
||||||
|
|
||||||
|
if end_error_exec_info:
|
||||||
|
exc_type, exc_value, exc_traceback = end_error_exec_info
|
||||||
|
traceback.print_exception(exc_type, exc_value, exc_traceback)
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
|
|
||||||
from synapse.storage import SCHEMA_VERSION, read_schema
|
|
||||||
from synapse.storage._base import SQLBaseStore
|
|
||||||
from synapse.storage.signatures import SignatureStore
|
|
||||||
from synapse.storage.event_federation import EventFederationStore
|
|
||||||
|
|
||||||
from syutil.base64util import encode_base64, decode_base64
|
|
||||||
|
|
||||||
from synapse.crypto.event_signing import compute_event_signature
|
|
||||||
|
|
||||||
from synapse.events.builder import EventBuilder
|
|
||||||
from synapse.events.utils import prune_event
|
|
||||||
|
|
||||||
from synapse.crypto.event_signing import check_event_content_hash
|
|
||||||
|
|
||||||
from syutil.crypto.jsonsign import (
|
|
||||||
verify_signed_json, SignatureVerifyException,
|
|
||||||
)
|
|
||||||
from syutil.crypto.signing_key import decode_verify_key_bytes
|
|
||||||
|
|
||||||
from syutil.jsonutil import encode_canonical_json
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
# import dns.resolver
|
|
||||||
import hashlib
|
|
||||||
import httplib
|
|
||||||
import json
|
|
||||||
import sqlite3
|
|
||||||
import syutil
|
|
||||||
import urllib2
|
|
||||||
|
|
||||||
|
|
||||||
delta_sql = """
|
|
||||||
CREATE TABLE IF NOT EXISTS event_json(
|
|
||||||
event_id TEXT NOT NULL,
|
|
||||||
room_id TEXT NOT NULL,
|
|
||||||
internal_metadata NOT NULL,
|
|
||||||
json BLOB NOT NULL,
|
|
||||||
CONSTRAINT ev_j_uniq UNIQUE (event_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS event_json_id ON event_json(event_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS event_json_room_id ON event_json(room_id);
|
|
||||||
|
|
||||||
PRAGMA user_version = 10;
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Store(object):
|
|
||||||
_get_event_signatures_txn = SignatureStore.__dict__["_get_event_signatures_txn"]
|
|
||||||
_get_event_content_hashes_txn = SignatureStore.__dict__["_get_event_content_hashes_txn"]
|
|
||||||
_get_event_reference_hashes_txn = SignatureStore.__dict__["_get_event_reference_hashes_txn"]
|
|
||||||
_get_prev_event_hashes_txn = SignatureStore.__dict__["_get_prev_event_hashes_txn"]
|
|
||||||
_get_prev_events_and_state = EventFederationStore.__dict__["_get_prev_events_and_state"]
|
|
||||||
_get_auth_events = EventFederationStore.__dict__["_get_auth_events"]
|
|
||||||
cursor_to_dict = SQLBaseStore.__dict__["cursor_to_dict"]
|
|
||||||
_simple_select_onecol_txn = SQLBaseStore.__dict__["_simple_select_onecol_txn"]
|
|
||||||
_simple_select_list_txn = SQLBaseStore.__dict__["_simple_select_list_txn"]
|
|
||||||
_simple_insert_txn = SQLBaseStore.__dict__["_simple_insert_txn"]
|
|
||||||
|
|
||||||
def _generate_event_json(self, txn, rows):
|
|
||||||
events = []
|
|
||||||
for row in rows:
|
|
||||||
d = dict(row)
|
|
||||||
|
|
||||||
d.pop("stream_ordering", None)
|
|
||||||
d.pop("topological_ordering", None)
|
|
||||||
d.pop("processed", None)
|
|
||||||
|
|
||||||
if "origin_server_ts" not in d:
|
|
||||||
d["origin_server_ts"] = d.pop("ts", 0)
|
|
||||||
else:
|
|
||||||
d.pop("ts", 0)
|
|
||||||
|
|
||||||
d.pop("prev_state", None)
|
|
||||||
d.update(json.loads(d.pop("unrecognized_keys")))
|
|
||||||
|
|
||||||
d["sender"] = d.pop("user_id")
|
|
||||||
|
|
||||||
d["content"] = json.loads(d["content"])
|
|
||||||
|
|
||||||
if "age_ts" not in d:
|
|
||||||
# For compatibility
|
|
||||||
d["age_ts"] = d.get("origin_server_ts", 0)
|
|
||||||
|
|
||||||
d.setdefault("unsigned", {})["age_ts"] = d.pop("age_ts")
|
|
||||||
|
|
||||||
outlier = d.pop("outlier", False)
|
|
||||||
|
|
||||||
# d.pop("membership", None)
|
|
||||||
|
|
||||||
d.pop("state_hash", None)
|
|
||||||
|
|
||||||
d.pop("replaces_state", None)
|
|
||||||
|
|
||||||
b = EventBuilder(d)
|
|
||||||
b.internal_metadata.outlier = outlier
|
|
||||||
|
|
||||||
events.append(b)
|
|
||||||
|
|
||||||
for i, ev in enumerate(events):
|
|
||||||
signatures = self._get_event_signatures_txn(
|
|
||||||
txn, ev.event_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
ev.signatures = {
|
|
||||||
n: {
|
|
||||||
k: encode_base64(v) for k, v in s.items()
|
|
||||||
}
|
|
||||||
for n, s in signatures.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
hashes = self._get_event_content_hashes_txn(
|
|
||||||
txn, ev.event_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
ev.hashes = {
|
|
||||||
k: encode_base64(v) for k, v in hashes.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
prevs = self._get_prev_events_and_state(txn, ev.event_id)
|
|
||||||
|
|
||||||
ev.prev_events = [
|
|
||||||
(e_id, h)
|
|
||||||
for e_id, h, is_state in prevs
|
|
||||||
if is_state == 0
|
|
||||||
]
|
|
||||||
|
|
||||||
# ev.auth_events = self._get_auth_events(txn, ev.event_id)
|
|
||||||
|
|
||||||
hashes = dict(ev.auth_events)
|
|
||||||
|
|
||||||
for e_id, hash in ev.prev_events:
|
|
||||||
if e_id in hashes and not hash:
|
|
||||||
hash.update(hashes[e_id])
|
|
||||||
#
|
|
||||||
# if hasattr(ev, "state_key"):
|
|
||||||
# ev.prev_state = [
|
|
||||||
# (e_id, h)
|
|
||||||
# for e_id, h, is_state in prevs
|
|
||||||
# if is_state == 1
|
|
||||||
# ]
|
|
||||||
|
|
||||||
return [e.build() for e in events]
|
|
||||||
|
|
||||||
|
|
||||||
store = Store()
|
|
||||||
|
|
||||||
|
|
||||||
# def get_key(server_name):
|
|
||||||
# print "Getting keys for: %s" % (server_name,)
|
|
||||||
# targets = []
|
|
||||||
# if ":" in server_name:
|
|
||||||
# target, port = server_name.split(":")
|
|
||||||
# targets.append((target, int(port)))
|
|
||||||
# try:
|
|
||||||
# answers = dns.resolver.query("_matrix._tcp." + server_name, "SRV")
|
|
||||||
# for srv in answers:
|
|
||||||
# targets.append((srv.target, srv.port))
|
|
||||||
# except dns.resolver.NXDOMAIN:
|
|
||||||
# targets.append((server_name, 8448))
|
|
||||||
# except:
|
|
||||||
# print "Failed to lookup keys for %s" % (server_name,)
|
|
||||||
# return {}
|
|
||||||
#
|
|
||||||
# for target, port in targets:
|
|
||||||
# url = "https://%s:%i/_matrix/key/v1" % (target, port)
|
|
||||||
# try:
|
|
||||||
# keys = json.load(urllib2.urlopen(url, timeout=2))
|
|
||||||
# verify_keys = {}
|
|
||||||
# for key_id, key_base64 in keys["verify_keys"].items():
|
|
||||||
# verify_key = decode_verify_key_bytes(
|
|
||||||
# key_id, decode_base64(key_base64)
|
|
||||||
# )
|
|
||||||
# verify_signed_json(keys, server_name, verify_key)
|
|
||||||
# verify_keys[key_id] = verify_key
|
|
||||||
# print "Got keys for: %s" % (server_name,)
|
|
||||||
# return verify_keys
|
|
||||||
# except urllib2.URLError:
|
|
||||||
# pass
|
|
||||||
# except urllib2.HTTPError:
|
|
||||||
# pass
|
|
||||||
# except httplib.HTTPException:
|
|
||||||
# pass
|
|
||||||
#
|
|
||||||
# print "Failed to get keys for %s" % (server_name,)
|
|
||||||
# return {}
|
|
||||||
|
|
||||||
|
|
||||||
def reinsert_events(cursor, server_name, signing_key):
|
|
||||||
print "Running delta: v10"
|
|
||||||
|
|
||||||
cursor.executescript(delta_sql)
|
|
||||||
|
|
||||||
cursor.execute(
|
|
||||||
"SELECT * FROM events ORDER BY rowid ASC"
|
|
||||||
)
|
|
||||||
|
|
||||||
print "Getting events..."
|
|
||||||
|
|
||||||
rows = store.cursor_to_dict(cursor)
|
|
||||||
|
|
||||||
events = store._generate_event_json(cursor, rows)
|
|
||||||
|
|
||||||
print "Got events from DB."
|
|
||||||
|
|
||||||
algorithms = {
|
|
||||||
"sha256": hashlib.sha256,
|
|
||||||
}
|
|
||||||
|
|
||||||
key_id = "%s:%s" % (signing_key.alg, signing_key.version)
|
|
||||||
verify_key = signing_key.verify_key
|
|
||||||
verify_key.alg = signing_key.alg
|
|
||||||
verify_key.version = signing_key.version
|
|
||||||
|
|
||||||
server_keys = {
|
|
||||||
server_name: {
|
|
||||||
key_id: verify_key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
N = len(events)
|
|
||||||
|
|
||||||
for event in events:
|
|
||||||
if i % 100 == 0:
|
|
||||||
print "Processed: %d/%d events" % (i,N,)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
# for alg_name in event.hashes:
|
|
||||||
# if check_event_content_hash(event, algorithms[alg_name]):
|
|
||||||
# pass
|
|
||||||
# else:
|
|
||||||
# pass
|
|
||||||
# print "FAIL content hash %s %s" % (alg_name, event.event_id, )
|
|
||||||
|
|
||||||
have_own_correctly_signed = False
|
|
||||||
for host, sigs in event.signatures.items():
|
|
||||||
pruned = prune_event(event)
|
|
||||||
|
|
||||||
for key_id in sigs:
|
|
||||||
if host not in server_keys:
|
|
||||||
server_keys[host] = {} # get_key(host)
|
|
||||||
if key_id in server_keys[host]:
|
|
||||||
try:
|
|
||||||
verify_signed_json(
|
|
||||||
pruned.get_pdu_json(),
|
|
||||||
host,
|
|
||||||
server_keys[host][key_id]
|
|
||||||
)
|
|
||||||
|
|
||||||
if host == server_name:
|
|
||||||
have_own_correctly_signed = True
|
|
||||||
except SignatureVerifyException:
|
|
||||||
print "FAIL signature check %s %s" % (
|
|
||||||
key_id, event.event_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: Re sign with our own server key
|
|
||||||
if not have_own_correctly_signed:
|
|
||||||
sigs = compute_event_signature(event, server_name, signing_key)
|
|
||||||
event.signatures.update(sigs)
|
|
||||||
|
|
||||||
pruned = prune_event(event)
|
|
||||||
|
|
||||||
for key_id in event.signatures[server_name]:
|
|
||||||
verify_signed_json(
|
|
||||||
pruned.get_pdu_json(),
|
|
||||||
server_name,
|
|
||||||
server_keys[server_name][key_id]
|
|
||||||
)
|
|
||||||
|
|
||||||
event_json = encode_canonical_json(
|
|
||||||
event.get_dict()
|
|
||||||
).decode("UTF-8")
|
|
||||||
|
|
||||||
metadata_json = encode_canonical_json(
|
|
||||||
event.internal_metadata.get_dict()
|
|
||||||
).decode("UTF-8")
|
|
||||||
|
|
||||||
store._simple_insert_txn(
|
|
||||||
cursor,
|
|
||||||
table="event_json",
|
|
||||||
values={
|
|
||||||
"event_id": event.event_id,
|
|
||||||
"room_id": event.room_id,
|
|
||||||
"internal_metadata": metadata_json,
|
|
||||||
"json": event_json,
|
|
||||||
},
|
|
||||||
or_replace=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main(database, server_name, signing_key):
|
|
||||||
conn = sqlite3.connect(database)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# Do other deltas:
|
|
||||||
cursor.execute("PRAGMA user_version")
|
|
||||||
row = cursor.fetchone()
|
|
||||||
|
|
||||||
if row and row[0]:
|
|
||||||
user_version = row[0]
|
|
||||||
# Run every version since after the current version.
|
|
||||||
for v in range(user_version + 1, 10):
|
|
||||||
print "Running delta: %d" % (v,)
|
|
||||||
sql_script = read_schema("delta/v%d" % (v,))
|
|
||||||
cursor.executescript(sql_script)
|
|
||||||
|
|
||||||
reinsert_events(cursor, server_name, signing_key)
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
print "Success!"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
|
|
||||||
parser.add_argument("database")
|
|
||||||
parser.add_argument("server_name")
|
|
||||||
parser.add_argument(
|
|
||||||
"signing_key", type=argparse.FileType('r'),
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
signing_key = syutil.crypto.signing_key.read_signing_keys(
|
|
||||||
args.signing_key
|
|
||||||
)
|
|
||||||
|
|
||||||
main(args.database, args.server_name, signing_key[0])
|
|
||||||
14
setup.cfg
14
setup.cfg
@@ -3,8 +3,16 @@ source-dir = docs/sphinx
|
|||||||
build-dir = docs/build
|
build-dir = docs/build
|
||||||
all_files = 1
|
all_files = 1
|
||||||
|
|
||||||
[aliases]
|
|
||||||
test = trial
|
|
||||||
|
|
||||||
[trial]
|
[trial]
|
||||||
test_suite = tests
|
test_suite = tests
|
||||||
|
|
||||||
|
[check-manifest]
|
||||||
|
ignore =
|
||||||
|
contrib
|
||||||
|
contrib/*
|
||||||
|
docs/*
|
||||||
|
pylint.cfg
|
||||||
|
tox.ini
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 90
|
||||||
|
|||||||
102
setup.py
102
setup.py
@@ -14,53 +14,77 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import glob
|
||||||
import os
|
import os
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages, Command
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
# Utility function to read the README file.
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
# Used for the long_description. It's nice, because now 1) we have a top level
|
|
||||||
# README file and 2) it's easier to type in the README file than to put a raw
|
|
||||||
# string in below ...
|
def read_file(path_segments):
|
||||||
def read(fname):
|
"""Read a file from the package. Takes a list of strings to join to
|
||||||
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
make the path"""
|
||||||
|
file_path = os.path.join(here, *path_segments)
|
||||||
|
with open(file_path) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def exec_file(path_segments):
|
||||||
|
"""Execute a single python file to get the variables defined in it"""
|
||||||
|
result = {}
|
||||||
|
code = read_file(path_segments)
|
||||||
|
exec(code, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Tox(Command):
|
||||||
|
user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
|
||||||
|
|
||||||
|
def initialize_options(self):
|
||||||
|
self.tox_args = None
|
||||||
|
|
||||||
|
def finalize_options(self):
|
||||||
|
self.test_args = []
|
||||||
|
self.test_suite = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
#import here, cause outside the eggs aren't loaded
|
||||||
|
try:
|
||||||
|
import tox
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
self.distribution.fetch_build_eggs("tox")
|
||||||
|
import tox
|
||||||
|
except:
|
||||||
|
raise RuntimeError(
|
||||||
|
"The tests need 'tox' to run. Please install 'tox'."
|
||||||
|
)
|
||||||
|
import shlex
|
||||||
|
args = self.tox_args
|
||||||
|
if args:
|
||||||
|
args = shlex.split(self.tox_args)
|
||||||
|
else:
|
||||||
|
args = []
|
||||||
|
errno = tox.cmdline(args=args)
|
||||||
|
sys.exit(errno)
|
||||||
|
|
||||||
|
|
||||||
|
version = exec_file(("synapse", "__init__.py"))["__version__"]
|
||||||
|
dependencies = exec_file(("synapse", "python_dependencies.py"))
|
||||||
|
long_description = read_file(("README.rst",))
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="matrix-synapse",
|
name="matrix-synapse",
|
||||||
version=read("VERSION").strip(),
|
version=version,
|
||||||
packages=find_packages(exclude=["tests", "tests.*"]),
|
packages=find_packages(exclude=["tests", "tests.*"]),
|
||||||
description="Reference Synapse Home Server",
|
description="Reference Synapse Home Server",
|
||||||
install_requires=[
|
install_requires=dependencies['requirements'](include_conditional=True).keys(),
|
||||||
"syutil==0.0.2",
|
dependency_links=dependencies["DEPENDENCY_LINKS"].values(),
|
||||||
"matrix_angular_sdk==0.6.0",
|
|
||||||
"Twisted>=14.0.0",
|
|
||||||
"service_identity>=1.0.0",
|
|
||||||
"pyopenssl>=0.14",
|
|
||||||
"pyyaml",
|
|
||||||
"pyasn1",
|
|
||||||
"pynacl",
|
|
||||||
"daemonize",
|
|
||||||
"py-bcrypt",
|
|
||||||
"frozendict>=0.4",
|
|
||||||
"pillow",
|
|
||||||
],
|
|
||||||
dependency_links=[
|
|
||||||
"https://github.com/matrix-org/syutil/tarball/v0.0.2#egg=syutil-0.0.2",
|
|
||||||
"https://github.com/pyca/pynacl/tarball/d4d3175589b892f6ea7c22f466e0e223853516fa#egg=pynacl-0.3.0",
|
|
||||||
"https://github.com/matrix-org/matrix-angular-sdk/tarball/v0.6.0/#egg=matrix_angular_sdk-0.6.0",
|
|
||||||
],
|
|
||||||
setup_requires=[
|
|
||||||
"setuptools_trial",
|
|
||||||
"setuptools>=1.0.0", # Needs setuptools that supports git+ssh.
|
|
||||||
# TODO: Do we need this now? we don't use git+ssh.
|
|
||||||
"mock"
|
|
||||||
],
|
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
long_description=read("README.rst"),
|
long_description=long_description,
|
||||||
entry_points="""
|
scripts=["synctl"] + glob.glob("scripts/*"),
|
||||||
[console_scripts]
|
cmdclass={'test': Tox},
|
||||||
synctl=synapse.app.synctl:main
|
|
||||||
synapse-homeserver=synapse.app.homeserver:main
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
|
|||||||
32
static/client/register/index.html
Normal file
32
static/client/register/index.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title> Registration </title>
|
||||||
|
<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="js/jquery-2.1.3.min.js"></script>
|
||||||
|
<script src="js/recaptcha_ajax.js"></script>
|
||||||
|
<script src="register_config.js"></script>
|
||||||
|
<script src="js/register.js"></script>
|
||||||
|
</head>
|
||||||
|
<body onload="matrixRegistration.onLoad()">
|
||||||
|
<form id="registrationForm" onsubmit="matrixRegistration.signUp(); return false;">
|
||||||
|
<div>
|
||||||
|
Create account:<br/>
|
||||||
|
|
||||||
|
<div style="text-align: center">
|
||||||
|
<input id="desired_user_id" size="32" type="text" placeholder="Matrix ID (e.g. bob)" autocapitalize="off" autocorrect="off" />
|
||||||
|
<br/>
|
||||||
|
<input id="pwd1" size="32" type="password" placeholder="Type a password"/>
|
||||||
|
<br/>
|
||||||
|
<input id="pwd2" size="32" type="password" placeholder="Confirm your password"/>
|
||||||
|
<br/>
|
||||||
|
<span id="feedback" style="color: #f00"></span>
|
||||||
|
<br/>
|
||||||
|
<div id="regcaptcha"></div>
|
||||||
|
|
||||||
|
<button type="submit" style="margin: 10px">Sign up</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
static/client/register/js/jquery-2.1.3.min.js
vendored
Normal file
4
static/client/register/js/jquery-2.1.3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
195
static/client/register/js/recaptcha_ajax.js
Normal file
195
static/client/register/js/recaptcha_ajax.js
Normal file
File diff suppressed because one or more lines are too long
117
static/client/register/js/register.js
Normal file
117
static/client/register/js/register.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
window.matrixRegistration = {
|
||||||
|
endpoint: location.origin + "/_matrix/client/api/v1/register"
|
||||||
|
};
|
||||||
|
|
||||||
|
var setupCaptcha = function() {
|
||||||
|
if (!window.matrixRegistrationConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.get(matrixRegistration.endpoint, function(response) {
|
||||||
|
var serverExpectsCaptcha = false;
|
||||||
|
for (var i=0; i<response.flows.length; i++) {
|
||||||
|
var flow = response.flows[i];
|
||||||
|
if ("m.login.recaptcha" === flow.type) {
|
||||||
|
serverExpectsCaptcha = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!serverExpectsCaptcha) {
|
||||||
|
console.log("This server does not require a captcha.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("Setting up ReCaptcha for "+matrixRegistration.endpoint);
|
||||||
|
var public_key = window.matrixRegistrationConfig.recaptcha_public_key;
|
||||||
|
if (public_key === undefined) {
|
||||||
|
console.error("No public key defined for captcha!");
|
||||||
|
setFeedbackString("Misconfigured captcha for server. Contact server admin.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Recaptcha.create(public_key,
|
||||||
|
"regcaptcha",
|
||||||
|
{
|
||||||
|
theme: "red",
|
||||||
|
callback: Recaptcha.focus_response_field
|
||||||
|
});
|
||||||
|
window.matrixRegistration.isUsingRecaptcha = true;
|
||||||
|
}).error(errorFunc);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
var submitCaptcha = function(user, pwd) {
|
||||||
|
var challengeToken = Recaptcha.get_challenge();
|
||||||
|
var captchaEntry = Recaptcha.get_response();
|
||||||
|
var data = {
|
||||||
|
type: "m.login.recaptcha",
|
||||||
|
challenge: challengeToken,
|
||||||
|
response: captchaEntry
|
||||||
|
};
|
||||||
|
console.log("Submitting captcha");
|
||||||
|
$.post(matrixRegistration.endpoint, JSON.stringify(data), function(response) {
|
||||||
|
console.log("Success -> "+JSON.stringify(response));
|
||||||
|
submitPassword(user, pwd, response.session);
|
||||||
|
}).error(function(err) {
|
||||||
|
Recaptcha.reload();
|
||||||
|
errorFunc(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var submitPassword = function(user, pwd, session) {
|
||||||
|
console.log("Registering...");
|
||||||
|
var data = {
|
||||||
|
type: "m.login.password",
|
||||||
|
user: user,
|
||||||
|
password: pwd,
|
||||||
|
session: session
|
||||||
|
};
|
||||||
|
$.post(matrixRegistration.endpoint, JSON.stringify(data), function(response) {
|
||||||
|
matrixRegistration.onRegistered(
|
||||||
|
response.home_server, response.user_id, response.access_token
|
||||||
|
);
|
||||||
|
}).error(errorFunc);
|
||||||
|
};
|
||||||
|
|
||||||
|
var errorFunc = function(err) {
|
||||||
|
if (err.responseJSON && err.responseJSON.error) {
|
||||||
|
setFeedbackString(err.responseJSON.error + " (" + err.responseJSON.errcode + ")");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setFeedbackString("Request failed: " + err.status);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var setFeedbackString = function(text) {
|
||||||
|
$("#feedback").text(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
matrixRegistration.onLoad = function() {
|
||||||
|
setupCaptcha();
|
||||||
|
};
|
||||||
|
|
||||||
|
matrixRegistration.signUp = function() {
|
||||||
|
var user = $("#desired_user_id").val();
|
||||||
|
if (user.length == 0) {
|
||||||
|
setFeedbackString("Must specify a username.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pwd1 = $("#pwd1").val();
|
||||||
|
var pwd2 = $("#pwd2").val();
|
||||||
|
if (pwd1.length < 6) {
|
||||||
|
setFeedbackString("Password: min. 6 characters.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pwd1 != pwd2) {
|
||||||
|
setFeedbackString("Passwords do not match.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (window.matrixRegistration.isUsingRecaptcha) {
|
||||||
|
submitCaptcha(user, pwd1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
submitPassword(user, pwd1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
matrixRegistration.onRegistered = function(hs_url, user_id, access_token) {
|
||||||
|
// clobber this function
|
||||||
|
console.log("onRegistered - This function should be replaced to proceed.");
|
||||||
|
};
|
||||||
3
static/client/register/register_config.sample.js
Normal file
3
static/client/register/register_config.sample.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
window.matrixRegistrationConfig = {
|
||||||
|
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
||||||
|
};
|
||||||
60
static/client/register/style.css
Normal file
60
static/client/register/style.css
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 12pt;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 20pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link { color: #666; }
|
||||||
|
a:visited { color: #666; }
|
||||||
|
a:hover { color: #000; }
|
||||||
|
a:active { color: #000; }
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea, input {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallPrint {
|
||||||
|
color: #888;
|
||||||
|
font-size: 9pt ! important;
|
||||||
|
font-style: italic ! important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#recaptcha_area {
|
||||||
|
margin: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
.g-recaptcha div {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#registrationForm {
|
||||||
|
text-align: left;
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
-moz-border-radius: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||||
|
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||||
|
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||||
|
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border: 1px #ccc solid;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
""" This is a reference implementation of a synapse home server.
|
""" This is a reference implementation of a Matrix home server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.6.0"
|
__version__ = "0.10.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -18,25 +18,37 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, Membership, JoinRules
|
from synapse.api.constants import EventTypes, Membership, JoinRules
|
||||||
from synapse.api.errors import AuthError, StoreError, Codes, SynapseError
|
from synapse.api.errors import AuthError, Codes, SynapseError
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.util.async import run_on_reactor
|
from synapse.types import EventID, RoomID, UserID
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
AuthEventTypes = (
|
||||||
|
EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels,
|
||||||
|
EventTypes.JoinRules, EventTypes.RoomHistoryVisibility,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Auth(object):
|
class Auth(object):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.hs = hs
|
self.hs = hs
|
||||||
self.store = hs.get_datastore()
|
self.store = hs.get_datastore()
|
||||||
self.state = hs.get_state_handler()
|
self.state = hs.get_state_handler()
|
||||||
|
self.TOKEN_NOT_FOUND_HTTP_STATUS = 401
|
||||||
|
|
||||||
def check(self, event, auth_events):
|
def check(self, event, auth_events):
|
||||||
""" Checks if this event is correctly authed.
|
""" Checks if this event is correctly authed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: the event being checked.
|
||||||
|
auth_events (dict: event-key -> event): the existing room state.
|
||||||
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if the auth checks pass.
|
True if the auth checks pass.
|
||||||
"""
|
"""
|
||||||
@@ -53,11 +65,35 @@ class Auth(object):
|
|||||||
# FIXME
|
# FIXME
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
creation_event = auth_events.get((EventTypes.Create, ""), None)
|
||||||
|
if not creation_event:
|
||||||
|
raise SynapseError(
|
||||||
|
403,
|
||||||
|
"Room %r does not exist" % (event.room_id,)
|
||||||
|
)
|
||||||
|
|
||||||
|
creating_domain = RoomID.from_string(event.room_id).domain
|
||||||
|
originating_domain = EventID.from_string(event.event_id).domain
|
||||||
|
if creating_domain != originating_domain:
|
||||||
|
if not self.can_federate(event, auth_events):
|
||||||
|
raise SynapseError(
|
||||||
|
403,
|
||||||
|
"This room has been marked as unfederatable."
|
||||||
|
)
|
||||||
|
|
||||||
# FIXME: Temp hack
|
# FIXME: Temp hack
|
||||||
if event.type == EventTypes.Aliases:
|
if event.type == EventTypes.Aliases:
|
||||||
return True
|
alias_domain = UserID.from_string(event.state_key).domain
|
||||||
|
if alias_domain != originating_domain:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"Can only set aliases for own domain"
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("Auth events: %s", auth_events)
|
logger.debug(
|
||||||
|
"Auth events: %s",
|
||||||
|
[a.event_id for a in auth_events.values()]
|
||||||
|
)
|
||||||
|
|
||||||
if event.type == EventTypes.Member:
|
if event.type == EventTypes.Member:
|
||||||
allowed = self.is_membership_change_allowed(
|
allowed = self.is_membership_change_allowed(
|
||||||
@@ -76,7 +112,7 @@ class Auth(object):
|
|||||||
self._check_power_levels(event, auth_events)
|
self._check_power_levels(event, auth_events)
|
||||||
|
|
||||||
if event.type == EventTypes.Redaction:
|
if event.type == EventTypes.Redaction:
|
||||||
self._check_redaction(event, auth_events)
|
self.check_redaction(event, auth_events)
|
||||||
|
|
||||||
logger.debug("Allowing! %s", event)
|
logger.debug("Allowing! %s", event)
|
||||||
except AuthError as e:
|
except AuthError as e:
|
||||||
@@ -88,12 +124,19 @@ class Auth(object):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def check_joined_room(self, room_id, user_id):
|
def check_joined_room(self, room_id, user_id, current_state=None):
|
||||||
member = yield self.state.get_current_state(
|
if current_state:
|
||||||
room_id=room_id,
|
member = current_state.get(
|
||||||
event_type=EventTypes.Member,
|
(EventTypes.Member, user_id),
|
||||||
state_key=user_id
|
None
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
member = yield self.state.get_current_state(
|
||||||
|
room_id=room_id,
|
||||||
|
event_type=EventTypes.Member,
|
||||||
|
state_key=user_id
|
||||||
|
)
|
||||||
|
|
||||||
self._check_joined_room(member, user_id, room_id)
|
self._check_joined_room(member, user_id, room_id)
|
||||||
defer.returnValue(member)
|
defer.returnValue(member)
|
||||||
|
|
||||||
@@ -101,10 +144,10 @@ class Auth(object):
|
|||||||
def check_host_in_room(self, room_id, host):
|
def check_host_in_room(self, room_id, host):
|
||||||
curr_state = yield self.state.get_current_state(room_id)
|
curr_state = yield self.state.get_current_state(room_id)
|
||||||
|
|
||||||
for event in curr_state:
|
for event in curr_state.values():
|
||||||
if event.type == EventTypes.Member:
|
if event.type == EventTypes.Member:
|
||||||
try:
|
try:
|
||||||
if self.hs.parse_userid(event.state_key).domain != host:
|
if UserID.from_string(event.state_key).domain != host:
|
||||||
continue
|
continue
|
||||||
except:
|
except:
|
||||||
logger.warn("state_key not user_id: %s", event.state_key)
|
logger.warn("state_key not user_id: %s", event.state_key)
|
||||||
@@ -131,6 +174,11 @@ class Auth(object):
|
|||||||
user_id, room_id, repr(member)
|
user_id, room_id, repr(member)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def can_federate(self, event, auth_events):
|
||||||
|
creation_event = auth_events.get((EventTypes.Create, ""))
|
||||||
|
|
||||||
|
return creation_event.content.get("m.federate", True) is True
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def is_membership_change_allowed(self, event, auth_events):
|
def is_membership_change_allowed(self, event, auth_events):
|
||||||
membership = event.content["membership"]
|
membership = event.content["membership"]
|
||||||
@@ -158,6 +206,7 @@ class Auth(object):
|
|||||||
target = auth_events.get(key)
|
target = auth_events.get(key)
|
||||||
|
|
||||||
target_in_room = target and target.membership == Membership.JOIN
|
target_in_room = target and target.membership == Membership.JOIN
|
||||||
|
target_banned = target and target.membership == Membership.BAN
|
||||||
|
|
||||||
key = (EventTypes.JoinRules, "", )
|
key = (EventTypes.JoinRules, "", )
|
||||||
join_rule_event = auth_events.get(key)
|
join_rule_event = auth_events.get(key)
|
||||||
@@ -168,24 +217,20 @@ class Auth(object):
|
|||||||
else:
|
else:
|
||||||
join_rule = JoinRules.INVITE
|
join_rule = JoinRules.INVITE
|
||||||
|
|
||||||
user_level = self._get_power_level_from_event_state(
|
user_level = self._get_user_power_level(event.user_id, auth_events)
|
||||||
event,
|
target_level = self._get_user_power_level(
|
||||||
event.user_id,
|
target_user_id, auth_events
|
||||||
auth_events,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ban_level, kick_level, redact_level = (
|
# FIXME (erikj): What should we do here as the default?
|
||||||
self._get_ops_level_from_event_state(
|
ban_level = self._get_named_level(auth_events, "ban", 50)
|
||||||
event,
|
|
||||||
auth_events,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"is_membership_change_allowed: %s",
|
"is_membership_change_allowed: %s",
|
||||||
{
|
{
|
||||||
"caller_in_room": caller_in_room,
|
"caller_in_room": caller_in_room,
|
||||||
"caller_invited": caller_invited,
|
"caller_invited": caller_invited,
|
||||||
|
"target_banned": target_banned,
|
||||||
"target_in_room": target_in_room,
|
"target_in_room": target_in_room,
|
||||||
"membership": membership,
|
"membership": membership,
|
||||||
"join_rule": join_rule,
|
"join_rule": join_rule,
|
||||||
@@ -194,25 +239,41 @@ class Auth(object):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if Membership.INVITE == membership:
|
if Membership.JOIN != membership:
|
||||||
# TODO (erikj): We should probably handle this more intelligently
|
# JOIN is the only action you can perform if you're not in the room
|
||||||
# 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
|
if not caller_in_room: # caller isn't joined
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403,
|
403,
|
||||||
"%s not in room %s." % (event.user_id, event.room_id,)
|
"%s not in room %s." % (event.user_id, event.room_id,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 target_banned:
|
||||||
|
raise AuthError(
|
||||||
|
403, "%s is banned from the room" % (target_user_id,)
|
||||||
|
)
|
||||||
elif target_in_room: # the target is already in the room.
|
elif target_in_room: # the target is already in the room.
|
||||||
raise AuthError(403, "%s is already in the room." %
|
raise AuthError(403, "%s is already in the room." %
|
||||||
target_user_id)
|
target_user_id)
|
||||||
|
else:
|
||||||
|
invite_level = self._get_named_level(auth_events, "invite", 0)
|
||||||
|
|
||||||
|
if user_level < invite_level:
|
||||||
|
raise AuthError(
|
||||||
|
403, "You cannot invite user %s." % target_user_id
|
||||||
|
)
|
||||||
elif Membership.JOIN == membership:
|
elif Membership.JOIN == membership:
|
||||||
# Joins are valid iff caller == target and they were:
|
# Joins are valid iff caller == target and they were:
|
||||||
# invited: They are accepting the invitation
|
# invited: They are accepting the invitation
|
||||||
# joined: It's a NOOP
|
# joined: It's a NOOP
|
||||||
if event.user_id != target_user_id:
|
if event.user_id != target_user_id:
|
||||||
raise AuthError(403, "Cannot force another user to join.")
|
raise AuthError(403, "Cannot force another user to join.")
|
||||||
|
elif target_banned:
|
||||||
|
raise AuthError(403, "You are banned from this room")
|
||||||
elif join_rule == JoinRules.PUBLIC:
|
elif join_rule == JoinRules.PUBLIC:
|
||||||
pass
|
pass
|
||||||
elif join_rule == JoinRules.INVITE:
|
elif join_rule == JoinRules.INVITE:
|
||||||
@@ -224,63 +285,61 @@ class Auth(object):
|
|||||||
raise AuthError(403, "You are not allowed to join this room")
|
raise AuthError(403, "You are not allowed to join this room")
|
||||||
elif Membership.LEAVE == membership:
|
elif Membership.LEAVE == membership:
|
||||||
# TODO (erikj): Implement kicks.
|
# TODO (erikj): Implement kicks.
|
||||||
|
if target_banned and user_level < ban_level:
|
||||||
if not caller_in_room: # trying to leave a room you aren't joined
|
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403,
|
403, "You cannot unban user &s." % (target_user_id,)
|
||||||
"%s not in room %s." % (target_user_id, event.room_id,)
|
|
||||||
)
|
)
|
||||||
elif target_user_id != event.user_id:
|
elif target_user_id != event.user_id:
|
||||||
if kick_level:
|
kick_level = self._get_named_level(auth_events, "kick", 50)
|
||||||
kick_level = int(kick_level)
|
|
||||||
else:
|
|
||||||
kick_level = 50 # FIXME (erikj): What should we do here?
|
|
||||||
|
|
||||||
if user_level < kick_level:
|
if user_level < kick_level or user_level <= target_level:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403, "You cannot kick user %s." % target_user_id
|
403, "You cannot kick user %s." % target_user_id
|
||||||
)
|
)
|
||||||
elif Membership.BAN == membership:
|
elif Membership.BAN == membership:
|
||||||
if ban_level:
|
if user_level < ban_level or user_level <= target_level:
|
||||||
ban_level = int(ban_level)
|
|
||||||
else:
|
|
||||||
ban_level = 50 # FIXME (erikj): What should we do here?
|
|
||||||
|
|
||||||
if user_level < ban_level:
|
|
||||||
raise AuthError(403, "You don't have permission to ban")
|
raise AuthError(403, "You don't have permission to ban")
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown membership %s" % membership)
|
raise AuthError(500, "Unknown membership %s" % membership)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _get_power_level_from_event_state(self, event, user_id, auth_events):
|
def _get_power_level_event(self, auth_events):
|
||||||
key = (EventTypes.PowerLevels, "", )
|
key = (EventTypes.PowerLevels, "", )
|
||||||
power_level_event = auth_events.get(key)
|
return auth_events.get(key)
|
||||||
level = None
|
|
||||||
|
def _get_user_power_level(self, user_id, auth_events):
|
||||||
|
power_level_event = self._get_power_level_event(auth_events)
|
||||||
|
|
||||||
if power_level_event:
|
if power_level_event:
|
||||||
level = power_level_event.content.get("users", {}).get(user_id)
|
level = power_level_event.content.get("users", {}).get(user_id)
|
||||||
if not level:
|
if not level:
|
||||||
level = power_level_event.content.get("users_default", 0)
|
level = power_level_event.content.get("users_default", 0)
|
||||||
|
|
||||||
|
if level is None:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return int(level)
|
||||||
else:
|
else:
|
||||||
key = (EventTypes.Create, "", )
|
key = (EventTypes.Create, "", )
|
||||||
create_event = auth_events.get(key)
|
create_event = auth_events.get(key)
|
||||||
if (create_event is not None and
|
if (create_event is not None and
|
||||||
create_event.content["creator"] == user_id):
|
create_event.content["creator"] == user_id):
|
||||||
return 100
|
return 100
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
return level
|
def _get_named_level(self, auth_events, name, default):
|
||||||
|
power_level_event = self._get_power_level_event(auth_events)
|
||||||
|
|
||||||
def _get_ops_level_from_event_state(self, event, auth_events):
|
if not power_level_event:
|
||||||
key = (EventTypes.PowerLevels, "", )
|
return default
|
||||||
power_level_event = auth_events.get(key)
|
|
||||||
|
|
||||||
if power_level_event:
|
level = power_level_event.content.get(name, None)
|
||||||
return (
|
if level is not None:
|
||||||
power_level_event.content.get("ban", 50),
|
return int(level)
|
||||||
power_level_event.content.get("kick", 50),
|
else:
|
||||||
power_level_event.content.get("redact", 50),
|
return default
|
||||||
)
|
|
||||||
return None, None, None,
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_user_by_req(self, request):
|
def get_user_by_req(self, request):
|
||||||
@@ -289,15 +348,46 @@ class Auth(object):
|
|||||||
Args:
|
Args:
|
||||||
request - An HTTP request with an access_token query parameter.
|
request - An HTTP request with an access_token query parameter.
|
||||||
Returns:
|
Returns:
|
||||||
UserID : User ID object of the user making the request
|
tuple of:
|
||||||
|
UserID (str)
|
||||||
|
Access token ID (str)
|
||||||
Raises:
|
Raises:
|
||||||
AuthError if no user by that token exists or the token is invalid.
|
AuthError if no user by that token exists or the token is invalid.
|
||||||
"""
|
"""
|
||||||
# Can optionally look elsewhere in the request (e.g. headers)
|
# Can optionally look elsewhere in the request (e.g. headers)
|
||||||
try:
|
try:
|
||||||
access_token = request.args["access_token"][0]
|
access_token = request.args["access_token"][0]
|
||||||
user_info = yield self.get_user_by_token(access_token)
|
|
||||||
|
# Check for application service tokens with a user_id override
|
||||||
|
try:
|
||||||
|
app_service = yield self.store.get_app_service_by_token(
|
||||||
|
access_token
|
||||||
|
)
|
||||||
|
if not app_service:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
user_id = app_service.sender
|
||||||
|
if "user_id" in request.args:
|
||||||
|
user_id = request.args["user_id"][0]
|
||||||
|
if not app_service.is_interested_in_user(user_id):
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"Application service cannot masquerade as this user."
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
request.authenticated_entity = user_id
|
||||||
|
|
||||||
|
defer.returnValue((UserID.from_string(user_id), ""))
|
||||||
|
return
|
||||||
|
except KeyError:
|
||||||
|
pass # normal users won't have the user_id query parameter set.
|
||||||
|
|
||||||
|
user_info = yield self.get_user_by_access_token(access_token)
|
||||||
user = user_info["user"]
|
user = user_info["user"]
|
||||||
|
token_id = user_info["token_id"]
|
||||||
|
|
||||||
ip_addr = self.hs.get_ip_from_request(request)
|
ip_addr = self.hs.get_ip_from_request(request)
|
||||||
user_agent = request.requestHeaders.getRawHeaders(
|
user_agent = request.requestHeaders.getRawHeaders(
|
||||||
@@ -305,73 +395,97 @@ class Auth(object):
|
|||||||
default=[""]
|
default=[""]
|
||||||
)[0]
|
)[0]
|
||||||
if user and access_token and ip_addr:
|
if user and access_token and ip_addr:
|
||||||
yield self.store.insert_client_ip(
|
self.store.insert_client_ip(
|
||||||
user=user,
|
user=user,
|
||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
device_id=user_info["device_id"],
|
|
||||||
ip=ip_addr,
|
ip=ip_addr,
|
||||||
user_agent=user_agent
|
user_agent=user_agent
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(user)
|
request.authenticated_entity = user.to_string()
|
||||||
|
|
||||||
|
defer.returnValue((user, token_id,))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise AuthError(403, "Missing access token.")
|
raise AuthError(
|
||||||
|
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token.",
|
||||||
|
errcode=Codes.MISSING_TOKEN
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_user_by_token(self, token):
|
def get_user_by_access_token(self, token):
|
||||||
""" Get a registered user's ID.
|
""" Get a registered user's ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
token (str): The access token to get the user by.
|
token (str): The access token to get the user by.
|
||||||
Returns:
|
Returns:
|
||||||
dict : dict that includes the user, device_id, and whether the
|
dict : dict that includes the user and the ID of their access token.
|
||||||
user is a server admin.
|
|
||||||
Raises:
|
Raises:
|
||||||
AuthError if no user by that token exists or the token is invalid.
|
AuthError if no user by that token exists or the token is invalid.
|
||||||
"""
|
"""
|
||||||
|
ret = yield self.store.get_user_by_access_token(token)
|
||||||
|
if not ret:
|
||||||
|
raise AuthError(
|
||||||
|
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Unrecognised access token.",
|
||||||
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
|
)
|
||||||
|
user_info = {
|
||||||
|
"user": UserID.from_string(ret.get("name")),
|
||||||
|
"token_id": ret.get("token_id", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer.returnValue(user_info)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_appservice_by_req(self, request):
|
||||||
try:
|
try:
|
||||||
ret = yield self.store.get_user_by_token(token=token)
|
token = request.args["access_token"][0]
|
||||||
if not ret:
|
service = yield self.store.get_app_service_by_token(token)
|
||||||
raise StoreError()
|
if not service:
|
||||||
|
raise AuthError(
|
||||||
user_info = {
|
self.TOKEN_NOT_FOUND_HTTP_STATUS,
|
||||||
"admin": bool(ret.get("admin", False)),
|
"Unrecognised access token.",
|
||||||
"device_id": ret.get("device_id"),
|
errcode=Codes.UNKNOWN_TOKEN
|
||||||
"user": self.hs.parse_userid(ret.get("name")),
|
)
|
||||||
}
|
request.authenticated_entity = service.sender
|
||||||
|
defer.returnValue(service)
|
||||||
defer.returnValue(user_info)
|
except KeyError:
|
||||||
except StoreError:
|
raise AuthError(
|
||||||
raise AuthError(403, "Unrecognised access token.",
|
self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token."
|
||||||
errcode=Codes.UNKNOWN_TOKEN)
|
)
|
||||||
|
|
||||||
def is_server_admin(self, user):
|
def is_server_admin(self, user):
|
||||||
return self.store.is_server_admin(user)
|
return self.store.is_server_admin(user)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def add_auth_events(self, builder, context):
|
def add_auth_events(self, builder, context):
|
||||||
yield run_on_reactor()
|
auth_ids = self.compute_auth_events(builder, context.current_state)
|
||||||
|
|
||||||
if builder.type == EventTypes.Create:
|
auth_events_entries = yield self.store.add_event_hashes(
|
||||||
builder.auth_events = []
|
auth_ids
|
||||||
return
|
)
|
||||||
|
|
||||||
|
builder.auth_events = auth_events_entries
|
||||||
|
|
||||||
|
def compute_auth_events(self, event, current_state):
|
||||||
|
if event.type == EventTypes.Create:
|
||||||
|
return []
|
||||||
|
|
||||||
auth_ids = []
|
auth_ids = []
|
||||||
|
|
||||||
key = (EventTypes.PowerLevels, "", )
|
key = (EventTypes.PowerLevels, "", )
|
||||||
power_level_event = context.current_state.get(key)
|
power_level_event = current_state.get(key)
|
||||||
|
|
||||||
if power_level_event:
|
if power_level_event:
|
||||||
auth_ids.append(power_level_event.event_id)
|
auth_ids.append(power_level_event.event_id)
|
||||||
|
|
||||||
key = (EventTypes.JoinRules, "", )
|
key = (EventTypes.JoinRules, "", )
|
||||||
join_rule_event = context.current_state.get(key)
|
join_rule_event = current_state.get(key)
|
||||||
|
|
||||||
key = (EventTypes.Member, builder.user_id, )
|
key = (EventTypes.Member, event.user_id, )
|
||||||
member_event = context.current_state.get(key)
|
member_event = current_state.get(key)
|
||||||
|
|
||||||
key = (EventTypes.Create, "", )
|
key = (EventTypes.Create, "", )
|
||||||
create_event = context.current_state.get(key)
|
create_event = current_state.get(key)
|
||||||
if create_event:
|
if create_event:
|
||||||
auth_ids.append(create_event.event_id)
|
auth_ids.append(create_event.event_id)
|
||||||
|
|
||||||
@@ -381,8 +495,8 @@ class Auth(object):
|
|||||||
else:
|
else:
|
||||||
is_public = False
|
is_public = False
|
||||||
|
|
||||||
if builder.type == EventTypes.Member:
|
if event.type == EventTypes.Member:
|
||||||
e_type = builder.content["membership"]
|
e_type = event.content["membership"]
|
||||||
if e_type in [Membership.JOIN, Membership.INVITE]:
|
if e_type in [Membership.JOIN, Membership.INVITE]:
|
||||||
if join_rule_event:
|
if join_rule_event:
|
||||||
auth_ids.append(join_rule_event.event_id)
|
auth_ids.append(join_rule_event.event_id)
|
||||||
@@ -397,17 +511,7 @@ class Auth(object):
|
|||||||
if member_event.content["membership"] == Membership.JOIN:
|
if member_event.content["membership"] == Membership.JOIN:
|
||||||
auth_ids.append(member_event.event_id)
|
auth_ids.append(member_event.event_id)
|
||||||
|
|
||||||
auth_events_entries = yield self.store.add_event_hashes(
|
return auth_ids
|
||||||
auth_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
builder.auth_events = auth_events_entries
|
|
||||||
|
|
||||||
context.auth_events = {
|
|
||||||
k: v
|
|
||||||
for k, v in context.current_state.items()
|
|
||||||
if v.event_id in auth_ids
|
|
||||||
}
|
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def _can_send_event(self, event, auth_events):
|
def _can_send_event(self, event, auth_events):
|
||||||
@@ -418,7 +522,7 @@ class Auth(object):
|
|||||||
send_level = send_level_event.content.get("events", {}).get(
|
send_level = send_level_event.content.get("events", {}).get(
|
||||||
event.type
|
event.type
|
||||||
)
|
)
|
||||||
if not send_level:
|
if send_level is None:
|
||||||
if hasattr(event, "state_key"):
|
if hasattr(event, "state_key"):
|
||||||
send_level = send_level_event.content.get(
|
send_level = send_level_event.content.get(
|
||||||
"state_default", 50
|
"state_default", 50
|
||||||
@@ -433,16 +537,7 @@ class Auth(object):
|
|||||||
else:
|
else:
|
||||||
send_level = 0
|
send_level = 0
|
||||||
|
|
||||||
user_level = self._get_power_level_from_event_state(
|
user_level = self._get_user_power_level(event.user_id, auth_events)
|
||||||
event,
|
|
||||||
event.user_id,
|
|
||||||
auth_events,
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_level:
|
|
||||||
user_level = int(user_level)
|
|
||||||
else:
|
|
||||||
user_level = 0
|
|
||||||
|
|
||||||
if user_level < send_level:
|
if user_level < send_level:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
@@ -453,50 +548,61 @@ class Auth(object):
|
|||||||
|
|
||||||
# Check state_key
|
# Check state_key
|
||||||
if hasattr(event, "state_key"):
|
if hasattr(event, "state_key"):
|
||||||
if not event.state_key.startswith("_"):
|
if event.state_key.startswith("@"):
|
||||||
if event.state_key.startswith("@"):
|
if event.state_key != event.user_id:
|
||||||
if event.state_key != event.user_id:
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You are not allowed to set others state"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
sender_domain = UserID.from_string(
|
||||||
|
event.user_id
|
||||||
|
).domain
|
||||||
|
|
||||||
|
if sender_domain != event.state_key:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403,
|
403,
|
||||||
"You are not allowed to set others state"
|
"You are not allowed to set others state"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
sender_domain = self.hs.parse_userid(
|
|
||||||
event.user_id
|
|
||||||
).domain
|
|
||||||
|
|
||||||
if sender_domain != event.state_key:
|
|
||||||
raise AuthError(
|
|
||||||
403,
|
|
||||||
"You are not allowed to set others state"
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _check_redaction(self, event, auth_events):
|
def check_redaction(self, event, auth_events):
|
||||||
user_level = self._get_power_level_from_event_state(
|
"""Check whether the event sender is allowed to redact the target event.
|
||||||
event,
|
|
||||||
event.user_id,
|
|
||||||
auth_events,
|
|
||||||
)
|
|
||||||
|
|
||||||
_, _, redact_level = self._get_ops_level_from_event_state(
|
Returns:
|
||||||
event,
|
True if the the sender is allowed to redact the target event if the
|
||||||
auth_events,
|
target event was created by them.
|
||||||
)
|
False if the sender is allowed to redact the target event with no
|
||||||
|
further checks.
|
||||||
|
|
||||||
if user_level < redact_level:
|
Raises:
|
||||||
raise AuthError(
|
AuthError if the event sender is definitely not allowed to redact
|
||||||
403,
|
the target event.
|
||||||
"You don't have permission to redact events"
|
"""
|
||||||
)
|
user_level = self._get_user_power_level(event.user_id, auth_events)
|
||||||
|
|
||||||
|
redact_level = self._get_named_level(auth_events, "redact", 50)
|
||||||
|
|
||||||
|
if user_level > redact_level:
|
||||||
|
return False
|
||||||
|
|
||||||
|
redacter_domain = EventID.from_string(event.event_id).domain
|
||||||
|
redactee_domain = EventID.from_string(event.redacts).domain
|
||||||
|
if redacter_domain == redactee_domain:
|
||||||
|
return True
|
||||||
|
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to redact events"
|
||||||
|
)
|
||||||
|
|
||||||
def _check_power_levels(self, event, auth_events):
|
def _check_power_levels(self, event, auth_events):
|
||||||
user_list = event.content.get("users", {})
|
user_list = event.content.get("users", {})
|
||||||
# Validate users
|
# Validate users
|
||||||
for k, v in user_list.items():
|
for k, v in user_list.items():
|
||||||
try:
|
try:
|
||||||
self.hs.parse_userid(k)
|
UserID.from_string(k)
|
||||||
except:
|
except:
|
||||||
raise SynapseError(400, "Not a valid user_id: %s" % (k,))
|
raise SynapseError(400, "Not a valid user_id: %s" % (k,))
|
||||||
|
|
||||||
@@ -511,32 +617,30 @@ class Auth(object):
|
|||||||
if not current_state:
|
if not current_state:
|
||||||
return
|
return
|
||||||
|
|
||||||
user_level = self._get_power_level_from_event_state(
|
user_level = self._get_user_power_level(event.user_id, auth_events)
|
||||||
event,
|
|
||||||
event.user_id,
|
|
||||||
auth_events,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check other levels:
|
# Check other levels:
|
||||||
levels_to_check = [
|
levels_to_check = [
|
||||||
("users_default", []),
|
("users_default", None),
|
||||||
("events_default", []),
|
("events_default", None),
|
||||||
("ban", []),
|
("state_default", None),
|
||||||
("redact", []),
|
("ban", None),
|
||||||
("kick", []),
|
("redact", None),
|
||||||
|
("kick", None),
|
||||||
|
("invite", None),
|
||||||
]
|
]
|
||||||
|
|
||||||
old_list = current_state.content.get("users")
|
old_list = current_state.content.get("users")
|
||||||
for user in set(old_list.keys() + user_list.keys()):
|
for user in set(old_list.keys() + user_list.keys()):
|
||||||
levels_to_check.append(
|
levels_to_check.append(
|
||||||
(user, ["users"])
|
(user, "users")
|
||||||
)
|
)
|
||||||
|
|
||||||
old_list = current_state.content.get("events")
|
old_list = current_state.content.get("events")
|
||||||
new_list = event.content.get("events")
|
new_list = event.content.get("events")
|
||||||
for ev_id in set(old_list.keys() + new_list.keys()):
|
for ev_id in set(old_list.keys() + new_list.keys()):
|
||||||
levels_to_check.append(
|
levels_to_check.append(
|
||||||
(ev_id, ["events"])
|
(ev_id, "events")
|
||||||
)
|
)
|
||||||
|
|
||||||
old_state = current_state.content
|
old_state = current_state.content
|
||||||
@@ -544,12 +648,10 @@ class Auth(object):
|
|||||||
|
|
||||||
for level_to_check, dir in levels_to_check:
|
for level_to_check, dir in levels_to_check:
|
||||||
old_loc = old_state
|
old_loc = old_state
|
||||||
for d in dir:
|
|
||||||
old_loc = old_loc.get(d, {})
|
|
||||||
|
|
||||||
new_loc = new_state
|
new_loc = new_state
|
||||||
for d in dir:
|
if dir:
|
||||||
new_loc = new_loc.get(d, {})
|
old_loc = old_loc.get(dir, {})
|
||||||
|
new_loc = new_loc.get(dir, {})
|
||||||
|
|
||||||
if level_to_check in old_loc:
|
if level_to_check in old_loc:
|
||||||
old_level = int(old_loc[level_to_check])
|
old_level = int(old_loc[level_to_check])
|
||||||
@@ -565,6 +667,14 @@ class Auth(object):
|
|||||||
if new_level == old_level:
|
if new_level == old_level:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if dir == "users" and level_to_check != event.user_id:
|
||||||
|
if old_level == user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to remove ops level equal "
|
||||||
|
"to your own"
|
||||||
|
)
|
||||||
|
|
||||||
if old_level > user_level or new_level > user_level:
|
if old_level > user_level or new_level > user_level:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403,
|
403,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -59,6 +59,11 @@ class LoginType(object):
|
|||||||
EMAIL_URL = u"m.login.email.url"
|
EMAIL_URL = u"m.login.email.url"
|
||||||
EMAIL_IDENTITY = u"m.login.email.identity"
|
EMAIL_IDENTITY = u"m.login.email.identity"
|
||||||
RECAPTCHA = u"m.login.recaptcha"
|
RECAPTCHA = u"m.login.recaptcha"
|
||||||
|
DUMMY = u"m.login.dummy"
|
||||||
|
|
||||||
|
# Only for C/S API v1
|
||||||
|
APPLICATION_SERVICE = u"m.login.application_service"
|
||||||
|
SHARED_SECRET = u"org.matrix.login.shared_secret"
|
||||||
|
|
||||||
|
|
||||||
class EventTypes(object):
|
class EventTypes(object):
|
||||||
@@ -70,7 +75,22 @@ class EventTypes(object):
|
|||||||
Redaction = "m.room.redaction"
|
Redaction = "m.room.redaction"
|
||||||
Feedback = "m.room.message.feedback"
|
Feedback = "m.room.message.feedback"
|
||||||
|
|
||||||
|
RoomHistoryVisibility = "m.room.history_visibility"
|
||||||
|
CanonicalAlias = "m.room.canonical_alias"
|
||||||
|
RoomAvatar = "m.room.avatar"
|
||||||
|
|
||||||
# These are used for validation
|
# These are used for validation
|
||||||
Message = "m.room.message"
|
Message = "m.room.message"
|
||||||
Topic = "m.room.topic"
|
Topic = "m.room.topic"
|
||||||
Name = "m.room.name"
|
Name = "m.room.name"
|
||||||
|
|
||||||
|
|
||||||
|
class RejectedReason(object):
|
||||||
|
AUTH_ERROR = "auth_error"
|
||||||
|
REPLACED = "replaced"
|
||||||
|
NOT_ANCESTOR = "not_ancestor"
|
||||||
|
|
||||||
|
|
||||||
|
class RoomCreationPreset(object):
|
||||||
|
PRIVATE_CHAT = "private_chat"
|
||||||
|
PUBLIC_CHAT = "public_chat"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -21,6 +21,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Codes(object):
|
class Codes(object):
|
||||||
|
UNRECOGNIZED = "M_UNRECOGNIZED"
|
||||||
UNAUTHORIZED = "M_UNAUTHORIZED"
|
UNAUTHORIZED = "M_UNAUTHORIZED"
|
||||||
FORBIDDEN = "M_FORBIDDEN"
|
FORBIDDEN = "M_FORBIDDEN"
|
||||||
BAD_JSON = "M_BAD_JSON"
|
BAD_JSON = "M_BAD_JSON"
|
||||||
@@ -30,14 +31,19 @@ class Codes(object):
|
|||||||
BAD_PAGINATION = "M_BAD_PAGINATION"
|
BAD_PAGINATION = "M_BAD_PAGINATION"
|
||||||
UNKNOWN = "M_UNKNOWN"
|
UNKNOWN = "M_UNKNOWN"
|
||||||
NOT_FOUND = "M_NOT_FOUND"
|
NOT_FOUND = "M_NOT_FOUND"
|
||||||
|
MISSING_TOKEN = "M_MISSING_TOKEN"
|
||||||
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||||
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
||||||
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
||||||
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
||||||
|
MISSING_PARAM = "M_MISSING_PARAM"
|
||||||
TOO_LARGE = "M_TOO_LARGE"
|
TOO_LARGE = "M_TOO_LARGE"
|
||||||
|
EXCLUSIVE = "M_EXCLUSIVE"
|
||||||
|
THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
|
||||||
|
THREEPID_IN_USE = "THREEPID_IN_USE"
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(Exception):
|
class CodeMessageException(RuntimeError):
|
||||||
"""An exception with integer code and message string attributes."""
|
"""An exception with integer code and message string attributes."""
|
||||||
|
|
||||||
def __init__(self, code, msg):
|
def __init__(self, code, msg):
|
||||||
@@ -81,6 +87,35 @@ class RegistrationError(SynapseError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UnrecognizedRequestError(SynapseError):
|
||||||
|
"""An error indicating we don't understand the request you're trying to make"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if "errcode" not in kwargs:
|
||||||
|
kwargs["errcode"] = Codes.UNRECOGNIZED
|
||||||
|
message = None
|
||||||
|
if len(args) == 0:
|
||||||
|
message = "Unrecognized request"
|
||||||
|
else:
|
||||||
|
message = args[0]
|
||||||
|
super(UnrecognizedRequestError, self).__init__(
|
||||||
|
400,
|
||||||
|
message,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(SynapseError):
|
||||||
|
"""An error indicating we can't find the thing you asked for"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if "errcode" not in kwargs:
|
||||||
|
kwargs["errcode"] = Codes.NOT_FOUND
|
||||||
|
super(NotFoundError, self).__init__(
|
||||||
|
404,
|
||||||
|
"Not found",
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthError(SynapseError):
|
class AuthError(SynapseError):
|
||||||
"""An error raised when there was a problem authorising an event."""
|
"""An error raised when there was a problem authorising an event."""
|
||||||
|
|
||||||
@@ -196,3 +231,9 @@ class FederationError(RuntimeError):
|
|||||||
"affected": self.affected,
|
"affected": self.affected,
|
||||||
"source": self.source if self.source else self.affected,
|
"source": self.source if self.source else self.affected,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HttpResponseException(CodeMessageException):
|
||||||
|
def __init__(self, code, msg, response):
|
||||||
|
self.response = response
|
||||||
|
super(HttpResponseException, self).__init__(code, msg)
|
||||||
|
|||||||
229
synapse/api/filtering.py
Normal file
229
synapse/api/filtering.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.types import UserID, RoomID
|
||||||
|
|
||||||
|
|
||||||
|
class Filtering(object):
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(Filtering, self).__init__()
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
|
def get_user_filter(self, user_localpart, filter_id):
|
||||||
|
result = self.store.get_user_filter(user_localpart, filter_id)
|
||||||
|
result.addCallback(Filter)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def add_user_filter(self, user_localpart, user_filter):
|
||||||
|
self._check_valid_filter(user_filter)
|
||||||
|
return self.store.add_user_filter(user_localpart, user_filter)
|
||||||
|
|
||||||
|
# TODO(paul): surely we should probably add a delete_user_filter or
|
||||||
|
# replace_user_filter at some point? There's no REST API specified for
|
||||||
|
# them however
|
||||||
|
|
||||||
|
def _check_valid_filter(self, user_filter_json):
|
||||||
|
"""Check if the provided filter is valid.
|
||||||
|
|
||||||
|
This inspects all definitions contained within the filter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_filter_json(dict): The filter
|
||||||
|
Raises:
|
||||||
|
SynapseError: If the filter is not valid.
|
||||||
|
"""
|
||||||
|
# NB: Filters are the complete json blobs. "Definitions" are an
|
||||||
|
# individual top-level key e.g. public_user_data. Filters are made of
|
||||||
|
# many definitions.
|
||||||
|
|
||||||
|
top_level_definitions = [
|
||||||
|
"public_user_data", "private_user_data", "server_data"
|
||||||
|
]
|
||||||
|
|
||||||
|
room_level_definitions = [
|
||||||
|
"state", "events", "ephemeral"
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in top_level_definitions:
|
||||||
|
if key in user_filter_json:
|
||||||
|
self._check_definition(user_filter_json[key])
|
||||||
|
|
||||||
|
if "room" in user_filter_json:
|
||||||
|
for key in room_level_definitions:
|
||||||
|
if key in user_filter_json["room"]:
|
||||||
|
self._check_definition(user_filter_json["room"][key])
|
||||||
|
|
||||||
|
def _check_definition(self, definition):
|
||||||
|
"""Check if the provided definition is valid.
|
||||||
|
|
||||||
|
This inspects not only the types but also the values to make sure they
|
||||||
|
make sense.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
definition(dict): The filter definition
|
||||||
|
Raises:
|
||||||
|
SynapseError: If there was a problem with this definition.
|
||||||
|
"""
|
||||||
|
# NB: Filters are the complete json blobs. "Definitions" are an
|
||||||
|
# individual top-level key e.g. public_user_data. Filters are made of
|
||||||
|
# many definitions.
|
||||||
|
if type(definition) != dict:
|
||||||
|
raise SynapseError(
|
||||||
|
400, "Expected JSON object, not %s" % (definition,)
|
||||||
|
)
|
||||||
|
|
||||||
|
# check rooms are valid room IDs
|
||||||
|
room_id_keys = ["rooms", "not_rooms"]
|
||||||
|
for key in room_id_keys:
|
||||||
|
if key in definition:
|
||||||
|
if type(definition[key]) != list:
|
||||||
|
raise SynapseError(400, "Expected %s to be a list." % key)
|
||||||
|
for room_id in definition[key]:
|
||||||
|
RoomID.from_string(room_id)
|
||||||
|
|
||||||
|
# check senders are valid user IDs
|
||||||
|
user_id_keys = ["senders", "not_senders"]
|
||||||
|
for key in user_id_keys:
|
||||||
|
if key in definition:
|
||||||
|
if type(definition[key]) != list:
|
||||||
|
raise SynapseError(400, "Expected %s to be a list." % key)
|
||||||
|
for user_id in definition[key]:
|
||||||
|
UserID.from_string(user_id)
|
||||||
|
|
||||||
|
# TODO: We don't limit event type values but we probably should...
|
||||||
|
# check types are valid event types
|
||||||
|
event_keys = ["types", "not_types"]
|
||||||
|
for key in event_keys:
|
||||||
|
if key in definition:
|
||||||
|
if type(definition[key]) != list:
|
||||||
|
raise SynapseError(400, "Expected %s to be a list." % key)
|
||||||
|
for event_type in definition[key]:
|
||||||
|
if not isinstance(event_type, basestring):
|
||||||
|
raise SynapseError(400, "Event type should be a string")
|
||||||
|
|
||||||
|
if "format" in definition:
|
||||||
|
event_format = definition["format"]
|
||||||
|
if event_format not in ["federation", "events"]:
|
||||||
|
raise SynapseError(400, "Invalid format: %s" % (event_format,))
|
||||||
|
|
||||||
|
if "select" in definition:
|
||||||
|
event_select_list = definition["select"]
|
||||||
|
for select_key in event_select_list:
|
||||||
|
if select_key not in ["event_id", "origin_server_ts",
|
||||||
|
"thread_id", "content", "content.body"]:
|
||||||
|
raise SynapseError(400, "Bad select: %s" % (select_key,))
|
||||||
|
|
||||||
|
if ("bundle_updates" in definition and
|
||||||
|
type(definition["bundle_updates"]) != bool):
|
||||||
|
raise SynapseError(400, "Bad bundle_updates: expected bool.")
|
||||||
|
|
||||||
|
|
||||||
|
class Filter(object):
|
||||||
|
def __init__(self, filter_json):
|
||||||
|
self.filter_json = filter_json
|
||||||
|
|
||||||
|
def filter_public_user_data(self, events):
|
||||||
|
return self._filter_on_key(events, ["public_user_data"])
|
||||||
|
|
||||||
|
def filter_private_user_data(self, events):
|
||||||
|
return self._filter_on_key(events, ["private_user_data"])
|
||||||
|
|
||||||
|
def filter_room_state(self, events):
|
||||||
|
return self._filter_on_key(events, ["room", "state"])
|
||||||
|
|
||||||
|
def filter_room_events(self, events):
|
||||||
|
return self._filter_on_key(events, ["room", "events"])
|
||||||
|
|
||||||
|
def filter_room_ephemeral(self, events):
|
||||||
|
return self._filter_on_key(events, ["room", "ephemeral"])
|
||||||
|
|
||||||
|
def _filter_on_key(self, events, keys):
|
||||||
|
filter_json = self.filter_json
|
||||||
|
if not filter_json:
|
||||||
|
return events
|
||||||
|
|
||||||
|
try:
|
||||||
|
# extract the right definition from the filter
|
||||||
|
definition = filter_json
|
||||||
|
for key in keys:
|
||||||
|
definition = definition[key]
|
||||||
|
return self._filter_with_definition(events, definition)
|
||||||
|
except KeyError:
|
||||||
|
# return all events if definition isn't specified.
|
||||||
|
return events
|
||||||
|
|
||||||
|
def _filter_with_definition(self, events, definition):
|
||||||
|
return [e for e in events if self._passes_definition(definition, e)]
|
||||||
|
|
||||||
|
def _passes_definition(self, definition, event):
|
||||||
|
"""Check if the event passes through the given definition.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
definition(dict): The definition to check against.
|
||||||
|
event(Event): The event to check.
|
||||||
|
Returns:
|
||||||
|
True if the event passes through the filter.
|
||||||
|
"""
|
||||||
|
# Algorithm notes:
|
||||||
|
# For each key in the definition, check the event meets the criteria:
|
||||||
|
# * For types: Literal match or prefix match (if ends with wildcard)
|
||||||
|
# * For senders/rooms: Literal match only
|
||||||
|
# * "not_" checks take presedence (e.g. if "m.*" is in both 'types'
|
||||||
|
# and 'not_types' then it is treated as only being in 'not_types')
|
||||||
|
|
||||||
|
# room checks
|
||||||
|
if hasattr(event, "room_id"):
|
||||||
|
room_id = event.room_id
|
||||||
|
allow_rooms = definition.get("rooms", None)
|
||||||
|
reject_rooms = definition.get("not_rooms", None)
|
||||||
|
if reject_rooms and room_id in reject_rooms:
|
||||||
|
return False
|
||||||
|
if allow_rooms and room_id not in allow_rooms:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# sender checks
|
||||||
|
if hasattr(event, "sender"):
|
||||||
|
# Should we be including event.state_key for some event types?
|
||||||
|
sender = event.sender
|
||||||
|
allow_senders = definition.get("senders", None)
|
||||||
|
reject_senders = definition.get("not_senders", None)
|
||||||
|
if reject_senders and sender in reject_senders:
|
||||||
|
return False
|
||||||
|
if allow_senders and sender not in allow_senders:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# type checks
|
||||||
|
if "not_types" in definition:
|
||||||
|
for def_type in definition["not_types"]:
|
||||||
|
if self._event_matches_type(event, def_type):
|
||||||
|
return False
|
||||||
|
if "types" in definition:
|
||||||
|
included = False
|
||||||
|
for def_type in definition["types"]:
|
||||||
|
if self._event_matches_type(event, def_type):
|
||||||
|
included = True
|
||||||
|
break
|
||||||
|
if not included:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _event_matches_type(self, event, def_type):
|
||||||
|
if def_type.endswith("*"):
|
||||||
|
type_prefix = def_type[:-1]
|
||||||
|
return event.type.startswith(type_prefix)
|
||||||
|
else:
|
||||||
|
return event.type == def_type
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -16,8 +16,12 @@
|
|||||||
"""Contains the URL paths to prefix various aspects of the server with. """
|
"""Contains the URL paths to prefix various aspects of the server with. """
|
||||||
|
|
||||||
CLIENT_PREFIX = "/_matrix/client/api/v1"
|
CLIENT_PREFIX = "/_matrix/client/api/v1"
|
||||||
|
CLIENT_V2_ALPHA_PREFIX = "/_matrix/client/v2_alpha"
|
||||||
FEDERATION_PREFIX = "/_matrix/federation/v1"
|
FEDERATION_PREFIX = "/_matrix/federation/v1"
|
||||||
|
STATIC_PREFIX = "/_matrix/static"
|
||||||
WEB_CLIENT_PREFIX = "/_matrix/client"
|
WEB_CLIENT_PREFIX = "/_matrix/client"
|
||||||
CONTENT_REPO_PREFIX = "/_matrix/content"
|
CONTENT_REPO_PREFIX = "/_matrix/content"
|
||||||
SERVER_KEY_PREFIX = "/_matrix/key/v1"
|
SERVER_KEY_PREFIX = "/_matrix/key/v1"
|
||||||
|
SERVER_KEY_V2_PREFIX = "/_matrix/key/v2"
|
||||||
MEDIA_PREFIX = "/_matrix/media/v1"
|
MEDIA_PREFIX = "/_matrix/media/v1"
|
||||||
|
APP_SERVICE_PREFIX = "/_matrix/appservice/v1"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,39 +14,72 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from synapse.storage import prepare_database, UpgradeDatabaseException
|
import sys
|
||||||
|
sys.dont_write_bytecode = True
|
||||||
|
from synapse.python_dependencies import check_requirements, DEPENDENCY_LINKS
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
check_requirements()
|
||||||
|
|
||||||
|
from synapse.storage.engines import create_engine, IncorrectDatabaseSetup
|
||||||
|
from synapse.storage import (
|
||||||
|
are_all_users_on_domain, UpgradeDatabaseException,
|
||||||
|
)
|
||||||
|
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
from twisted.application import service
|
||||||
from twisted.enterprise import adbapi
|
from twisted.enterprise import adbapi
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource, EncodingResourceWrapper
|
||||||
from twisted.web.static import File
|
from twisted.web.static import File
|
||||||
from twisted.web.server import Site
|
from twisted.web.server import Site, GzipEncoderFactory, Request
|
||||||
from synapse.http.server import JsonResource, RootRedirect
|
from synapse.http.server import JsonResource, RootRedirect
|
||||||
from synapse.media.v0.content_repository import ContentRepoResource
|
from synapse.rest.media.v0.content_repository import ContentRepoResource
|
||||||
from synapse.media.v1.media_repository import MediaRepositoryResource
|
from synapse.rest.media.v1.media_repository import MediaRepositoryResource
|
||||||
from synapse.http.server_key_resource import LocalKey
|
from synapse.rest.key.v1.server_key_resource import LocalKey
|
||||||
|
from synapse.rest.key.v2 import KeyApiV2Resource
|
||||||
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
from synapse.http.matrixfederationclient import MatrixFederationHttpClient
|
||||||
from synapse.api.urls import (
|
from synapse.api.urls import (
|
||||||
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
|
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
|
||||||
SERVER_KEY_PREFIX, MEDIA_PREFIX
|
SERVER_KEY_PREFIX, MEDIA_PREFIX, CLIENT_V2_ALPHA_PREFIX, STATIC_PREFIX,
|
||||||
|
SERVER_KEY_V2_PREFIX,
|
||||||
)
|
)
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
from synapse.crypto import context_factory
|
from synapse.crypto import context_factory
|
||||||
from synapse.util.logcontext import LoggingContext
|
from synapse.util.logcontext import LoggingContext
|
||||||
|
from synapse.rest.client.v1 import ClientV1RestResource
|
||||||
|
from synapse.rest.client.v2_alpha import ClientV2AlphaRestResource
|
||||||
|
from synapse.metrics.resource import MetricsResource, METRICS_PREFIX
|
||||||
|
|
||||||
|
from synapse import events
|
||||||
|
|
||||||
from daemonize import Daemonize
|
from daemonize import Daemonize
|
||||||
import twisted.manhole.telnet
|
import twisted.manhole.telnet
|
||||||
|
|
||||||
|
import synapse
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import resource
|
||||||
import sqlite3
|
import subprocess
|
||||||
import syweb
|
import time
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
logger = logging.getLogger("synapse.app.homeserver")
|
||||||
|
|
||||||
|
|
||||||
|
class GzipFile(File):
|
||||||
|
def getChild(self, path, request):
|
||||||
|
child = File.getChild(self, path, request)
|
||||||
|
return EncodingResourceWrapper(child, [GzipEncoderFactory()])
|
||||||
|
|
||||||
|
|
||||||
|
def gz_wrap(r):
|
||||||
|
return EncodingResourceWrapper(r, [GzipEncoderFactory()])
|
||||||
|
|
||||||
|
|
||||||
class SynapseHomeServer(HomeServer):
|
class SynapseHomeServer(HomeServer):
|
||||||
@@ -55,19 +88,49 @@ class SynapseHomeServer(HomeServer):
|
|||||||
return MatrixFederationHttpClient(self)
|
return MatrixFederationHttpClient(self)
|
||||||
|
|
||||||
def build_resource_for_client(self):
|
def build_resource_for_client(self):
|
||||||
return JsonResource()
|
return ClientV1RestResource(self)
|
||||||
|
|
||||||
|
def build_resource_for_client_v2_alpha(self):
|
||||||
|
return ClientV2AlphaRestResource(self)
|
||||||
|
|
||||||
def build_resource_for_federation(self):
|
def build_resource_for_federation(self):
|
||||||
return JsonResource()
|
return JsonResource(self)
|
||||||
|
|
||||||
def build_resource_for_web_client(self):
|
def build_resource_for_web_client(self):
|
||||||
syweb_path = os.path.dirname(syweb.__file__)
|
webclient_path = self.get_config().web_client_location
|
||||||
webclient_path = os.path.join(syweb_path, "webclient")
|
if not webclient_path:
|
||||||
|
try:
|
||||||
|
import syweb
|
||||||
|
except ImportError:
|
||||||
|
quit_with_error(
|
||||||
|
"Could not find a webclient.\n\n"
|
||||||
|
"Please either install the matrix-angular-sdk or configure\n"
|
||||||
|
"the location of the source to serve via the configuration\n"
|
||||||
|
"option `web_client_location`\n\n"
|
||||||
|
"To install the `matrix-angular-sdk` via pip, run:\n\n"
|
||||||
|
" pip install '%(dep)s'\n"
|
||||||
|
"\n"
|
||||||
|
"You can also disable hosting of the webclient via the\n"
|
||||||
|
"configuration option `web_client`\n"
|
||||||
|
% {"dep": DEPENDENCY_LINKS["matrix-angular-sdk"]}
|
||||||
|
)
|
||||||
|
syweb_path = os.path.dirname(syweb.__file__)
|
||||||
|
webclient_path = os.path.join(syweb_path, "webclient")
|
||||||
|
# GZip is disabled here due to
|
||||||
|
# https://twistedmatrix.com/trac/ticket/7678
|
||||||
|
# (It can stay enabled for the API resources: they call
|
||||||
|
# write() with the whole body and then finish() straight
|
||||||
|
# after and so do not trigger the bug.
|
||||||
|
# return GzipFile(webclient_path) # TODO configurable?
|
||||||
return File(webclient_path) # TODO configurable?
|
return File(webclient_path) # TODO configurable?
|
||||||
|
|
||||||
|
def build_resource_for_static_content(self):
|
||||||
|
# This is old and should go away: not going to bother adding gzip
|
||||||
|
return File("static")
|
||||||
|
|
||||||
def build_resource_for_content_repo(self):
|
def build_resource_for_content_repo(self):
|
||||||
return ContentRepoResource(
|
return ContentRepoResource(
|
||||||
self, self.upload_dir, self.auth, self.content_addr
|
self, self.config.uploads_path, self.auth, self.content_addr
|
||||||
)
|
)
|
||||||
|
|
||||||
def build_resource_for_media_repository(self):
|
def build_resource_for_media_repository(self):
|
||||||
@@ -76,190 +139,546 @@ class SynapseHomeServer(HomeServer):
|
|||||||
def build_resource_for_server_key(self):
|
def build_resource_for_server_key(self):
|
||||||
return LocalKey(self)
|
return LocalKey(self)
|
||||||
|
|
||||||
|
def build_resource_for_server_key_v2(self):
|
||||||
|
return KeyApiV2Resource(self)
|
||||||
|
|
||||||
|
def build_resource_for_metrics(self):
|
||||||
|
if self.get_config().enable_metrics:
|
||||||
|
return MetricsResource(self)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def build_db_pool(self):
|
def build_db_pool(self):
|
||||||
|
name = self.db_config["name"]
|
||||||
|
|
||||||
return adbapi.ConnectionPool(
|
return adbapi.ConnectionPool(
|
||||||
"sqlite3", self.get_db_name(),
|
name,
|
||||||
check_same_thread=False,
|
**self.db_config.get("args", {})
|
||||||
cp_min=1,
|
|
||||||
cp_max=1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_resource_tree(self, web_client, redirect_root_to_web_client):
|
def _listener_http(self, config, listener_config):
|
||||||
"""Create the resource tree for this Home Server.
|
port = listener_config["port"]
|
||||||
|
bind_address = listener_config.get("bind_address", "")
|
||||||
|
tls = listener_config.get("tls", False)
|
||||||
|
site_tag = listener_config.get("tag", port)
|
||||||
|
|
||||||
This in unduly complicated because Twisted does not support putting
|
if tls and config.no_tls:
|
||||||
child resources more than 1 level deep at a time.
|
return
|
||||||
|
|
||||||
Args:
|
metrics_resource = self.get_resource_for_metrics()
|
||||||
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()),
|
|
||||||
(SERVER_KEY_PREFIX, self.get_resource_for_server_key()),
|
|
||||||
(MEDIA_PREFIX, self.get_resource_for_media_repository()),
|
|
||||||
]
|
|
||||||
if web_client:
|
|
||||||
logger.info("Adding the web client.")
|
|
||||||
desired_tree.append((WEB_CLIENT_PREFIX,
|
|
||||||
self.get_resource_for_web_client()))
|
|
||||||
|
|
||||||
if web_client and redirect_root_to_web_client:
|
resources = {}
|
||||||
self.root_resource = RootRedirect(WEB_CLIENT_PREFIX)
|
for res in listener_config["resources"]:
|
||||||
else:
|
for name in res["names"]:
|
||||||
self.root_resource = Resource()
|
if name == "client":
|
||||||
|
if res["compress"]:
|
||||||
|
client_v1 = gz_wrap(self.get_resource_for_client())
|
||||||
|
client_v2 = gz_wrap(self.get_resource_for_client_v2_alpha())
|
||||||
|
else:
|
||||||
|
client_v1 = self.get_resource_for_client()
|
||||||
|
client_v2 = self.get_resource_for_client_v2_alpha()
|
||||||
|
|
||||||
# ideally we'd just use getChild and putChild but getChild doesn't work
|
resources.update({
|
||||||
# unless you give it a Request object IN ADDITION to the name :/ So
|
CLIENT_PREFIX: client_v1,
|
||||||
# instead, we'll store a copy of this mapping so we can actually add
|
CLIENT_V2_ALPHA_PREFIX: client_v2,
|
||||||
# extra resources to existing nodes. See self._resource_id for the key.
|
})
|
||||||
resource_mappings = {}
|
|
||||||
for (full_path, resource) in desired_tree:
|
|
||||||
logger.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]
|
|
||||||
|
|
||||||
# ===========================
|
if name == "federation":
|
||||||
# now attach the actual desired resource
|
resources.update({
|
||||||
last_path_seg = full_path.split('/')[-1]
|
FEDERATION_PREFIX: self.get_resource_for_federation(),
|
||||||
|
})
|
||||||
|
|
||||||
# if there is already a resource here, thieve its children and
|
if name in ["static", "client"]:
|
||||||
# replace it
|
resources.update({
|
||||||
res_id = self._resource_id(last_resource, last_path_seg)
|
STATIC_PREFIX: self.get_resource_for_static_content(),
|
||||||
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)
|
|
||||||
|
|
||||||
# finally, insert the desired resource in the right place
|
if name in ["media", "federation", "client"]:
|
||||||
last_resource.putChild(last_path_seg, resource)
|
resources.update({
|
||||||
res_id = self._resource_id(last_resource, last_path_seg)
|
MEDIA_PREFIX: self.get_resource_for_media_repository(),
|
||||||
resource_mappings[res_id] = resource
|
CONTENT_REPO_PREFIX: self.get_resource_for_content_repo(),
|
||||||
|
})
|
||||||
|
|
||||||
return self.root_resource
|
if name in ["keys", "federation"]:
|
||||||
|
resources.update({
|
||||||
|
SERVER_KEY_PREFIX: self.get_resource_for_server_key(),
|
||||||
|
SERVER_KEY_V2_PREFIX: self.get_resource_for_server_key_v2(),
|
||||||
|
})
|
||||||
|
|
||||||
def _resource_id(self, resource, path_seg):
|
if name == "webclient":
|
||||||
"""Construct an arbitrary resource ID so you can retrieve the mapping
|
resources[WEB_CLIENT_PREFIX] = self.get_resource_for_web_client()
|
||||||
later.
|
|
||||||
|
|
||||||
If you want to represent resource A putChild resource B with path C,
|
if name == "metrics" and metrics_resource:
|
||||||
the mapping should looks like _resource_id(A,C) = B.
|
resources[METRICS_PREFIX] = metrics_resource
|
||||||
|
|
||||||
Args:
|
root_resource = create_resource_tree(resources)
|
||||||
resource (Resource): The *parent* Resource
|
if tls:
|
||||||
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(
|
reactor.listenSSL(
|
||||||
secure_port, Site(self.root_resource), self.tls_context_factory
|
port,
|
||||||
|
SynapseSite(
|
||||||
|
"synapse.access.https.%s" % (site_tag,),
|
||||||
|
site_tag,
|
||||||
|
listener_config,
|
||||||
|
root_resource,
|
||||||
|
),
|
||||||
|
self.tls_context_factory,
|
||||||
|
interface=bind_address
|
||||||
)
|
)
|
||||||
logger.info("Synapse now listening on port %d", secure_port)
|
else:
|
||||||
if unsecure_port is not None:
|
|
||||||
reactor.listenTCP(
|
reactor.listenTCP(
|
||||||
unsecure_port, Site(self.root_resource)
|
port,
|
||||||
|
SynapseSite(
|
||||||
|
"synapse.access.http.%s" % (site_tag,),
|
||||||
|
site_tag,
|
||||||
|
listener_config,
|
||||||
|
root_resource,
|
||||||
|
),
|
||||||
|
interface=bind_address
|
||||||
)
|
)
|
||||||
logger.info("Synapse now listening on port %d", unsecure_port)
|
logger.info("Synapse now listening on port %d", port)
|
||||||
|
|
||||||
|
def start_listening(self):
|
||||||
|
config = self.get_config()
|
||||||
|
|
||||||
|
for listener in config.listeners:
|
||||||
|
if listener["type"] == "http":
|
||||||
|
self._listener_http(config, listener)
|
||||||
|
elif listener["type"] == "manhole":
|
||||||
|
f = twisted.manhole.telnet.ShellFactory()
|
||||||
|
f.username = "matrix"
|
||||||
|
f.password = "rabbithole"
|
||||||
|
f.namespace['hs'] = self
|
||||||
|
reactor.listenTCP(
|
||||||
|
listener["port"],
|
||||||
|
f,
|
||||||
|
interface=listener.get("bind_address", '127.0.0.1')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warn("Unrecognized listener type: %s", listener["type"])
|
||||||
|
|
||||||
|
def run_startup_checks(self, db_conn, database_engine):
|
||||||
|
all_users_native = are_all_users_on_domain(
|
||||||
|
db_conn.cursor(), database_engine, self.hostname
|
||||||
|
)
|
||||||
|
if not all_users_native:
|
||||||
|
quit_with_error(
|
||||||
|
"Found users in database not native to %s!\n"
|
||||||
|
"You cannot changed a synapse server_name after it's been configured"
|
||||||
|
% (self.hostname,)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
database_engine.check_database(db_conn.cursor())
|
||||||
|
except IncorrectDatabaseSetup as e:
|
||||||
|
quit_with_error(e.message)
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
def quit_with_error(error_string):
|
||||||
|
message_lines = error_string.split("\n")
|
||||||
|
line_length = max([len(l) for l in message_lines if len(l) < 80]) + 2
|
||||||
|
sys.stderr.write("*" * line_length + '\n')
|
||||||
|
for line in message_lines:
|
||||||
|
sys.stderr.write(" %s\n" % (line.rstrip(),))
|
||||||
|
sys.stderr.write("*" * line_length + '\n')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_version_string():
|
||||||
|
try:
|
||||||
|
null = open(os.devnull, 'w')
|
||||||
|
cwd = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
try:
|
||||||
|
git_branch = subprocess.check_output(
|
||||||
|
['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
||||||
|
stderr=null,
|
||||||
|
cwd=cwd,
|
||||||
|
).strip()
|
||||||
|
git_branch = "b=" + git_branch
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
git_branch = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
git_tag = subprocess.check_output(
|
||||||
|
['git', 'describe', '--exact-match'],
|
||||||
|
stderr=null,
|
||||||
|
cwd=cwd,
|
||||||
|
).strip()
|
||||||
|
git_tag = "t=" + git_tag
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
git_tag = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
git_commit = subprocess.check_output(
|
||||||
|
['git', 'rev-parse', '--short', 'HEAD'],
|
||||||
|
stderr=null,
|
||||||
|
cwd=cwd,
|
||||||
|
).strip()
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
git_commit = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
dirty_string = "-this_is_a_dirty_checkout"
|
||||||
|
is_dirty = subprocess.check_output(
|
||||||
|
['git', 'describe', '--dirty=' + dirty_string],
|
||||||
|
stderr=null,
|
||||||
|
cwd=cwd,
|
||||||
|
).strip().endswith(dirty_string)
|
||||||
|
|
||||||
|
git_dirty = "dirty" if is_dirty else ""
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
git_dirty = ""
|
||||||
|
|
||||||
|
if git_branch or git_tag or git_commit or git_dirty:
|
||||||
|
git_version = ",".join(
|
||||||
|
s for s in
|
||||||
|
(git_branch, git_tag, git_commit, git_dirty,)
|
||||||
|
if s
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
"Synapse/%s (%s)" % (
|
||||||
|
synapse.__version__, git_version,
|
||||||
|
)
|
||||||
|
).encode("ascii")
|
||||||
|
except Exception as e:
|
||||||
|
logger.info("Failed to check for git repository: %s", e)
|
||||||
|
|
||||||
|
return ("Synapse/%s" % (synapse.__version__,)).encode("ascii")
|
||||||
|
|
||||||
|
|
||||||
|
def change_resource_limit(soft_file_no):
|
||||||
|
try:
|
||||||
|
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||||
|
|
||||||
|
if not soft_file_no:
|
||||||
|
soft_file_no = hard
|
||||||
|
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (soft_file_no, hard))
|
||||||
|
|
||||||
|
logger.info("Set file limit to: %d", soft_file_no)
|
||||||
|
except (ValueError, resource.error) as e:
|
||||||
|
logger.warn("Failed to set file limit: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(config_options):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
config_options_options: The options passed to Synapse. Usually
|
||||||
|
`sys.argv[1:]`.
|
||||||
|
should_run (bool): Whether to start the reactor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HomeServer
|
||||||
|
"""
|
||||||
config = HomeServerConfig.load_config(
|
config = HomeServerConfig.load_config(
|
||||||
"Synapse Homeserver",
|
"Synapse Homeserver",
|
||||||
sys.argv[1:],
|
config_options,
|
||||||
generate_section="Homeserver"
|
generate_section="Homeserver"
|
||||||
)
|
)
|
||||||
|
|
||||||
config.setup_logging()
|
config.setup_logging()
|
||||||
|
|
||||||
logger.info("Server hostname: %s", config.server_name)
|
# check any extra requirements we have now we have a config
|
||||||
|
check_requirements(config)
|
||||||
|
|
||||||
if re.search(":[0-9]+$", config.server_name):
|
version_string = get_version_string()
|
||||||
domain_with_port = config.server_name
|
|
||||||
else:
|
logger.info("Server hostname: %s", config.server_name)
|
||||||
domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
|
logger.info("Server version: %s", version_string)
|
||||||
|
|
||||||
|
events.USE_FROZEN_DICTS = config.use_frozen_dicts
|
||||||
|
|
||||||
tls_context_factory = context_factory.ServerContextFactory(config)
|
tls_context_factory = context_factory.ServerContextFactory(config)
|
||||||
|
|
||||||
|
database_engine = create_engine(config.database_config["name"])
|
||||||
|
config.database_config["args"]["cp_openfun"] = database_engine.on_new_connection
|
||||||
|
|
||||||
hs = SynapseHomeServer(
|
hs = SynapseHomeServer(
|
||||||
config.server_name,
|
config.server_name,
|
||||||
domain_with_port=domain_with_port,
|
db_config=config.database_config,
|
||||||
upload_dir=os.path.abspath("uploads"),
|
|
||||||
db_name=config.database_path,
|
|
||||||
tls_context_factory=tls_context_factory,
|
tls_context_factory=tls_context_factory,
|
||||||
config=config,
|
config=config,
|
||||||
content_addr=config.content_addr,
|
content_addr=config.content_addr,
|
||||||
|
version_string=version_string,
|
||||||
|
database_engine=database_engine,
|
||||||
)
|
)
|
||||||
|
|
||||||
hs.register_servlets()
|
logger.info("Preparing database: %s...", config.database_config['name'])
|
||||||
|
|
||||||
hs.create_resource_tree(
|
|
||||||
web_client=config.webclient,
|
|
||||||
redirect_root_to_web_client=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
db_name = hs.get_db_name()
|
|
||||||
|
|
||||||
logger.info("Preparing database: %s...", db_name)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with sqlite3.connect(db_name) as db_conn:
|
db_conn = database_engine.module.connect(
|
||||||
prepare_database(db_conn)
|
**{
|
||||||
|
k: v for k, v in config.database_config.get("args", {}).items()
|
||||||
|
if not k.startswith("cp_")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
database_engine.prepare_database(db_conn)
|
||||||
|
hs.run_startup_checks(db_conn, database_engine)
|
||||||
|
|
||||||
|
db_conn.commit()
|
||||||
except UpgradeDatabaseException:
|
except UpgradeDatabaseException:
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
"\nFailed to upgrade database.\n"
|
"\nFailed to upgrade database.\n"
|
||||||
"Have you followed any instructions in UPGRADES.rst?\n"
|
"Have you checked for version specific instructions in"
|
||||||
|
" UPGRADES.rst?\n"
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
logger.info("Database prepared in %s.", db_name)
|
logger.info("Database prepared in %s.", config.database_config['name'])
|
||||||
|
|
||||||
hs.get_db_pool()
|
hs.start_listening()
|
||||||
|
|
||||||
if config.manhole:
|
hs.get_pusherpool().start()
|
||||||
f = twisted.manhole.telnet.ShellFactory()
|
hs.get_state_handler().start_caching()
|
||||||
f.username = "matrix"
|
hs.get_datastore().start_profiling()
|
||||||
f.password = "rabbithole"
|
hs.get_replication_layer().start_get_pdu_cache()
|
||||||
f.namespace['hs'] = hs
|
|
||||||
reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
|
|
||||||
|
|
||||||
bind_port = config.bind_port
|
return hs
|
||||||
if config.no_tls:
|
|
||||||
bind_port = None
|
|
||||||
hs.start_listening(bind_port, config.unsecure_port)
|
class SynapseService(service.Service):
|
||||||
|
"""A twisted Service class that will start synapse. Used to run synapse
|
||||||
|
via twistd and a .tac.
|
||||||
|
"""
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def startService(self):
|
||||||
|
hs = setup(self.config)
|
||||||
|
change_resource_limit(hs.config.soft_file_limit)
|
||||||
|
|
||||||
|
def stopService(self):
|
||||||
|
return self._port.stopListening()
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseRequest(Request):
|
||||||
|
def __init__(self, site, *args, **kw):
|
||||||
|
Request.__init__(self, *args, **kw)
|
||||||
|
self.site = site
|
||||||
|
self.authenticated_entity = None
|
||||||
|
self.start_time = 0
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
# We overwrite this so that we don't log ``access_token``
|
||||||
|
return '<%s at 0x%x method=%s uri=%s clientproto=%s site=%s>' % (
|
||||||
|
self.__class__.__name__,
|
||||||
|
id(self),
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri(),
|
||||||
|
self.clientproto,
|
||||||
|
self.site.site_tag,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_redacted_uri(self):
|
||||||
|
return re.sub(
|
||||||
|
r'(\?.*access_token=)[^&]*(.*)$',
|
||||||
|
r'\1<redacted>\2',
|
||||||
|
self.uri
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_user_agent(self):
|
||||||
|
return self.requestHeaders.getRawHeaders("User-Agent", [None])[-1]
|
||||||
|
|
||||||
|
def started_processing(self):
|
||||||
|
self.site.access_logger.info(
|
||||||
|
"%s - %s - Received request: %s %s",
|
||||||
|
self.getClientIP(),
|
||||||
|
self.site.site_tag,
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri()
|
||||||
|
)
|
||||||
|
self.start_time = int(time.time() * 1000)
|
||||||
|
|
||||||
|
def finished_processing(self):
|
||||||
|
self.site.access_logger.info(
|
||||||
|
"%s - %s - {%s}"
|
||||||
|
" Processed request: %dms %sB %s \"%s %s %s\" \"%s\"",
|
||||||
|
self.getClientIP(),
|
||||||
|
self.site.site_tag,
|
||||||
|
self.authenticated_entity,
|
||||||
|
int(time.time() * 1000) - self.start_time,
|
||||||
|
self.sentLength,
|
||||||
|
self.code,
|
||||||
|
self.method,
|
||||||
|
self.get_redacted_uri(),
|
||||||
|
self.clientproto,
|
||||||
|
self.get_user_agent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def processing(self):
|
||||||
|
self.started_processing()
|
||||||
|
yield
|
||||||
|
self.finished_processing()
|
||||||
|
|
||||||
|
|
||||||
|
class XForwardedForRequest(SynapseRequest):
|
||||||
|
def __init__(self, *args, **kw):
|
||||||
|
SynapseRequest.__init__(self, *args, **kw)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Add a layer on top of another request that only uses the value of an
|
||||||
|
X-Forwarded-For header as the result of C{getClientIP}.
|
||||||
|
"""
|
||||||
|
def getClientIP(self):
|
||||||
|
"""
|
||||||
|
@return: The client address (the first address) in the value of the
|
||||||
|
I{X-Forwarded-For header}. If the header is not present, return
|
||||||
|
C{b"-"}.
|
||||||
|
"""
|
||||||
|
return self.requestHeaders.getRawHeaders(
|
||||||
|
b"x-forwarded-for", [b"-"])[0].split(b",")[0].strip()
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseRequestFactory(object):
|
||||||
|
def __init__(self, site, x_forwarded_for):
|
||||||
|
self.site = site
|
||||||
|
self.x_forwarded_for = x_forwarded_for
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
if self.x_forwarded_for:
|
||||||
|
return XForwardedForRequest(self.site, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
return SynapseRequest(self.site, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseSite(Site):
|
||||||
|
"""
|
||||||
|
Subclass of a twisted http Site that does access logging with python's
|
||||||
|
standard logging
|
||||||
|
"""
|
||||||
|
def __init__(self, logger_name, site_tag, config, resource, *args, **kwargs):
|
||||||
|
Site.__init__(self, resource, *args, **kwargs)
|
||||||
|
|
||||||
|
self.site_tag = site_tag
|
||||||
|
|
||||||
|
proxied = config.get("x_forwarded", False)
|
||||||
|
self.requestFactory = SynapseRequestFactory(self, proxied)
|
||||||
|
self.access_logger = logging.getLogger(logger_name)
|
||||||
|
|
||||||
|
def log(self, request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def create_resource_tree(desired_tree, redirect_root_to_web_client=True):
|
||||||
|
"""Create the resource tree for this Home Server.
|
||||||
|
|
||||||
|
This in unduly complicated because Twisted does not support putting
|
||||||
|
child resources more than 1 level deep at a time.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
if redirect_root_to_web_client and WEB_CLIENT_PREFIX in desired_tree:
|
||||||
|
root_resource = RootRedirect(WEB_CLIENT_PREFIX)
|
||||||
|
else:
|
||||||
|
root_resource = Resource()
|
||||||
|
|
||||||
|
# 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, res in desired_tree.items():
|
||||||
|
logger.info("Attaching %s to path %s", res, full_path)
|
||||||
|
last_resource = root_resource
|
||||||
|
for path_seg in full_path.split('/')[1:-1]:
|
||||||
|
if path_seg not 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 = _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 = _resource_id(last_resource, path_seg)
|
||||||
|
last_resource = resource_mappings[res_id]
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# now attach the actual desired resource
|
||||||
|
last_path_seg = full_path.split('/')[-1]
|
||||||
|
|
||||||
|
# if there is already a resource here, thieve its children and
|
||||||
|
# replace it
|
||||||
|
res_id = _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 = _resource_id(
|
||||||
|
existing_dummy_resource, child_name
|
||||||
|
)
|
||||||
|
child_resource = resource_mappings[child_res_id]
|
||||||
|
# steal the children
|
||||||
|
res.putChild(child_name, child_resource)
|
||||||
|
|
||||||
|
# finally, insert the desired resource in the right place
|
||||||
|
last_resource.putChild(last_path_seg, res)
|
||||||
|
res_id = _resource_id(last_resource, last_path_seg)
|
||||||
|
resource_mappings[res_id] = res
|
||||||
|
|
||||||
|
return root_resource
|
||||||
|
|
||||||
|
|
||||||
|
def _resource_id(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 run(hs):
|
||||||
|
PROFILE_SYNAPSE = False
|
||||||
|
if PROFILE_SYNAPSE:
|
||||||
|
def profile(func):
|
||||||
|
from cProfile import Profile
|
||||||
|
from threading import current_thread
|
||||||
|
|
||||||
|
def profiled(*args, **kargs):
|
||||||
|
profile = Profile()
|
||||||
|
profile.enable()
|
||||||
|
func(*args, **kargs)
|
||||||
|
profile.disable()
|
||||||
|
ident = current_thread().ident
|
||||||
|
profile.dump_stats("/tmp/%s.%s.%i.pstat" % (
|
||||||
|
hs.hostname, func.__name__, ident
|
||||||
|
))
|
||||||
|
|
||||||
|
return profiled
|
||||||
|
|
||||||
|
from twisted.python.threadpool import ThreadPool
|
||||||
|
ThreadPool._worker = profile(ThreadPool._worker)
|
||||||
|
reactor.run = profile(reactor.run)
|
||||||
|
|
||||||
|
def in_thread():
|
||||||
|
with LoggingContext("run"):
|
||||||
|
change_resource_limit(hs.config.soft_file_limit)
|
||||||
|
reactor.run()
|
||||||
|
|
||||||
|
if hs.config.daemonize:
|
||||||
|
|
||||||
|
if hs.config.print_pidfile:
|
||||||
|
print hs.config.pid_file
|
||||||
|
|
||||||
if config.daemonize:
|
|
||||||
print config.pid_file
|
|
||||||
daemon = Daemonize(
|
daemon = Daemonize(
|
||||||
app="synapse-homeserver",
|
app="synapse-homeserver",
|
||||||
pid=config.pid_file,
|
pid=hs.config.pid_file,
|
||||||
action=run,
|
action=lambda: in_thread(),
|
||||||
auto_close_fds=False,
|
auto_close_fds=False,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
logger=logger,
|
logger=logger,
|
||||||
@@ -267,17 +686,15 @@ def setup():
|
|||||||
|
|
||||||
daemon.start()
|
daemon.start()
|
||||||
else:
|
else:
|
||||||
reactor.run()
|
in_thread()
|
||||||
|
|
||||||
|
|
||||||
def run():
|
|
||||||
with LoggingContext("run"):
|
|
||||||
reactor.run()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with LoggingContext("main"):
|
with LoggingContext("main"):
|
||||||
setup()
|
# check base requirements
|
||||||
|
check_requirements()
|
||||||
|
hs = setup(sys.argv[1:])
|
||||||
|
run(hs)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -18,29 +18,33 @@ import sys
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import signal
|
import signal
|
||||||
|
import yaml
|
||||||
|
|
||||||
SYNAPSE = ["python", "-m", "synapse.app.homeserver"]
|
SYNAPSE = ["python", "-B", "-m", "synapse.app.homeserver"]
|
||||||
|
|
||||||
CONFIGFILE = "homeserver.yaml"
|
CONFIGFILE = "homeserver.yaml"
|
||||||
PIDFILE = "homeserver.pid"
|
|
||||||
|
|
||||||
GREEN = "\x1b[1;32m"
|
GREEN = "\x1b[1;32m"
|
||||||
NORMAL = "\x1b[m"
|
NORMAL = "\x1b[m"
|
||||||
|
|
||||||
|
if not os.path.exists(CONFIGFILE):
|
||||||
|
sys.stderr.write(
|
||||||
|
"No config file found\n"
|
||||||
|
"To generate a config file, run '%s -c %s --generate-config"
|
||||||
|
" --server-name=<server name>'\n" % (
|
||||||
|
" ".join(SYNAPSE), CONFIGFILE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
CONFIG = yaml.load(open(CONFIGFILE))
|
||||||
|
PIDFILE = CONFIG["pid_file"]
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
if not os.path.exists(CONFIGFILE):
|
|
||||||
sys.stderr.write(
|
|
||||||
"No config file found\n"
|
|
||||||
"To generate a config file, run '%s -c %s --generate-config"
|
|
||||||
" --server-name=<server name>'\n" % (
|
|
||||||
" ".join(SYNAPSE), CONFIGFILE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
print "Starting ...",
|
print "Starting ...",
|
||||||
args = SYNAPSE
|
args = SYNAPSE
|
||||||
args.extend(["--daemonize", "-c", CONFIGFILE, "--pid-file", PIDFILE])
|
args.extend(["--daemonize", "-c", CONFIGFILE])
|
||||||
subprocess.check_call(args)
|
subprocess.check_call(args)
|
||||||
print GREEN + "started" + NORMAL
|
print GREEN + "started" + NORMAL
|
||||||
|
|
||||||
|
|||||||
226
synapse/appservice/__init__.py
Normal file
226
synapse/appservice/__init__.py
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
from synapse.api.constants import EventTypes
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationServiceState(object):
|
||||||
|
DOWN = "down"
|
||||||
|
UP = "up"
|
||||||
|
|
||||||
|
|
||||||
|
class AppServiceTransaction(object):
|
||||||
|
"""Represents an application service transaction."""
|
||||||
|
|
||||||
|
def __init__(self, service, id, events):
|
||||||
|
self.service = service
|
||||||
|
self.id = id
|
||||||
|
self.events = events
|
||||||
|
|
||||||
|
def send(self, as_api):
|
||||||
|
"""Sends this transaction using the provided AS API interface.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
as_api(ApplicationServiceApi): The API to use to send.
|
||||||
|
Returns:
|
||||||
|
A Deferred which resolves to True if the transaction was sent.
|
||||||
|
"""
|
||||||
|
return as_api.push_bulk(
|
||||||
|
service=self.service,
|
||||||
|
events=self.events,
|
||||||
|
txn_id=self.id
|
||||||
|
)
|
||||||
|
|
||||||
|
def complete(self, store):
|
||||||
|
"""Completes this transaction as successful.
|
||||||
|
|
||||||
|
Marks this transaction ID on the application service and removes the
|
||||||
|
transaction contents from the database.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
store: The database store to operate on.
|
||||||
|
Returns:
|
||||||
|
A Deferred which resolves to True if the transaction was completed.
|
||||||
|
"""
|
||||||
|
return store.complete_appservice_txn(
|
||||||
|
service=self.service,
|
||||||
|
txn_id=self.id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationService(object):
|
||||||
|
"""Defines an application service. This definition is mostly what is
|
||||||
|
provided to the /register AS API.
|
||||||
|
|
||||||
|
Provides methods to check if this service is "interested" in events.
|
||||||
|
"""
|
||||||
|
NS_USERS = "users"
|
||||||
|
NS_ALIASES = "aliases"
|
||||||
|
NS_ROOMS = "rooms"
|
||||||
|
# The ordering here is important as it is used to map database values (which
|
||||||
|
# are stored as ints representing the position in this list) to namespace
|
||||||
|
# values.
|
||||||
|
NS_LIST = [NS_USERS, NS_ALIASES, NS_ROOMS]
|
||||||
|
|
||||||
|
def __init__(self, token, url=None, namespaces=None, hs_token=None,
|
||||||
|
sender=None, id=None):
|
||||||
|
self.token = token
|
||||||
|
self.url = url
|
||||||
|
self.hs_token = hs_token
|
||||||
|
self.sender = sender
|
||||||
|
self.namespaces = self._check_namespaces(namespaces)
|
||||||
|
self.id = id
|
||||||
|
|
||||||
|
def _check_namespaces(self, namespaces):
|
||||||
|
# Sanity check that it is of the form:
|
||||||
|
# {
|
||||||
|
# users: [ {regex: "[A-z]+.*", exclusive: true}, ...],
|
||||||
|
# aliases: [ {regex: "[A-z]+.*", exclusive: true}, ...],
|
||||||
|
# rooms: [ {regex: "[A-z]+.*", exclusive: true}, ...],
|
||||||
|
# }
|
||||||
|
if not namespaces:
|
||||||
|
namespaces = {}
|
||||||
|
|
||||||
|
for ns in ApplicationService.NS_LIST:
|
||||||
|
if ns not in namespaces:
|
||||||
|
namespaces[ns] = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
if type(namespaces[ns]) != list:
|
||||||
|
raise ValueError("Bad namespace value for '%s'" % ns)
|
||||||
|
for regex_obj in namespaces[ns]:
|
||||||
|
if not isinstance(regex_obj, dict):
|
||||||
|
raise ValueError("Expected dict regex for ns '%s'" % ns)
|
||||||
|
if not isinstance(regex_obj.get("exclusive"), bool):
|
||||||
|
raise ValueError(
|
||||||
|
"Expected bool for 'exclusive' in ns '%s'" % ns
|
||||||
|
)
|
||||||
|
if not isinstance(regex_obj.get("regex"), basestring):
|
||||||
|
raise ValueError(
|
||||||
|
"Expected string for 'regex' in ns '%s'" % ns
|
||||||
|
)
|
||||||
|
return namespaces
|
||||||
|
|
||||||
|
def _matches_regex(self, test_string, namespace_key, return_obj=False):
|
||||||
|
if not isinstance(test_string, basestring):
|
||||||
|
logger.error(
|
||||||
|
"Expected a string to test regex against, but got %s",
|
||||||
|
test_string
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
for regex_obj in self.namespaces[namespace_key]:
|
||||||
|
if re.match(regex_obj["regex"], test_string):
|
||||||
|
if return_obj:
|
||||||
|
return regex_obj
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _is_exclusive(self, ns_key, test_string):
|
||||||
|
regex_obj = self._matches_regex(test_string, ns_key, return_obj=True)
|
||||||
|
if regex_obj:
|
||||||
|
return regex_obj["exclusive"]
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _matches_user(self, event, member_list):
|
||||||
|
if (hasattr(event, "sender") and
|
||||||
|
self.is_interested_in_user(event.sender)):
|
||||||
|
return True
|
||||||
|
# also check m.room.member state key
|
||||||
|
if (hasattr(event, "type") and event.type == EventTypes.Member
|
||||||
|
and hasattr(event, "state_key")
|
||||||
|
and self.is_interested_in_user(event.state_key)):
|
||||||
|
return True
|
||||||
|
# check joined member events
|
||||||
|
for user_id in member_list:
|
||||||
|
if self.is_interested_in_user(user_id):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _matches_room_id(self, event):
|
||||||
|
if hasattr(event, "room_id"):
|
||||||
|
return self.is_interested_in_room(event.room_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _matches_aliases(self, event, alias_list):
|
||||||
|
for alias in alias_list:
|
||||||
|
if self.is_interested_in_alias(alias):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_interested(self, event, restrict_to=None, aliases_for_event=None,
|
||||||
|
member_list=None):
|
||||||
|
"""Check if this service is interested in this event.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event(Event): The event to check.
|
||||||
|
restrict_to(str): The namespace to restrict regex tests to.
|
||||||
|
aliases_for_event(list): A list of all the known room aliases for
|
||||||
|
this event.
|
||||||
|
member_list(list): A list of all joined user_ids in this room.
|
||||||
|
Returns:
|
||||||
|
bool: True if this service would like to know about this event.
|
||||||
|
"""
|
||||||
|
if aliases_for_event is None:
|
||||||
|
aliases_for_event = []
|
||||||
|
if member_list is None:
|
||||||
|
member_list = []
|
||||||
|
|
||||||
|
if restrict_to and restrict_to not in ApplicationService.NS_LIST:
|
||||||
|
# this is a programming error, so fail early and raise a general
|
||||||
|
# exception
|
||||||
|
raise Exception("Unexpected restrict_to value: %s". restrict_to)
|
||||||
|
|
||||||
|
if not restrict_to:
|
||||||
|
return (self._matches_user(event, member_list)
|
||||||
|
or self._matches_aliases(event, aliases_for_event)
|
||||||
|
or self._matches_room_id(event))
|
||||||
|
elif restrict_to == ApplicationService.NS_ALIASES:
|
||||||
|
return self._matches_aliases(event, aliases_for_event)
|
||||||
|
elif restrict_to == ApplicationService.NS_ROOMS:
|
||||||
|
return self._matches_room_id(event)
|
||||||
|
elif restrict_to == ApplicationService.NS_USERS:
|
||||||
|
return self._matches_user(event, member_list)
|
||||||
|
|
||||||
|
def is_interested_in_user(self, user_id):
|
||||||
|
return (
|
||||||
|
self._matches_regex(user_id, ApplicationService.NS_USERS)
|
||||||
|
or user_id == self.sender
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_interested_in_alias(self, alias):
|
||||||
|
return self._matches_regex(alias, ApplicationService.NS_ALIASES)
|
||||||
|
|
||||||
|
def is_interested_in_room(self, room_id):
|
||||||
|
return self._matches_regex(room_id, ApplicationService.NS_ROOMS)
|
||||||
|
|
||||||
|
def is_exclusive_user(self, user_id):
|
||||||
|
return (
|
||||||
|
self._is_exclusive(ApplicationService.NS_USERS, user_id)
|
||||||
|
or user_id == self.sender
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_exclusive_alias(self, alias):
|
||||||
|
return self._is_exclusive(ApplicationService.NS_ALIASES, alias)
|
||||||
|
|
||||||
|
def is_exclusive_room(self, room_id):
|
||||||
|
return self._is_exclusive(ApplicationService.NS_ROOMS, room_id)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "ApplicationService: %s" % (self.__dict__,)
|
||||||
112
synapse/appservice/api.py
Normal file
112
synapse/appservice/api.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from synapse.api.errors import CodeMessageException
|
||||||
|
from synapse.http.client import SimpleHttpClient
|
||||||
|
from synapse.events.utils import serialize_event
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationServiceApi(SimpleHttpClient):
|
||||||
|
"""This class manages HS -> AS communications, including querying and
|
||||||
|
pushing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hs):
|
||||||
|
super(ApplicationServiceApi, self).__init__(hs)
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def query_user(self, service, user_id):
|
||||||
|
uri = service.url + ("/users/%s" % urllib.quote(user_id))
|
||||||
|
response = None
|
||||||
|
try:
|
||||||
|
response = yield self.get_json(uri, {
|
||||||
|
"access_token": service.hs_token
|
||||||
|
})
|
||||||
|
if response is not None: # just an empty json object
|
||||||
|
defer.returnValue(True)
|
||||||
|
except CodeMessageException as e:
|
||||||
|
if e.code == 404:
|
||||||
|
defer.returnValue(False)
|
||||||
|
return
|
||||||
|
logger.warning("query_user to %s received %s", uri, e.code)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning("query_user to %s threw exception %s", uri, ex)
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def query_alias(self, service, alias):
|
||||||
|
uri = service.url + ("/rooms/%s" % urllib.quote(alias))
|
||||||
|
response = None
|
||||||
|
try:
|
||||||
|
response = yield self.get_json(uri, {
|
||||||
|
"access_token": service.hs_token
|
||||||
|
})
|
||||||
|
if response is not None: # just an empty json object
|
||||||
|
defer.returnValue(True)
|
||||||
|
except CodeMessageException as e:
|
||||||
|
logger.warning("query_alias to %s received %s", uri, e.code)
|
||||||
|
if e.code == 404:
|
||||||
|
defer.returnValue(False)
|
||||||
|
return
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning("query_alias to %s threw exception %s", uri, ex)
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def push_bulk(self, service, events, txn_id=None):
|
||||||
|
events = self._serialize(events)
|
||||||
|
|
||||||
|
if txn_id is None:
|
||||||
|
logger.warning("push_bulk: Missing txn ID sending events to %s",
|
||||||
|
service.url)
|
||||||
|
txn_id = str(0)
|
||||||
|
txn_id = str(txn_id)
|
||||||
|
|
||||||
|
uri = service.url + ("/transactions/%s" %
|
||||||
|
urllib.quote(txn_id))
|
||||||
|
try:
|
||||||
|
yield self.put_json(
|
||||||
|
uri=uri,
|
||||||
|
json_body={
|
||||||
|
"events": events
|
||||||
|
},
|
||||||
|
args={
|
||||||
|
"access_token": service.hs_token
|
||||||
|
})
|
||||||
|
defer.returnValue(True)
|
||||||
|
return
|
||||||
|
except CodeMessageException as e:
|
||||||
|
logger.warning("push_bulk to %s received %s", uri, e.code)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.warning("push_bulk to %s threw exception %s", uri, ex)
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def push(self, service, event, txn_id=None):
|
||||||
|
response = yield self.push_bulk(service, [event], txn_id)
|
||||||
|
defer.returnValue(response)
|
||||||
|
|
||||||
|
def _serialize(self, events):
|
||||||
|
time_now = self.clock.time_msec()
|
||||||
|
return [
|
||||||
|
serialize_event(e, time_now, as_client_event=True) for e in events
|
||||||
|
]
|
||||||
254
synapse/appservice/scheduler.py
Normal file
254
synapse/appservice/scheduler.py
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
"""
|
||||||
|
This module controls the reliability for application service transactions.
|
||||||
|
|
||||||
|
The nominal flow through this module looks like:
|
||||||
|
__________
|
||||||
|
1---ASa[e]-->| Service |--> Queue ASa[f]
|
||||||
|
2----ASb[e]->| Queuer |
|
||||||
|
3--ASa[f]--->|__________|-----------+ ASa[e], ASb[e]
|
||||||
|
V
|
||||||
|
-````````- +------------+
|
||||||
|
|````````|<--StoreTxn-|Transaction |
|
||||||
|
|Database| | Controller |---> SEND TO AS
|
||||||
|
`--------` +------------+
|
||||||
|
What happens on SEND TO AS depends on the state of the Application Service:
|
||||||
|
- If the AS is marked as DOWN, do nothing.
|
||||||
|
- If the AS is marked as UP, send the transaction.
|
||||||
|
* SUCCESS : Increment where the AS is up to txn-wise and nuke the txn
|
||||||
|
contents from the db.
|
||||||
|
* FAILURE : Marked AS as DOWN and start Recoverer.
|
||||||
|
|
||||||
|
Recoverer attempts to recover ASes who have died. The flow for this looks like:
|
||||||
|
,--------------------- backoff++ --------------.
|
||||||
|
V |
|
||||||
|
START ---> Wait exp ------> Get oldest txn ID from ----> FAILURE
|
||||||
|
backoff DB and try to send it
|
||||||
|
^ |___________
|
||||||
|
Mark AS as | V
|
||||||
|
UP & quit +---------- YES SUCCESS
|
||||||
|
| | |
|
||||||
|
NO <--- Have more txns? <------ Mark txn success & nuke <-+
|
||||||
|
from db; incr AS pos.
|
||||||
|
Reset backoff.
|
||||||
|
|
||||||
|
This is all tied together by the AppServiceScheduler which DIs the required
|
||||||
|
components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from synapse.appservice import ApplicationServiceState
|
||||||
|
from twisted.internet import defer
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AppServiceScheduler(object):
|
||||||
|
""" Public facing API for this module. Does the required DI to tie the
|
||||||
|
components together. This also serves as the "event_pool", which in this
|
||||||
|
case is a simple array.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, clock, store, as_api):
|
||||||
|
self.clock = clock
|
||||||
|
self.store = store
|
||||||
|
self.as_api = as_api
|
||||||
|
|
||||||
|
def create_recoverer(service, callback):
|
||||||
|
return _Recoverer(clock, store, as_api, service, callback)
|
||||||
|
|
||||||
|
self.txn_ctrl = _TransactionController(
|
||||||
|
clock, store, as_api, create_recoverer
|
||||||
|
)
|
||||||
|
self.queuer = _ServiceQueuer(self.txn_ctrl)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def start(self):
|
||||||
|
logger.info("Starting appservice scheduler")
|
||||||
|
# check for any DOWN ASes and start recoverers for them.
|
||||||
|
recoverers = yield _Recoverer.start(
|
||||||
|
self.clock, self.store, self.as_api, self.txn_ctrl.on_recovered
|
||||||
|
)
|
||||||
|
self.txn_ctrl.add_recoverers(recoverers)
|
||||||
|
|
||||||
|
def submit_event_for_as(self, service, event):
|
||||||
|
self.queuer.enqueue(service, event)
|
||||||
|
|
||||||
|
|
||||||
|
class _ServiceQueuer(object):
|
||||||
|
"""Queues events for the same application service together, sending
|
||||||
|
transactions as soon as possible. Once a transaction is sent successfully,
|
||||||
|
this schedules any other events in the queue to run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, txn_ctrl):
|
||||||
|
self.queued_events = {} # dict of {service_id: [events]}
|
||||||
|
self.pending_requests = {} # dict of {service_id: Deferred}
|
||||||
|
self.txn_ctrl = txn_ctrl
|
||||||
|
|
||||||
|
def enqueue(self, service, event):
|
||||||
|
# if this service isn't being sent something
|
||||||
|
if not self.pending_requests.get(service.id):
|
||||||
|
self._send_request(service, [event])
|
||||||
|
else:
|
||||||
|
# add to queue for this service
|
||||||
|
if service.id not in self.queued_events:
|
||||||
|
self.queued_events[service.id] = []
|
||||||
|
self.queued_events[service.id].append(event)
|
||||||
|
|
||||||
|
def _send_request(self, service, events):
|
||||||
|
# send request and add callbacks
|
||||||
|
d = self.txn_ctrl.send(service, events)
|
||||||
|
d.addBoth(self._on_request_finish)
|
||||||
|
d.addErrback(self._on_request_fail)
|
||||||
|
self.pending_requests[service.id] = d
|
||||||
|
|
||||||
|
def _on_request_finish(self, service):
|
||||||
|
self.pending_requests[service.id] = None
|
||||||
|
# if there are queued events, then send them.
|
||||||
|
if (service.id in self.queued_events
|
||||||
|
and len(self.queued_events[service.id]) > 0):
|
||||||
|
self._send_request(service, self.queued_events[service.id])
|
||||||
|
self.queued_events[service.id] = []
|
||||||
|
|
||||||
|
def _on_request_fail(self, err):
|
||||||
|
logger.error("AS request failed: %s", err)
|
||||||
|
|
||||||
|
|
||||||
|
class _TransactionController(object):
|
||||||
|
|
||||||
|
def __init__(self, clock, store, as_api, recoverer_fn):
|
||||||
|
self.clock = clock
|
||||||
|
self.store = store
|
||||||
|
self.as_api = as_api
|
||||||
|
self.recoverer_fn = recoverer_fn
|
||||||
|
# keep track of how many recoverers there are
|
||||||
|
self.recoverers = []
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def send(self, service, events):
|
||||||
|
try:
|
||||||
|
txn = yield self.store.create_appservice_txn(
|
||||||
|
service=service,
|
||||||
|
events=events
|
||||||
|
)
|
||||||
|
service_is_up = yield self._is_service_up(service)
|
||||||
|
if service_is_up:
|
||||||
|
sent = yield txn.send(self.as_api)
|
||||||
|
if sent:
|
||||||
|
txn.complete(self.store)
|
||||||
|
else:
|
||||||
|
self._start_recoverer(service)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
self._start_recoverer(service)
|
||||||
|
# request has finished
|
||||||
|
defer.returnValue(service)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_recovered(self, recoverer):
|
||||||
|
self.recoverers.remove(recoverer)
|
||||||
|
logger.info("Successfully recovered application service AS ID %s",
|
||||||
|
recoverer.service.id)
|
||||||
|
logger.info("Remaining active recoverers: %s", len(self.recoverers))
|
||||||
|
yield self.store.set_appservice_state(
|
||||||
|
recoverer.service,
|
||||||
|
ApplicationServiceState.UP
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_recoverers(self, recoverers):
|
||||||
|
for r in recoverers:
|
||||||
|
self.recoverers.append(r)
|
||||||
|
if len(recoverers) > 0:
|
||||||
|
logger.info("New active recoverers: %s", len(self.recoverers))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _start_recoverer(self, service):
|
||||||
|
yield self.store.set_appservice_state(
|
||||||
|
service,
|
||||||
|
ApplicationServiceState.DOWN
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Application service falling behind. Starting recoverer. AS ID %s",
|
||||||
|
service.id
|
||||||
|
)
|
||||||
|
recoverer = self.recoverer_fn(service, self.on_recovered)
|
||||||
|
self.add_recoverers([recoverer])
|
||||||
|
recoverer.recover()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _is_service_up(self, service):
|
||||||
|
state = yield self.store.get_appservice_state(service)
|
||||||
|
defer.returnValue(state == ApplicationServiceState.UP or state is None)
|
||||||
|
|
||||||
|
|
||||||
|
class _Recoverer(object):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def start(clock, store, as_api, callback):
|
||||||
|
services = yield store.get_appservices_by_state(
|
||||||
|
ApplicationServiceState.DOWN
|
||||||
|
)
|
||||||
|
recoverers = [
|
||||||
|
_Recoverer(clock, store, as_api, s, callback) for s in services
|
||||||
|
]
|
||||||
|
for r in recoverers:
|
||||||
|
logger.info("Starting recoverer for AS ID %s which was marked as "
|
||||||
|
"DOWN", r.service.id)
|
||||||
|
r.recover()
|
||||||
|
defer.returnValue(recoverers)
|
||||||
|
|
||||||
|
def __init__(self, clock, store, as_api, service, callback):
|
||||||
|
self.clock = clock
|
||||||
|
self.store = store
|
||||||
|
self.as_api = as_api
|
||||||
|
self.service = service
|
||||||
|
self.callback = callback
|
||||||
|
self.backoff_counter = 1
|
||||||
|
|
||||||
|
def recover(self):
|
||||||
|
self.clock.call_later((2 ** self.backoff_counter), self.retry)
|
||||||
|
|
||||||
|
def _backoff(self):
|
||||||
|
# cap the backoff to be around 18h => (2^16) = 65536 secs
|
||||||
|
if self.backoff_counter < 16:
|
||||||
|
self.backoff_counter += 1
|
||||||
|
self.recover()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def retry(self):
|
||||||
|
try:
|
||||||
|
txn = yield self.store.get_oldest_unsent_txn(self.service)
|
||||||
|
if txn:
|
||||||
|
logger.info("Retrying transaction %s for AS ID %s",
|
||||||
|
txn.id, txn.service.id)
|
||||||
|
sent = yield txn.send(self.as_api)
|
||||||
|
if sent:
|
||||||
|
yield txn.complete(self.store)
|
||||||
|
# reset the backoff counter and retry immediately
|
||||||
|
self.backoff_counter = 1
|
||||||
|
yield self.retry()
|
||||||
|
else:
|
||||||
|
self._backoff()
|
||||||
|
else:
|
||||||
|
self._set_service_recovered()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
self._backoff()
|
||||||
|
|
||||||
|
def _set_service_recovered(self):
|
||||||
|
self.callback(self)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
|||||||
30
synapse/config/__main__.py
Normal file
30
synapse/config/__main__.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
from homeserver import HomeServerConfig
|
||||||
|
|
||||||
|
action = sys.argv[1]
|
||||||
|
|
||||||
|
if action == "read":
|
||||||
|
key = sys.argv[2]
|
||||||
|
config = HomeServerConfig.load_config("", sys.argv[3:])
|
||||||
|
|
||||||
|
print getattr(config, key)
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
sys.stderr.write("Unknown command %r\n" % (action,))
|
||||||
|
sys.exit(1)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,9 +14,10 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(Exception):
|
class ConfigError(Exception):
|
||||||
@@ -24,8 +25,35 @@ class ConfigError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class Config(object):
|
class Config(object):
|
||||||
def __init__(self, args):
|
|
||||||
pass
|
@staticmethod
|
||||||
|
def parse_size(value):
|
||||||
|
if isinstance(value, int) or isinstance(value, long):
|
||||||
|
return value
|
||||||
|
sizes = {"K": 1024, "M": 1024 * 1024}
|
||||||
|
size = 1
|
||||||
|
suffix = value[-1]
|
||||||
|
if suffix in sizes:
|
||||||
|
value = value[:-1]
|
||||||
|
size = sizes[suffix]
|
||||||
|
return int(value) * size
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_duration(value):
|
||||||
|
if isinstance(value, int) or isinstance(value, long):
|
||||||
|
return value
|
||||||
|
second = 1000
|
||||||
|
hour = 60 * 60 * second
|
||||||
|
day = 24 * hour
|
||||||
|
week = 7 * day
|
||||||
|
year = 365 * day
|
||||||
|
sizes = {"s": second, "h": hour, "d": day, "w": week, "y": year}
|
||||||
|
size = 1
|
||||||
|
suffix = value[-1]
|
||||||
|
if suffix in sizes:
|
||||||
|
value = value[:-1]
|
||||||
|
size = sizes[suffix]
|
||||||
|
return int(value) * size
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def abspath(file_path):
|
def abspath(file_path):
|
||||||
@@ -50,8 +78,9 @@ class Config(object):
|
|||||||
)
|
)
|
||||||
return cls.abspath(file_path)
|
return cls.abspath(file_path)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def ensure_directory(dir_path):
|
def ensure_directory(cls, dir_path):
|
||||||
|
dir_path = cls.abspath(dir_path)
|
||||||
if not os.path.exists(dir_path):
|
if not os.path.exists(dir_path):
|
||||||
os.makedirs(dir_path)
|
os.makedirs(dir_path)
|
||||||
if not os.path.isdir(dir_path):
|
if not os.path.isdir(dir_path):
|
||||||
@@ -75,83 +104,173 @@ class Config(object):
|
|||||||
with open(file_path) as file_stream:
|
with open(file_path) as file_stream:
|
||||||
return yaml.load(file_stream)
|
return yaml.load(file_stream)
|
||||||
|
|
||||||
@classmethod
|
def invoke_all(self, name, *args, **kargs):
|
||||||
def add_arguments(cls, parser):
|
results = []
|
||||||
pass
|
for cls in type(self).mro():
|
||||||
|
if name in cls.__dict__:
|
||||||
|
results.append(getattr(cls, name)(self, *args, **kargs))
|
||||||
|
return results
|
||||||
|
|
||||||
@classmethod
|
def generate_config(self, config_dir_path, server_name):
|
||||||
def generate_config(cls, args, config_dir_path):
|
default_config = "# vim:ft=yaml\n"
|
||||||
pass
|
|
||||||
|
default_config += "\n\n".join(dedent(conf) for conf in self.invoke_all(
|
||||||
|
"default_config", config_dir_path, server_name
|
||||||
|
))
|
||||||
|
|
||||||
|
config = yaml.load(default_config)
|
||||||
|
|
||||||
|
return default_config, config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_config(cls, description, argv, generate_section=None):
|
def load_config(cls, description, argv, generate_section=None):
|
||||||
|
obj = cls()
|
||||||
|
|
||||||
config_parser = argparse.ArgumentParser(add_help=False)
|
config_parser = argparse.ArgumentParser(add_help=False)
|
||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
"-c", "--config-path",
|
"-c", "--config-path",
|
||||||
|
action="append",
|
||||||
metavar="CONFIG_FILE",
|
metavar="CONFIG_FILE",
|
||||||
help="Specify config file"
|
help="Specify config file. Can be given multiple times and"
|
||||||
|
" may specify directories containing *.yaml files."
|
||||||
)
|
)
|
||||||
config_parser.add_argument(
|
config_parser.add_argument(
|
||||||
"--generate-config",
|
"--generate-config",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Generate config file"
|
help="Generate a config file for the server name"
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"--generate-keys",
|
||||||
|
action="store_true",
|
||||||
|
help="Generate any missing key files then exit"
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"--keys-directory",
|
||||||
|
metavar="DIRECTORY",
|
||||||
|
help="Used with 'generate-*' options to specify where files such as"
|
||||||
|
" certs and signing keys should be stored in, unless explicitly"
|
||||||
|
" specified in the config."
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"-H", "--server-name",
|
||||||
|
help="The server name to generate a config file for"
|
||||||
)
|
)
|
||||||
config_args, remaining_args = config_parser.parse_known_args(argv)
|
config_args, remaining_args = config_parser.parse_known_args(argv)
|
||||||
|
|
||||||
|
generate_keys = config_args.generate_keys
|
||||||
|
|
||||||
|
config_files = []
|
||||||
|
if config_args.config_path:
|
||||||
|
for config_path in config_args.config_path:
|
||||||
|
if os.path.isdir(config_path):
|
||||||
|
# We accept specifying directories as config paths, we search
|
||||||
|
# inside that directory for all files matching *.yaml, and then
|
||||||
|
# we apply them in *sorted* order.
|
||||||
|
files = []
|
||||||
|
for entry in os.listdir(config_path):
|
||||||
|
entry_path = os.path.join(config_path, entry)
|
||||||
|
if not os.path.isfile(entry_path):
|
||||||
|
print (
|
||||||
|
"Found subdirectory in config directory: %r. IGNORING."
|
||||||
|
) % (entry_path, )
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not entry.endswith(".yaml"):
|
||||||
|
print (
|
||||||
|
"Found file in config directory that does not"
|
||||||
|
" end in '.yaml': %r. IGNORING."
|
||||||
|
) % (entry_path, )
|
||||||
|
continue
|
||||||
|
|
||||||
|
files.append(entry_path)
|
||||||
|
|
||||||
|
config_files.extend(sorted(files))
|
||||||
|
else:
|
||||||
|
config_files.append(config_path)
|
||||||
|
|
||||||
if config_args.generate_config:
|
if config_args.generate_config:
|
||||||
if not config_args.config_path:
|
if not config_files:
|
||||||
config_parser.error(
|
config_parser.error(
|
||||||
"Must specify where to generate the config file"
|
"Must supply a config file.\nA config file can be automatically"
|
||||||
|
" generated using \"--generate-config -H SERVER_NAME"
|
||||||
|
" -c CONFIG-FILE\""
|
||||||
)
|
)
|
||||||
config_dir_path = os.path.dirname(config_args.config_path)
|
(config_path,) = config_files
|
||||||
if os.path.exists(config_args.config_path):
|
if not os.path.exists(config_path):
|
||||||
defaults = cls.read_config_file(config_args.config_path)
|
if config_args.keys_directory:
|
||||||
|
config_dir_path = config_args.keys_directory
|
||||||
|
else:
|
||||||
|
config_dir_path = os.path.dirname(config_path)
|
||||||
|
config_dir_path = os.path.abspath(config_dir_path)
|
||||||
|
|
||||||
|
server_name = config_args.server_name
|
||||||
|
if not server_name:
|
||||||
|
print "Must specify a server_name to a generate config for."
|
||||||
|
sys.exit(1)
|
||||||
|
if not os.path.exists(config_dir_path):
|
||||||
|
os.makedirs(config_dir_path)
|
||||||
|
with open(config_path, "wb") as config_file:
|
||||||
|
config_bytes, config = obj.generate_config(
|
||||||
|
config_dir_path, server_name
|
||||||
|
)
|
||||||
|
obj.invoke_all("generate_files", config)
|
||||||
|
config_file.write(config_bytes)
|
||||||
|
print (
|
||||||
|
"A config file has been generated in %r for server name"
|
||||||
|
" %r with corresponding SSL keys and self-signed"
|
||||||
|
" certificates. Please review this file and customise it"
|
||||||
|
" to your needs."
|
||||||
|
) % (config_path, server_name)
|
||||||
|
print (
|
||||||
|
"If this server name is incorrect, you will need to"
|
||||||
|
" regenerate the SSL certificates"
|
||||||
|
)
|
||||||
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
defaults = {}
|
print (
|
||||||
else:
|
"Config file %r already exists. Generating any missing key"
|
||||||
if config_args.config_path:
|
" files."
|
||||||
defaults = cls.read_config_file(config_args.config_path)
|
) % (config_path,)
|
||||||
else:
|
generate_keys = True
|
||||||
defaults = {}
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
parents=[config_parser],
|
parents=[config_parser],
|
||||||
description=description,
|
description=description,
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
)
|
)
|
||||||
cls.add_arguments(parser)
|
|
||||||
parser.set_defaults(**defaults)
|
|
||||||
|
|
||||||
|
obj.invoke_all("add_arguments", parser)
|
||||||
args = parser.parse_args(remaining_args)
|
args = parser.parse_args(remaining_args)
|
||||||
|
|
||||||
if config_args.generate_config:
|
if not config_files:
|
||||||
config_dir_path = os.path.dirname(config_args.config_path)
|
config_parser.error(
|
||||||
config_dir_path = os.path.abspath(config_dir_path)
|
"Must supply a config file.\nA config file can be automatically"
|
||||||
if not os.path.exists(config_dir_path):
|
" generated using \"--generate-config -H SERVER_NAME"
|
||||||
os.makedirs(config_dir_path)
|
" -c CONFIG-FILE\""
|
||||||
cls.generate_config(args, config_dir_path)
|
|
||||||
config = {}
|
|
||||||
for key, value in vars(args).items():
|
|
||||||
if (key not in set(["config_path", "generate_config"])
|
|
||||||
and value is not None):
|
|
||||||
config[key] = value
|
|
||||||
with open(config_args.config_path, "w") as config_file:
|
|
||||||
# TODO(paul) it would be lovely if we wrote out vim- and emacs-
|
|
||||||
# style mode markers into the file, to hint to people that
|
|
||||||
# this is a YAML file.
|
|
||||||
yaml.dump(config, config_file, default_flow_style=False)
|
|
||||||
print (
|
|
||||||
"A config file has been generated in %s for server name"
|
|
||||||
" '%s' with corresponding SSL keys and self-signed"
|
|
||||||
" certificates. Please review this file and customise it to"
|
|
||||||
" your needs."
|
|
||||||
) % (
|
|
||||||
config_args.config_path, config['server_name']
|
|
||||||
)
|
|
||||||
print (
|
|
||||||
"If this server name is incorrect, you will need to regenerate"
|
|
||||||
" the SSL certificates"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if config_args.keys_directory:
|
||||||
|
config_dir_path = config_args.keys_directory
|
||||||
|
else:
|
||||||
|
config_dir_path = os.path.dirname(config_args.config_path[-1])
|
||||||
|
config_dir_path = os.path.abspath(config_dir_path)
|
||||||
|
|
||||||
|
specified_config = {}
|
||||||
|
for config_file in config_files:
|
||||||
|
yaml_config = cls.read_config_file(config_file)
|
||||||
|
specified_config.update(yaml_config)
|
||||||
|
|
||||||
|
server_name = specified_config["server_name"]
|
||||||
|
_, config = obj.generate_config(config_dir_path, server_name)
|
||||||
|
config.pop("log_config")
|
||||||
|
config.update(specified_config)
|
||||||
|
|
||||||
|
if generate_keys:
|
||||||
|
obj.invoke_all("generate_files", config)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
return cls(args)
|
obj.invoke_all("read_config", config)
|
||||||
|
|
||||||
|
obj.invoke_all("read_arguments", args)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|||||||
27
synapse/config/appservice.py
Normal file
27
synapse/config/appservice.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class AppServiceConfig(Config):
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
self.app_service_config_files = config.get("app_service_config_files", [])
|
||||||
|
|
||||||
|
def default_config(cls, config_dir_path, server_name):
|
||||||
|
return """\
|
||||||
|
# A list of application service config file to use
|
||||||
|
app_service_config_files: []
|
||||||
|
"""
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,35 +17,31 @@ from ._base import Config
|
|||||||
|
|
||||||
class CaptchaConfig(Config):
|
class CaptchaConfig(Config):
|
||||||
|
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(CaptchaConfig, self).__init__(args)
|
self.recaptcha_private_key = config["recaptcha_private_key"]
|
||||||
self.recaptcha_private_key = args.recaptcha_private_key
|
self.recaptcha_public_key = config["recaptcha_public_key"]
|
||||||
self.enable_registration_captcha = args.enable_registration_captcha
|
self.enable_registration_captcha = config["enable_registration_captcha"]
|
||||||
self.captcha_ip_origin_is_x_forwarded = (
|
self.captcha_bypass_secret = config.get("captcha_bypass_secret")
|
||||||
args.captcha_ip_origin_is_x_forwarded
|
self.recaptcha_siteverify_api = config["recaptcha_siteverify_api"]
|
||||||
)
|
|
||||||
self.captcha_bypass_secret = args.captcha_bypass_secret
|
|
||||||
|
|
||||||
@classmethod
|
def default_config(self, config_dir_path, server_name):
|
||||||
def add_arguments(cls, parser):
|
return """\
|
||||||
super(CaptchaConfig, cls).add_arguments(parser)
|
## Captcha ##
|
||||||
group = parser.add_argument_group("recaptcha")
|
|
||||||
group.add_argument(
|
# This Home Server's ReCAPTCHA public key.
|
||||||
"--recaptcha-private-key", type=str, default="YOUR_PRIVATE_KEY",
|
recaptcha_private_key: "YOUR_PRIVATE_KEY"
|
||||||
help="The matching private key for the web client's public key."
|
|
||||||
)
|
# This Home Server's ReCAPTCHA private key.
|
||||||
group.add_argument(
|
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
||||||
"--enable-registration-captcha", type=bool, default=False,
|
|
||||||
help="Enables ReCaptcha checks when registering, preventing signup"
|
# Enables ReCaptcha checks when registering, preventing signup
|
||||||
+ " unless a captcha is answered. Requires a valid ReCaptcha "
|
# unless a captcha is answered. Requires a valid ReCaptcha
|
||||||
+ "public/private key."
|
# public/private key.
|
||||||
)
|
enable_registration_captcha: False
|
||||||
group.add_argument(
|
|
||||||
"--captcha_ip_origin_is_x_forwarded", type=bool, default=False,
|
# A secret key used to bypass the captcha test entirely.
|
||||||
help="When checking captchas, use the X-Forwarded-For (XFF) header"
|
#captcha_bypass_secret: "YOUR_SECRET_HERE"
|
||||||
+ " as the client IP and not the actual client IP."
|
|
||||||
)
|
# The API endpoint to use for verifying m.login.recaptcha responses.
|
||||||
group.add_argument(
|
recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify"
|
||||||
"--captcha_bypass_secret", type=str,
|
"""
|
||||||
help="A secret key used to bypass the captcha test entirely."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,24 +14,66 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config
|
from ._base import Config
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig(Config):
|
class DatabaseConfig(Config):
|
||||||
def __init__(self, args):
|
|
||||||
super(DatabaseConfig, self).__init__(args)
|
|
||||||
self.database_path = self.abspath(args.database_path)
|
|
||||||
|
|
||||||
@classmethod
|
def read_config(self, config):
|
||||||
def add_arguments(cls, parser):
|
self.event_cache_size = self.parse_size(
|
||||||
super(DatabaseConfig, cls).add_arguments(parser)
|
config.get("event_cache_size", "10K")
|
||||||
db_group = parser.add_argument_group("database")
|
|
||||||
db_group.add_argument(
|
|
||||||
"-d", "--database-path", default="homeserver.db",
|
|
||||||
help="The database name."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
self.database_config = config.get("database")
|
||||||
def generate_config(cls, args, config_dir_path):
|
|
||||||
super(DatabaseConfig, cls).generate_config(args, config_dir_path)
|
if self.database_config is None:
|
||||||
args.database_path = os.path.abspath(args.database_path)
|
self.database_config = {
|
||||||
|
"name": "sqlite3",
|
||||||
|
"args": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
name = self.database_config.get("name", None)
|
||||||
|
if name == "psycopg2":
|
||||||
|
pass
|
||||||
|
elif name == "sqlite3":
|
||||||
|
self.database_config.setdefault("args", {}).update({
|
||||||
|
"cp_min": 1,
|
||||||
|
"cp_max": 1,
|
||||||
|
"check_same_thread": False,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Unsupported database type '%s'" % (name,))
|
||||||
|
|
||||||
|
self.set_databasepath(config.get("database_path"))
|
||||||
|
|
||||||
|
def default_config(self, config, config_dir_path):
|
||||||
|
database_path = self.abspath("homeserver.db")
|
||||||
|
return """\
|
||||||
|
# Database configuration
|
||||||
|
database:
|
||||||
|
# The database engine name
|
||||||
|
name: "sqlite3"
|
||||||
|
# Arguments to pass to the engine
|
||||||
|
args:
|
||||||
|
# Path to the database
|
||||||
|
database: "%(database_path)s"
|
||||||
|
|
||||||
|
# Number of events to cache in memory.
|
||||||
|
event_cache_size: "10K"
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def read_arguments(self, args):
|
||||||
|
self.set_databasepath(args.database_path)
|
||||||
|
|
||||||
|
def set_databasepath(self, database_path):
|
||||||
|
if database_path != ":memory:":
|
||||||
|
database_path = self.abspath(database_path)
|
||||||
|
if self.database_config.get("name", None) == "sqlite3":
|
||||||
|
if database_path is not None:
|
||||||
|
self.database_config["args"]["database"] = database_path
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
db_group = parser.add_argument_group("database")
|
||||||
|
db_group.add_argument(
|
||||||
|
"-d", "--database-path", metavar="SQLITE_DATABASE_PATH",
|
||||||
|
help="The path to a sqlite database to use."
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 OpenMarket Ltd
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ._base import Config
|
|
||||||
|
|
||||||
|
|
||||||
class EmailConfig(Config):
|
|
||||||
|
|
||||||
def __init__(self, args):
|
|
||||||
super(EmailConfig, self).__init__(args)
|
|
||||||
self.email_from_address = args.email_from_address
|
|
||||||
self.email_smtp_server = args.email_smtp_server
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_arguments(cls, parser):
|
|
||||||
super(EmailConfig, cls).add_arguments(parser)
|
|
||||||
email_group = parser.add_argument_group("email")
|
|
||||||
email_group.add_argument(
|
|
||||||
"--email-from-address",
|
|
||||||
default="FROM@EXAMPLE.COM",
|
|
||||||
help="The address to send emails from (e.g. for password resets)."
|
|
||||||
)
|
|
||||||
email_group.add_argument(
|
|
||||||
"--email-smtp-server",
|
|
||||||
default="",
|
|
||||||
help=(
|
|
||||||
"The SMTP server to send emails from (e.g. for password"
|
|
||||||
" resets)."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -20,16 +20,23 @@ from .database import DatabaseConfig
|
|||||||
from .ratelimiting import RatelimitConfig
|
from .ratelimiting import RatelimitConfig
|
||||||
from .repository import ContentRepositoryConfig
|
from .repository import ContentRepositoryConfig
|
||||||
from .captcha import CaptchaConfig
|
from .captcha import CaptchaConfig
|
||||||
from .email import EmailConfig
|
|
||||||
from .voip import VoipConfig
|
from .voip import VoipConfig
|
||||||
|
from .registration import RegistrationConfig
|
||||||
|
from .metrics import MetricsConfig
|
||||||
|
from .appservice import AppServiceConfig
|
||||||
|
from .key import KeyConfig
|
||||||
|
from .saml2 import SAML2Config
|
||||||
|
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||||
EmailConfig, VoipConfig):
|
VoipConfig, RegistrationConfig, MetricsConfig,
|
||||||
|
AppServiceConfig, KeyConfig, SAML2Config, ):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
|
sys.stdout.write(
|
||||||
|
HomeServerConfig().generate_config(sys.argv[1], sys.argv[2])[0]
|
||||||
|
)
|
||||||
|
|||||||
130
synapse/config/key.py
Normal file
130
synapse/config/key.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config, ConfigError
|
||||||
|
|
||||||
|
from synapse.util.stringutils import random_string
|
||||||
|
from signedjson.key import (
|
||||||
|
generate_signing_key, is_signing_algorithm_supported,
|
||||||
|
decode_signing_key_base64, decode_verify_key_bytes,
|
||||||
|
read_signing_keys, write_signing_keys, NACL_ED25519
|
||||||
|
)
|
||||||
|
from unpaddedbase64 import decode_base64
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class KeyConfig(Config):
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
self.signing_key = self.read_signing_key(config["signing_key_path"])
|
||||||
|
self.old_signing_keys = self.read_old_signing_keys(
|
||||||
|
config["old_signing_keys"]
|
||||||
|
)
|
||||||
|
self.key_refresh_interval = self.parse_duration(
|
||||||
|
config["key_refresh_interval"]
|
||||||
|
)
|
||||||
|
self.perspectives = self.read_perspectives(
|
||||||
|
config["perspectives"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name):
|
||||||
|
base_key_name = os.path.join(config_dir_path, server_name)
|
||||||
|
return """\
|
||||||
|
## Signing Keys ##
|
||||||
|
|
||||||
|
# Path to the signing key to sign messages with
|
||||||
|
signing_key_path: "%(base_key_name)s.signing.key"
|
||||||
|
|
||||||
|
# The keys that the server used to sign messages with but won't use
|
||||||
|
# to sign new messages. E.g. it has lost its private key
|
||||||
|
old_signing_keys: {}
|
||||||
|
# "ed25519:auto":
|
||||||
|
# # Base64 encoded public key
|
||||||
|
# key: "The public part of your old signing key."
|
||||||
|
# # Millisecond POSIX timestamp when the key expired.
|
||||||
|
# expired_ts: 123456789123
|
||||||
|
|
||||||
|
# How long key response published by this server is valid for.
|
||||||
|
# Used to set the valid_until_ts in /key/v2 APIs.
|
||||||
|
# Determines how quickly servers will query to check which keys
|
||||||
|
# are still valid.
|
||||||
|
key_refresh_interval: "1d" # 1 Day.
|
||||||
|
|
||||||
|
# The trusted servers to download signing keys from.
|
||||||
|
perspectives:
|
||||||
|
servers:
|
||||||
|
"matrix.org":
|
||||||
|
verify_keys:
|
||||||
|
"ed25519:auto":
|
||||||
|
key: "Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw"
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def read_perspectives(self, perspectives_config):
|
||||||
|
servers = {}
|
||||||
|
for server_name, server_config in perspectives_config["servers"].items():
|
||||||
|
for key_id, key_data in server_config["verify_keys"].items():
|
||||||
|
if is_signing_algorithm_supported(key_id):
|
||||||
|
key_base64 = key_data["key"]
|
||||||
|
key_bytes = decode_base64(key_base64)
|
||||||
|
verify_key = decode_verify_key_bytes(key_id, key_bytes)
|
||||||
|
servers.setdefault(server_name, {})[key_id] = verify_key
|
||||||
|
return servers
|
||||||
|
|
||||||
|
def read_signing_key(self, signing_key_path):
|
||||||
|
signing_keys = self.read_file(signing_key_path, "signing_key")
|
||||||
|
try:
|
||||||
|
return read_signing_keys(signing_keys.splitlines(True))
|
||||||
|
except Exception:
|
||||||
|
raise ConfigError(
|
||||||
|
"Error reading signing_key."
|
||||||
|
" Try running again with --generate-config"
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_old_signing_keys(self, old_signing_keys):
|
||||||
|
keys = {}
|
||||||
|
for key_id, key_data in old_signing_keys.items():
|
||||||
|
if is_signing_algorithm_supported(key_id):
|
||||||
|
key_base64 = key_data["key"]
|
||||||
|
key_bytes = decode_base64(key_base64)
|
||||||
|
verify_key = decode_verify_key_bytes(key_id, key_bytes)
|
||||||
|
verify_key.expired_ts = key_data["expired_ts"]
|
||||||
|
keys[key_id] = verify_key
|
||||||
|
else:
|
||||||
|
raise ConfigError(
|
||||||
|
"Unsupported signing algorithm for old key: %r" % (key_id,)
|
||||||
|
)
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def generate_files(self, config):
|
||||||
|
signing_key_path = config["signing_key_path"]
|
||||||
|
if not os.path.exists(signing_key_path):
|
||||||
|
with open(signing_key_path, "w") as signing_key_file:
|
||||||
|
key_id = "a_" + random_string(4)
|
||||||
|
write_signing_keys(
|
||||||
|
signing_key_file, (generate_signing_key(key_id),),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
signing_keys = self.read_file(signing_key_path, "signing_key")
|
||||||
|
if len(signing_keys.split("\n")[0].split()) == 1:
|
||||||
|
# handle keys in the old format.
|
||||||
|
key_id = "a_" + random_string(4)
|
||||||
|
key = decode_signing_key_base64(
|
||||||
|
NACL_ED25519, key_id, signing_keys.split("\n")[0]
|
||||||
|
)
|
||||||
|
with open(signing_key_path, "w") as signing_key_file:
|
||||||
|
write_signing_keys(
|
||||||
|
signing_key_file, (key,),
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -18,25 +18,89 @@ from synapse.util.logcontext import LoggingContextFilter
|
|||||||
from twisted.python.log import PythonLoggingObserver
|
from twisted.python.log import PythonLoggingObserver
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
|
import yaml
|
||||||
|
from string import Template
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_LOG_CONFIG = Template("""
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
precise:
|
||||||
|
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s\
|
||||||
|
- %(message)s'
|
||||||
|
|
||||||
|
filters:
|
||||||
|
context:
|
||||||
|
(): synapse.util.logcontext.LoggingContextFilter
|
||||||
|
request: ""
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
file:
|
||||||
|
class: logging.handlers.RotatingFileHandler
|
||||||
|
formatter: precise
|
||||||
|
filename: ${log_file}
|
||||||
|
maxBytes: 104857600
|
||||||
|
backupCount: 10
|
||||||
|
filters: [context]
|
||||||
|
level: INFO
|
||||||
|
console:
|
||||||
|
class: logging.StreamHandler
|
||||||
|
formatter: precise
|
||||||
|
|
||||||
|
loggers:
|
||||||
|
synapse:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
synapse.storage.SQL:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
root:
|
||||||
|
level: INFO
|
||||||
|
handlers: [file, console]
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
class LoggingConfig(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 read_config(self, config):
|
||||||
|
self.verbosity = config.get("verbose", 0)
|
||||||
|
self.log_config = self.abspath(config.get("log_config"))
|
||||||
|
self.log_file = self.abspath(config.get("log_file"))
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name):
|
||||||
|
log_file = self.abspath("homeserver.log")
|
||||||
|
log_config = self.abspath(
|
||||||
|
os.path.join(config_dir_path, server_name + ".log.config")
|
||||||
|
)
|
||||||
|
return """
|
||||||
|
# Logging verbosity level.
|
||||||
|
verbose: 0
|
||||||
|
|
||||||
|
# File to write logging to
|
||||||
|
log_file: "%(log_file)s"
|
||||||
|
|
||||||
|
# A yaml python logging config file
|
||||||
|
log_config: "%(log_config)s"
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def read_arguments(self, args):
|
||||||
|
if args.verbose is not None:
|
||||||
|
self.verbosity = args.verbose
|
||||||
|
if args.log_config is not None:
|
||||||
|
self.log_config = args.log_config
|
||||||
|
if args.log_file is not None:
|
||||||
|
self.log_file = args.log_file
|
||||||
|
|
||||||
def add_arguments(cls, parser):
|
def add_arguments(cls, parser):
|
||||||
super(LoggingConfig, cls).add_arguments(parser)
|
|
||||||
logging_group = parser.add_argument_group("logging")
|
logging_group = parser.add_argument_group("logging")
|
||||||
logging_group.add_argument(
|
logging_group.add_argument(
|
||||||
'-v', '--verbose', dest="verbose", action='count',
|
'-v', '--verbose', dest="verbose", action='count',
|
||||||
help="The verbosity level."
|
help="The verbosity level."
|
||||||
)
|
)
|
||||||
logging_group.add_argument(
|
logging_group.add_argument(
|
||||||
'-f', '--log-file', dest="log_file", default="homeserver.log",
|
'-f', '--log-file', dest="log_file",
|
||||||
help="File to log to."
|
help="File to log to."
|
||||||
)
|
)
|
||||||
logging_group.add_argument(
|
logging_group.add_argument(
|
||||||
@@ -44,6 +108,14 @@ class LoggingConfig(Config):
|
|||||||
help="Python logging config file"
|
help="Python logging config file"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def generate_files(self, config):
|
||||||
|
log_config = config.get("log_config")
|
||||||
|
if log_config and not os.path.exists(log_config):
|
||||||
|
with open(log_config, "wb") as log_config_file:
|
||||||
|
log_config_file.write(
|
||||||
|
DEFAULT_LOG_CONFIG.substitute(log_file=config["log_file"])
|
||||||
|
)
|
||||||
|
|
||||||
def setup_logging(self):
|
def setup_logging(self):
|
||||||
log_format = (
|
log_format = (
|
||||||
"%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s"
|
"%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s"
|
||||||
@@ -66,7 +138,10 @@ class LoggingConfig(Config):
|
|||||||
|
|
||||||
formatter = logging.Formatter(log_format)
|
formatter = logging.Formatter(log_format)
|
||||||
if self.log_file:
|
if self.log_file:
|
||||||
handler = logging.FileHandler(self.log_file)
|
# TODO: Customisable file size / backup count
|
||||||
|
handler = logging.handlers.RotatingFileHandler(
|
||||||
|
self.log_file, maxBytes=(1000 * 1000 * 100), backupCount=3
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
handler.setFormatter(formatter)
|
handler.setFormatter(formatter)
|
||||||
@@ -74,9 +149,9 @@ class LoggingConfig(Config):
|
|||||||
handler.addFilter(LoggingContextFilter(request=""))
|
handler.addFilter(LoggingContextFilter(request=""))
|
||||||
|
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
logger.info("Test")
|
|
||||||
else:
|
else:
|
||||||
logging.config.fileConfig(self.log_config)
|
with open(self.log_config, 'r') as f:
|
||||||
|
logging.config.dictConfig(yaml.load(f))
|
||||||
|
|
||||||
observer = PythonLoggingObserver()
|
observer = PythonLoggingObserver()
|
||||||
observer.start()
|
observer.start()
|
||||||
|
|||||||
31
synapse/config/metrics.py
Normal file
31
synapse/config/metrics.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsConfig(Config):
|
||||||
|
def read_config(self, config):
|
||||||
|
self.enable_metrics = config["enable_metrics"]
|
||||||
|
self.metrics_port = config.get("metrics_port")
|
||||||
|
self.metrics_bind_host = config.get("metrics_bind_host", "127.0.0.1")
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name):
|
||||||
|
return """\
|
||||||
|
## Metrics ###
|
||||||
|
|
||||||
|
# Enable collection and rendering of performance metrics
|
||||||
|
enable_metrics: False
|
||||||
|
"""
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2014 OpenMarket Ltd
|
# Copyright 2014, 2015 OpenMarket Ltd
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -17,20 +17,42 @@ from ._base import Config
|
|||||||
|
|
||||||
class RatelimitConfig(Config):
|
class RatelimitConfig(Config):
|
||||||
|
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(RatelimitConfig, self).__init__(args)
|
self.rc_messages_per_second = config["rc_messages_per_second"]
|
||||||
self.rc_messages_per_second = args.rc_messages_per_second
|
self.rc_message_burst_count = config["rc_message_burst_count"]
|
||||||
self.rc_message_burst_count = args.rc_message_burst_count
|
|
||||||
|
|
||||||
@classmethod
|
self.federation_rc_window_size = config["federation_rc_window_size"]
|
||||||
def add_arguments(cls, parser):
|
self.federation_rc_sleep_limit = config["federation_rc_sleep_limit"]
|
||||||
super(RatelimitConfig, cls).add_arguments(parser)
|
self.federation_rc_sleep_delay = config["federation_rc_sleep_delay"]
|
||||||
rc_group = parser.add_argument_group("ratelimiting")
|
self.federation_rc_reject_limit = config["federation_rc_reject_limit"]
|
||||||
rc_group.add_argument(
|
self.federation_rc_concurrent = config["federation_rc_concurrent"]
|
||||||
"--rc-messages-per-second", type=float, default=0.2,
|
|
||||||
help="number of messages a client can send per second"
|
def default_config(self, config_dir_path, server_name):
|
||||||
)
|
return """\
|
||||||
rc_group.add_argument(
|
## Ratelimiting ##
|
||||||
"--rc-message-burst-count", type=float, default=10,
|
|
||||||
help="number of message a client can send before being throttled"
|
# Number of messages a client can send per second
|
||||||
)
|
rc_messages_per_second: 0.2
|
||||||
|
|
||||||
|
# Number of message a client can send before being throttled
|
||||||
|
rc_message_burst_count: 10.0
|
||||||
|
|
||||||
|
# The federation window size in milliseconds
|
||||||
|
federation_rc_window_size: 1000
|
||||||
|
|
||||||
|
# The number of federation requests from a single server in a window
|
||||||
|
# before the server will delay processing the request.
|
||||||
|
federation_rc_sleep_limit: 10
|
||||||
|
|
||||||
|
# The duration in milliseconds to delay processing events from
|
||||||
|
# remote servers by if they go over the sleep limit.
|
||||||
|
federation_rc_sleep_delay: 500
|
||||||
|
|
||||||
|
# The maximum number of concurrent federation requests allowed
|
||||||
|
# from a single server
|
||||||
|
federation_rc_reject_limit: 50
|
||||||
|
|
||||||
|
# The number of federation requests to concurrently process from a
|
||||||
|
# single server
|
||||||
|
federation_rc_concurrent: 3
|
||||||
|
"""
|
||||||
|
|||||||
64
synapse/config/registration.py
Normal file
64
synapse/config/registration.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
from synapse.util.stringutils import random_string_with_symbols
|
||||||
|
|
||||||
|
from distutils.util import strtobool
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationConfig(Config):
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
self.disable_registration = not bool(
|
||||||
|
strtobool(str(config["enable_registration"]))
|
||||||
|
)
|
||||||
|
if "disable_registration" in config:
|
||||||
|
self.disable_registration = bool(
|
||||||
|
strtobool(str(config["disable_registration"]))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.registration_shared_secret = config.get("registration_shared_secret")
|
||||||
|
self.macaroon_secret_key = config.get("macaroon_secret_key")
|
||||||
|
|
||||||
|
def default_config(self, config_dir, server_name):
|
||||||
|
registration_shared_secret = random_string_with_symbols(50)
|
||||||
|
macaroon_secret_key = random_string_with_symbols(50)
|
||||||
|
return """\
|
||||||
|
## Registration ##
|
||||||
|
|
||||||
|
# Enable registration for new users.
|
||||||
|
enable_registration: False
|
||||||
|
|
||||||
|
# If set, allows registration by anyone who also has the shared
|
||||||
|
# secret, even if registration is otherwise disabled.
|
||||||
|
registration_shared_secret: "%(registration_shared_secret)s"
|
||||||
|
|
||||||
|
macaroon_secret_key: "%(macaroon_secret_key)s"
|
||||||
|
""" % locals()
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
reg_group = parser.add_argument_group("registration")
|
||||||
|
reg_group.add_argument(
|
||||||
|
"--enable-registration", action="store_true", default=None,
|
||||||
|
help="Enable registration for new users."
|
||||||
|
)
|
||||||
|
|
||||||
|
def read_arguments(self, args):
|
||||||
|
if args.enable_registration is not None:
|
||||||
|
self.disable_registration = not bool(
|
||||||
|
strtobool(str(args.enable_registration))
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2014 matrix.org
|
# Copyright 2014, 2015 matrix.org
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -14,35 +14,87 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from ._base import Config
|
from ._base import Config
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
ThumbnailRequirement = namedtuple(
|
||||||
|
"ThumbnailRequirement", ["width", "height", "method", "media_type"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_thumbnail_requirements(thumbnail_sizes):
|
||||||
|
""" Takes a list of dictionaries with "width", "height", and "method" keys
|
||||||
|
and creates a map from image media types to the thumbnail size, thumnailing
|
||||||
|
method, and thumbnail media type to precalculate
|
||||||
|
|
||||||
|
Args:
|
||||||
|
thumbnail_sizes(list): List of dicts with "width", "height", and
|
||||||
|
"method" keys
|
||||||
|
Returns:
|
||||||
|
Dictionary mapping from media type string to list of
|
||||||
|
ThumbnailRequirement tuples.
|
||||||
|
"""
|
||||||
|
requirements = {}
|
||||||
|
for size in thumbnail_sizes:
|
||||||
|
width = size["width"]
|
||||||
|
height = size["height"]
|
||||||
|
method = size["method"]
|
||||||
|
jpeg_thumbnail = ThumbnailRequirement(width, height, method, "image/jpeg")
|
||||||
|
png_thumbnail = ThumbnailRequirement(width, height, method, "image/png")
|
||||||
|
requirements.setdefault("image/jpeg", []).append(jpeg_thumbnail)
|
||||||
|
requirements.setdefault("image/gif", []).append(png_thumbnail)
|
||||||
|
requirements.setdefault("image/png", []).append(png_thumbnail)
|
||||||
|
return {
|
||||||
|
media_type: tuple(thumbnails)
|
||||||
|
for media_type, thumbnails in requirements.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ContentRepositoryConfig(Config):
|
class ContentRepositoryConfig(Config):
|
||||||
def __init__(self, args):
|
def read_config(self, config):
|
||||||
super(ContentRepositoryConfig, self).__init__(args)
|
self.max_upload_size = self.parse_size(config["max_upload_size"])
|
||||||
self.max_upload_size = self.parse_size(args.max_upload_size)
|
self.max_image_pixels = self.parse_size(config["max_image_pixels"])
|
||||||
self.max_image_pixels = self.parse_size(args.max_image_pixels)
|
self.media_store_path = self.ensure_directory(config["media_store_path"])
|
||||||
self.media_store_path = self.ensure_directory(args.media_store_path)
|
self.uploads_path = self.ensure_directory(config["uploads_path"])
|
||||||
|
self.dynamic_thumbnails = config["dynamic_thumbnails"]
|
||||||
|
self.thumbnail_requirements = parse_thumbnail_requirements(
|
||||||
|
config["thumbnail_sizes"]
|
||||||
|
)
|
||||||
|
|
||||||
def parse_size(self, string):
|
def default_config(self, config_dir_path, server_name):
|
||||||
sizes = {"K": 1024, "M": 1024 * 1024}
|
media_store = self.default_path("media_store")
|
||||||
size = 1
|
uploads_path = self.default_path("uploads")
|
||||||
suffix = string[-1]
|
return """
|
||||||
if suffix in sizes:
|
# Directory where uploaded images and attachments are stored.
|
||||||
string = string[:-1]
|
media_store_path: "%(media_store)s"
|
||||||
size = sizes[suffix]
|
|
||||||
return int(string) * size
|
|
||||||
|
|
||||||
@classmethod
|
# Directory where in-progress uploads are stored.
|
||||||
def add_arguments(cls, parser):
|
uploads_path: "%(uploads_path)s"
|
||||||
super(ContentRepositoryConfig, cls).add_arguments(parser)
|
|
||||||
db_group = parser.add_argument_group("content_repository")
|
# The largest allowed upload size in bytes
|
||||||
db_group.add_argument(
|
max_upload_size: "10M"
|
||||||
"--max-upload-size", default="1M"
|
|
||||||
)
|
# Maximum number of pixels that will be thumbnailed
|
||||||
db_group.add_argument(
|
max_image_pixels: "32M"
|
||||||
"--media-store-path", default=cls.default_path("media_store")
|
|
||||||
)
|
# Whether to generate new thumbnails on the fly to precisely match
|
||||||
db_group.add_argument(
|
# the resolution requested by the client. If true then whenever
|
||||||
"--max-image-pixels", default="32M",
|
# a new resolution is requested by the client the server will
|
||||||
help="Maximum number of pixels that will be thumbnailed"
|
# generate a new thumbnail. If false the server will pick a thumbnail
|
||||||
)
|
# from a precalcualted list.
|
||||||
|
dynamic_thumbnails: false
|
||||||
|
|
||||||
|
# List of thumbnail to precalculate when an image is uploaded.
|
||||||
|
thumbnail_sizes:
|
||||||
|
- width: 32
|
||||||
|
height: 32
|
||||||
|
method: crop
|
||||||
|
- width: 96
|
||||||
|
height: 96
|
||||||
|
method: crop
|
||||||
|
- width: 320
|
||||||
|
height: 240
|
||||||
|
method: scale
|
||||||
|
- width: 640
|
||||||
|
height: 480
|
||||||
|
method: scale
|
||||||
|
""" % locals()
|
||||||
|
|||||||
54
synapse/config/saml2.py
Normal file
54
synapse/config/saml2.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2015 Ericsson
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class SAML2Config(Config):
|
||||||
|
"""SAML2 Configuration
|
||||||
|
Synapse uses pysaml2 libraries for providing SAML2 support
|
||||||
|
|
||||||
|
config_path: Path to the sp_conf.py configuration file
|
||||||
|
idp_redirect_url: Identity provider URL which will redirect
|
||||||
|
the user back to /login/saml2 with proper info.
|
||||||
|
|
||||||
|
sp_conf.py file is something like:
|
||||||
|
https://github.com/rohe/pysaml2/blob/master/example/sp-repoze/sp_conf.py.example
|
||||||
|
|
||||||
|
More information: https://pythonhosted.org/pysaml2/howto/config.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
def read_config(self, config):
|
||||||
|
saml2_config = config.get("saml2_config", None)
|
||||||
|
if saml2_config:
|
||||||
|
self.saml2_enabled = True
|
||||||
|
self.saml2_config_path = saml2_config["config_path"]
|
||||||
|
self.saml2_idp_redirect_url = saml2_config["idp_redirect_url"]
|
||||||
|
else:
|
||||||
|
self.saml2_enabled = False
|
||||||
|
self.saml2_config_path = None
|
||||||
|
self.saml2_idp_redirect_url = None
|
||||||
|
|
||||||
|
def default_config(self, config_dir_path, server_name):
|
||||||
|
return """
|
||||||
|
# Enable SAML2 for registration and login. Uses pysaml2
|
||||||
|
# config_path: Path to the sp_conf.py configuration file
|
||||||
|
# idp_redirect_url: Identity provider URL which will redirect
|
||||||
|
# the user back to /login/saml2 with proper info.
|
||||||
|
# See pysaml2 docs for format of config.
|
||||||
|
#saml2_config:
|
||||||
|
# config_path: "%s/sp_conf.py"
|
||||||
|
# idp_redirect_url: "http://%s/idp"
|
||||||
|
""" % (config_dir_path, server_name)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user