Compare commits
776 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
99ab234f40 | ||
![]() |
e83416bb5a | ||
![]() |
726b6f0fa6 | ||
![]() |
d7fd89df49 | ||
![]() |
f4e57fdb22 | ||
![]() |
4f096adcfa | ||
21a5fa3ef0 | |||
b27e9ea95c | |||
8aa915acb9 | |||
ace9637bc2 | |||
![]() |
be1e2e9307 | ||
![]() |
1c6a4b1b24 | ||
976a73a0e5 | |||
4c06f329c4 | |||
d841b81c56 | |||
e707084345 | |||
![]() |
6dcc8b6cf1 | ||
![]() |
a2ac491c54 | ||
![]() |
72a13d8353 | ||
![]() |
3a63f9dfb6 | ||
![]() |
f4f2d05b5b | ||
![]() |
c3c7bcb2ed | ||
![]() |
d6c57f9b2e | ||
![]() |
7fb9e99649 | ||
![]() |
1274b48ebb | ||
![]() |
0a281e81a5 | ||
![]() |
a43bde69fa | ||
![]() |
986343877c | ||
![]() |
2d47710b55 | ||
![]() |
10542a1d70 | ||
![]() |
c167f7a6ad | ||
![]() |
5787a70bab | ||
![]() |
cf8f1f2546 | ||
![]() |
3c2fc4a4c6 | ||
![]() |
dffd771e7c | ||
![]() |
4da8c7e282 | ||
![]() |
0df5d18fd6 | ||
![]() |
825ceac1c3 | ||
![]() |
3e389256f5 | ||
![]() |
a7892a28ec | ||
![]() |
9453dbc740 | ||
![]() |
bf48c10d28 | ||
![]() |
7c1a3e41d9 | ||
![]() |
2a04a361e0 | ||
![]() |
0e8e4f1083 | ||
![]() |
81ae579b25 | ||
![]() |
3a3cafe912 | ||
![]() |
d29591d47d | ||
![]() |
67d280dd2e | ||
![]() |
3ac9be5a78 | ||
![]() |
52954f7a11 | ||
![]() |
692a31620d | ||
![]() |
cf4015b830 | ||
![]() |
9cef03127b | ||
![]() |
249fc7769d | ||
![]() |
5cc53c9e14 | ||
![]() |
bdc46f6392 | ||
![]() |
6ae776218c | ||
![]() |
bd2b146d5d | ||
![]() |
f7cc4fb3bb | ||
![]() |
ca198c51fa | ||
![]() |
fe86d28428 | ||
![]() |
c86f9a5c5b | ||
![]() |
e0358a9de5 | ||
![]() |
69d0003222 | ||
![]() |
5cf9f3df48 | ||
![]() |
0b7ed5adc9 | ||
![]() |
4de54db305 | ||
![]() |
02781e4f9b | ||
![]() |
f8bdfd82b0 | ||
![]() |
7e66d2e2c0 | ||
![]() |
ffd03a256b | ||
![]() |
9d592d60d2 | ||
![]() |
25ceb5ebd8 | ||
![]() |
6f052fff98 | ||
![]() |
e8ac881b2f | ||
![]() |
0d17aedae5 | ||
![]() |
ab1fff2642 | ||
![]() |
92c5b6b86c | ||
![]() |
dc2f53e773 | ||
![]() |
2475995102 | ||
![]() |
835f4ad8cf | ||
![]() |
ca6219723b | ||
![]() |
40c7c248fb | ||
![]() |
8f3f5c01f9 | ||
![]() |
9d7f7b871b | ||
![]() |
30f0871e21 | ||
![]() |
98e81c6217 | ||
![]() |
f3b6b3e222 | ||
![]() |
3bfdae795d | ||
![]() |
75c80df271 | ||
![]() |
094cb888d4 | ||
![]() |
fa725a14e2 | ||
![]() |
9b3664aeeb | ||
![]() |
90fea00dc7 | ||
![]() |
20924a44f1 | ||
![]() |
38d6426b0e | ||
![]() |
9b55ce933a | ||
![]() |
f73a657a23 | ||
![]() |
6dfb262ddf | ||
![]() |
75cdc3a1f6 | ||
![]() |
11103a92ed | ||
![]() |
ce2017a10e | ||
![]() |
0c2cfda3ae | ||
![]() |
4bf8ee1f74 | ||
![]() |
5d16948030 | ||
![]() |
b7b2eb9d05 | ||
![]() |
19bfee1835 | ||
![]() |
9db87550fd | ||
![]() |
606b25b9e7 | ||
![]() |
fd9e52a559 | ||
![]() |
0a0f227601 | ||
![]() |
183558150d | ||
![]() |
c028e0553c | ||
![]() |
2581f7a10b | ||
![]() |
3e518773e2 | ||
![]() |
888f7e4403 | ||
![]() |
d82c26f0a9 | ||
![]() |
c1e2ffc0cd | ||
![]() |
06fccbc340 | ||
![]() |
fbd8090b0b | ||
![]() |
06ab707c79 | ||
![]() |
174a580319 | ||
![]() |
fbb256dd91 | ||
![]() |
5a7bade476 | ||
![]() |
d2bfcb018e | ||
![]() |
08f0f17ff7 | ||
![]() |
57b86f1130 | ||
![]() |
3a6eee7019 | ||
![]() |
9ce1cad983 | ||
![]() |
10da9485a5 | ||
![]() |
acfe381dd3 | ||
![]() |
83805c66e5 | ||
![]() |
afd8112e25 | ||
![]() |
b8c164dc60 | ||
![]() |
0453a72890 | ||
![]() |
e2c914cc11 | ||
![]() |
da907451e7 | ||
![]() |
2b4a6c96ee | ||
![]() |
d7061e6984 | ||
![]() |
3494d7759e | ||
![]() |
cc5dcceacc | ||
![]() |
863103450c | ||
![]() |
a0148a9996 | ||
![]() |
1f867a2c86 | ||
![]() |
c0a2acb869 | ||
![]() |
97835541ce | ||
![]() |
081cc66eda | ||
![]() |
7489e2c4f6 | ||
![]() |
1e675dbb68 | ||
![]() |
f4c1748ab1 | ||
![]() |
7990822f72 | ||
![]() |
2a100412fa | ||
![]() |
3e7652909b | ||
![]() |
9fb8498067 | ||
![]() |
291290db92 | ||
![]() |
54a115caf3 | ||
![]() |
81866170f0 | ||
![]() |
bf46829595 | ||
![]() |
9f14ad7125 | ||
![]() |
90a10c84ef | ||
![]() |
d220641d64 | ||
![]() |
caddc656fb | ||
![]() |
b1a591a06c | ||
![]() |
3cd3d0e0ff | ||
![]() |
433dad6ac2 | ||
![]() |
8cf408e966 | ||
![]() |
1e560529d8 | ||
![]() |
ff98444d03 | ||
![]() |
82f31d6b72 | ||
![]() |
6ae5143ff5 | ||
![]() |
bd8fec3836 | ||
![]() |
742331e054 | ||
![]() |
abd8e1bf54 | ||
![]() |
fa3b1fd9bd | ||
![]() |
e9946f81a0 | ||
![]() |
a9ba067e77 | ||
![]() |
706148f941 | ||
![]() |
24402312c5 | ||
![]() |
17180a3e08 | ||
![]() |
3c6ffd88bf | ||
![]() |
c3966f501c | ||
![]() |
56f0f3dfa4 | ||
![]() |
ad06d475de | ||
![]() |
0b4e3de9c0 | ||
![]() |
edd4a3733f | ||
![]() |
c17187777f | ||
![]() |
78e7b711df | ||
![]() |
4b7d3e24dd | ||
![]() |
e4f769963f | ||
![]() |
eab5dac6e8 | ||
![]() |
c4824a6ebc | ||
![]() |
f8a36e7554 | ||
![]() |
a2c3256ced | ||
![]() |
833c1505f1 | ||
![]() |
bac13d08ae | ||
![]() |
f0a27dcb00 | ||
![]() |
9d49d599f3 | ||
![]() |
2640f67e4b | ||
![]() |
eb8bc1af8d | ||
![]() |
0ded637b4a | ||
![]() |
dc50197a13 | ||
![]() |
06a1321e56 | ||
![]() |
6a6f8e80f1 | ||
![]() |
fd1ccbd3ad | ||
![]() |
3a1a72df98 | ||
![]() |
84784970b2 | ||
![]() |
d64a56d88b | ||
![]() |
be877ef719 | ||
![]() |
7c6d25dcd1 | ||
![]() |
b671238aa0 | ||
![]() |
91180e011d | ||
![]() |
26b8605fa0 | ||
![]() |
dbd360ebb9 | ||
![]() |
48e6e0659f | ||
![]() |
72eb1972c1 | ||
![]() |
63cbaedb79 | ||
![]() |
db6def8800 | ||
![]() |
caa841c434 | ||
![]() |
49a0f3a60d | ||
![]() |
bac82f43af | ||
![]() |
15cc801840 | ||
![]() |
5f9ca8e458 | ||
![]() |
c7e0ea525a | ||
![]() |
abd0a014e8 | ||
![]() |
4a7d3c7301 | ||
![]() |
15e60818c9 | ||
![]() |
def079267d | ||
![]() |
a3a9b60abc | ||
![]() |
808b12f618 | ||
![]() |
faa9208a3e | ||
![]() |
1ea27c4f97 | ||
![]() |
422ee40107 | ||
![]() |
0280fa5793 | ||
![]() |
664d6baace | ||
![]() |
be9196430d | ||
![]() |
533bccad8f | ||
![]() |
a4261aac76 | ||
![]() |
c38df57279 | ||
![]() |
4e2bbf9d6a | ||
![]() |
7a9ec851fc | ||
![]() |
d62cd2ae51 | ||
![]() |
49b5af6d45 | ||
![]() |
1f1444da8c | ||
![]() |
2a9a908343 | ||
![]() |
921b266d86 | ||
![]() |
dbbd164e39 | ||
![]() |
f5e3b0e2dd | ||
![]() |
1b9e63f426 | ||
![]() |
eb4323cc0f | ||
![]() |
a6712627e4 | ||
![]() |
3be32c4dac | ||
![]() |
55149e3336 | ||
![]() |
2b63e46fc5 | ||
![]() |
a0c449e570 | ||
![]() |
c997311bea | ||
![]() |
1929ca5d9d | ||
![]() |
88c6bf7595 | ||
![]() |
4635644e21 | ||
![]() |
f53ecaa97d | ||
![]() |
f704169aeb | ||
![]() |
2a7c4693b8 | ||
![]() |
da3871f39a | ||
![]() |
664ee7d89a | ||
![]() |
42b12934e3 | ||
![]() |
63f787f635 | ||
![]() |
a1bd348977 | ||
![]() |
27f29ba699 | ||
![]() |
cb0ce5b08f | ||
![]() |
b7c99788e4 | ||
![]() |
2316d89048 | ||
![]() |
bde4880c1d | ||
![]() |
8b648d0d3f | ||
![]() |
4617ee2b6b | ||
![]() |
10fa686c77 | ||
![]() |
2a16a5e967 | ||
![]() |
2aa0a2474b | ||
![]() |
d39003ffc0 | ||
![]() |
eae0989c40 | ||
![]() |
17a6431f5f | ||
![]() |
fcfb06ffa6 | ||
![]() |
7bdd9660aa | ||
![]() |
23b18d71ee | ||
![]() |
84cfed5231 | ||
![]() |
cdcf4a017d | ||
![]() |
fc0aff20cf | ||
![]() |
4223288cdf | ||
![]() |
a4f18f99ad | ||
![]() |
06df04f61c | ||
![]() |
cfcc9086ff | ||
![]() |
11b9cfad5e | ||
![]() |
5d913f7010 | ||
![]() |
d68dad580b | ||
![]() |
e13dc7c14a | ||
![]() |
b158896396 | ||
![]() |
f95dd4521c | ||
![]() |
1e77373332 | ||
![]() |
f01b96588d | ||
![]() |
4d589d9788 | ||
![]() |
815db0d962 | ||
![]() |
809c9b4481 | ||
![]() |
c6e3438e76 | ||
![]() |
844508bc48 | ||
![]() |
b3aec63d67 | ||
![]() |
2da4ae6b3b | ||
![]() |
5e6b498c22 | ||
![]() |
391beddaf4 | ||
![]() |
112b76b1c1 | ||
![]() |
315944968b | ||
![]() |
9f74555c88 | ||
![]() |
0a4e8e5909 | ||
![]() |
19156c7bbf | ||
![]() |
2a66ad4329 | ||
![]() |
53f14a2c4c | ||
![]() |
d20f21ae32 | ||
![]() |
f7db3490f6 | ||
![]() |
c7a7c913d4 | ||
![]() |
76a82339a2 | ||
![]() |
94df9cdbba | ||
![]() |
b231d7f15c | ||
![]() |
7cc346bc18 | ||
![]() |
48bc0db723 | ||
![]() |
7c196f4e00 | ||
![]() |
c86313d4fa | ||
![]() |
7b98741163 | ||
![]() |
2a04c213f9 | ||
![]() |
d7eaa9c5cc | ||
![]() |
2a0515f528 | ||
![]() |
3930fd08d7 | ||
![]() |
683eefbd0b | ||
![]() |
d963ad8cc1 | ||
![]() |
6d5e54a66b | ||
![]() |
2b2bfb91c2 | ||
![]() |
f1d2574651 | ||
![]() |
d39ce1401d | ||
![]() |
7fd5b22e3b | ||
![]() |
db7a7085f4 | ||
![]() |
5894d35eb2 | ||
![]() |
b9fd6127e2 | ||
![]() |
bb9bc0a001 | ||
![]() |
f4dd051a1d | ||
![]() |
06d3efc4d0 | ||
![]() |
66ad114e19 | ||
![]() |
4b737b46ac | ||
![]() |
bcd522e75f | ||
![]() |
249960b111 | ||
![]() |
583aea187b | ||
![]() |
396dac6d82 | ||
![]() |
9149be31af | ||
![]() |
32a4ded4a1 | ||
![]() |
e3dabdf525 | ||
![]() |
b59304a4df | ||
![]() |
66bc41125c | ||
![]() |
6786c44f4d | ||
![]() |
a3a1db124d | ||
![]() |
3b3c451c83 | ||
![]() |
cf99316082 | ||
![]() |
c063700255 | ||
![]() |
7540227388 | ||
![]() |
09015f113c | ||
![]() |
a2d8aec1e3 | ||
![]() |
ccdaaceb33 | ||
![]() |
b37876f3b2 | ||
![]() |
e8e0a4dcc5 | ||
![]() |
23cf39c525 | ||
![]() |
00996dd834 | ||
![]() |
2a52f666dc | ||
![]() |
0cf6545116 | ||
![]() |
5d691f405e | ||
![]() |
c61914c8e1 | ||
![]() |
9548c84d32 | ||
![]() |
02dd3d32f2 | ||
![]() |
7c98ba64aa | ||
![]() |
52018c3967 | ||
![]() |
e86fb11512 | ||
![]() |
20e3c42456 | ||
![]() |
1aff2a54ef | ||
![]() |
238ebcfcac | ||
![]() |
876fdf480d | ||
![]() |
3bc0a1924b | ||
![]() |
4af998963b | ||
![]() |
10d2da3009 | ||
![]() |
d47c1a8ba6 | ||
![]() |
9c0c74f547 | ||
![]() |
215d909e59 | ||
![]() |
ada15ceacc | ||
![]() |
716f82db6d | ||
![]() |
fe7d8c4f12 | ||
![]() |
cb2b5beea8 | ||
![]() |
2231a69b4c | ||
![]() |
13052388a7 | ||
![]() |
6fd39ae174 | ||
![]() |
2627ca5e3d | ||
![]() |
ed5b8d6a46 | ||
![]() |
2d0fdddd34 | ||
![]() |
3054af41ba | ||
![]() |
1e1a144dfa | ||
![]() |
cc3e1f58cc | ||
![]() |
b1991c8f4f | ||
![]() |
6f7f2820ce | ||
![]() |
e9697f13d6 | ||
![]() |
3b0aa23fdf | ||
![]() |
aca6218c0a | ||
![]() |
3a45628e1d | ||
![]() |
e923f63c49 | ||
![]() |
842feabced | ||
![]() |
286936db32 | ||
![]() |
bf7c4b4001 | ||
![]() |
d755a96c2c | ||
![]() |
c948324cf2 | ||
![]() |
76f81ac201 | ||
![]() |
ce188daccb | ||
![]() |
98702da4e6 | ||
![]() |
92f7f0c849 | ||
![]() |
7451abe3ea | ||
![]() |
3e6c66b899 | ||
![]() |
3a40bf8ae0 | ||
![]() |
9c922db14b | ||
![]() |
175fba5739 | ||
![]() |
912491cb28 | ||
![]() |
da2dbd2877 | ||
![]() |
c67f95ebff | ||
![]() |
3a8321f9ad | ||
![]() |
f46d64e52f | ||
![]() |
8c6e75a0cd | ||
![]() |
c23b4946c5 | ||
![]() |
ac52b234fa | ||
![]() |
9a47069f45 | ||
![]() |
7ef9fe3454 | ||
![]() |
fc852f8be6 | ||
![]() |
4710f739c0 | ||
![]() |
3c20c1b72e | ||
![]() |
37eb686b5b | ||
![]() |
fdd64fc966 | ||
![]() |
4d982d05af | ||
![]() |
1e725bc548 | ||
![]() |
dd8f4681a2 | ||
![]() |
0290f1f355 | ||
![]() |
cd835fc7a8 | ||
![]() |
2b70d9604a | ||
![]() |
d3968c2fd1 | ||
![]() |
8105c5cc60 | ||
![]() |
d1e5acd7b3 | ||
![]() |
68227c06c3 | ||
![]() |
31d1801912 | ||
![]() |
fb6bfa9753 | ||
![]() |
c30cc6120b | ||
![]() |
2b7c19835b | ||
![]() |
c2a5315e9f | ||
![]() |
0ddc3c01ef | ||
![]() |
c15205fb46 | ||
![]() |
cb837d5a1c | ||
![]() |
18ca2e4c29 | ||
![]() |
a10dae38e2 | ||
![]() |
7cf060ae5b | ||
![]() |
de9b0cec50 | ||
![]() |
773eded0af | ||
![]() |
df8703cc13 | ||
![]() |
71cffcd537 | ||
![]() |
f430b87459 | ||
![]() |
ca82b2940d | ||
![]() |
229444c932 | ||
![]() |
076e9810ba | ||
![]() |
6b131202b9 | ||
![]() |
275c6b447d | ||
![]() |
1a7893dbbd | ||
![]() |
5a04559cb4 | ||
![]() |
25c3d89f28 | ||
![]() |
8b5b7a1f63 | ||
![]() |
50b0eb9929 | ||
![]() |
7822a385bb | ||
![]() |
d5b4754cf4 | ||
![]() |
f47a5cd5d5 | ||
![]() |
a4637e2ba1 | ||
![]() |
33a2b2b772 | ||
![]() |
44fe6d1554 | ||
![]() |
cff52d7ebb | ||
![]() |
face766e0f | ||
![]() |
8708cd3b63 | ||
![]() |
bd8b616ca0 | ||
![]() |
785ddfc4aa | ||
![]() |
232978087a | ||
![]() |
7946c5f29e | ||
![]() |
efad401751 | ||
![]() |
e1e87b8d0c | ||
![]() |
c6d1421e81 | ||
![]() |
5a29511d34 | ||
![]() |
d024d205c0 | ||
![]() |
4649cd82b5 | ||
![]() |
057f8364cc | ||
![]() |
82e7f57b38 | ||
![]() |
3e22bbeecd | ||
![]() |
213579ee9d | ||
![]() |
810a6baf34 | ||
![]() |
61f6ac0d66 | ||
![]() |
6d981f37a2 | ||
![]() |
7e0b8ec0ac | ||
![]() |
19743ae195 | ||
![]() |
fd0ea4bf71 | ||
![]() |
f56424bc8d | ||
![]() |
01bf348811 | ||
![]() |
bea5d1e0d8 | ||
![]() |
e8b33e8c5a | ||
![]() |
dc7670f3a8 | ||
![]() |
94ce06bb76 | ||
![]() |
70863260f6 | ||
![]() |
cb9458122c | ||
![]() |
e62b0904ea | ||
![]() |
306ff5ee4e | ||
![]() |
e045abe961 | ||
![]() |
0daa3209db | ||
![]() |
8d0ed3ec51 | ||
![]() |
691e69847f | ||
![]() |
c8f64844ab | ||
![]() |
b0029c49b9 | ||
![]() |
91ad250177 | ||
![]() |
f6040ef2d7 | ||
![]() |
877ee48480 | ||
![]() |
03e6e43ecd | ||
![]() |
6ace16abf6 | ||
![]() |
158de9ca08 | ||
![]() |
ea2dcf4ff0 | ||
![]() |
332e7c9dba | ||
![]() |
0213a32e6a | ||
![]() |
cd3a163816 | ||
![]() |
2950349adf | ||
![]() |
56cacf6f1c | ||
![]() |
0f77ae14e4 | ||
![]() |
8fa990330f | ||
![]() |
84630f90b7 | ||
![]() |
1869a38b85 | ||
![]() |
e39358d375 | ||
![]() |
c7e601eb0b | ||
![]() |
ac4724e82c | ||
![]() |
73217f238c | ||
![]() |
d410f08642 | ||
![]() |
bfccd4f136 | ||
![]() |
c21820083b | ||
![]() |
865e35df17 | ||
![]() |
604b1a5cf1 | ||
![]() |
81ac01c2f5 | ||
![]() |
1ccc226c6b | ||
![]() |
0ce4446b1a | ||
![]() |
daa969508f | ||
![]() |
715b30a2b5 | ||
![]() |
42fe118cbe | ||
![]() |
06bfddf0da | ||
![]() |
931c8ece4a | ||
![]() |
85e571badd | ||
![]() |
0071a9cbf4 | ||
![]() |
a563b1ba9a | ||
![]() |
9e1ab74bb4 | ||
![]() |
adafb335ff | ||
![]() |
05487c7c15 | ||
![]() |
a2a327af7c | ||
![]() |
33c0e0f430 | ||
![]() |
1442c64420 | ||
![]() |
28644f236e | ||
![]() |
cc80152889 | ||
![]() |
dcdbcc0851 | ||
![]() |
1b0477d569 | ||
![]() |
57c92f8044 | ||
![]() |
e1d8c03e47 | ||
![]() |
7d2b22f58d | ||
![]() |
9efd9f06c6 | ||
![]() |
d0cbe46ff0 | ||
![]() |
025b64befc | ||
![]() |
92e59f14e0 | ||
![]() |
7989c7cdda | ||
![]() |
e22f5fef1f | ||
![]() |
64a022a4d2 | ||
![]() |
751be39376 | ||
![]() |
d05b84d0f5 | ||
![]() |
54bf91b76e | ||
![]() |
8ed79a00fd | ||
![]() |
8dffdadfd3 | ||
![]() |
2dbfbd45a2 | ||
![]() |
249440115b | ||
![]() |
baa8224cce | ||
![]() |
bd7b49b098 | ||
![]() |
27e2f0d545 | ||
![]() |
4dc14e1580 | ||
![]() |
df16012661 | ||
![]() |
e8cd85fee4 | ||
![]() |
35fd732b04 | ||
![]() |
10b1557c0e | ||
![]() |
49bd75b856 | ||
![]() |
02f8218867 | ||
![]() |
40eebbd9d8 | ||
![]() |
9ee199b0c3 | ||
![]() |
1c31f7905f | ||
![]() |
f6183e457d | ||
![]() |
2ecbcdda42 | ||
![]() |
0c8e51e1b7 | ||
![]() |
86b23338dd | ||
![]() |
7bee9c1c69 | ||
![]() |
6ef1e8c4f9 | ||
![]() |
0bc03e90a1 | ||
![]() |
9b898248c7 | ||
![]() |
566dc0a6a2 | ||
![]() |
b4be087a46 | ||
![]() |
722e553c6e | ||
![]() |
f8547ecba4 | ||
![]() |
e70cff196b | ||
![]() |
0286a804f4 | ||
![]() |
7239243163 | ||
![]() |
124471199c | ||
![]() |
84ec057f6e | ||
![]() |
8e08a72229 | ||
![]() |
e03a2b8636 | ||
![]() |
83d3cbfa99 | ||
![]() |
84cb0667f3 | ||
![]() |
c3924b566b | ||
![]() |
0f86506288 | ||
![]() |
b862283ed9 | ||
![]() |
ba682fa3b4 | ||
![]() |
8a63a2cc68 | ||
![]() |
f6dc22127a | ||
![]() |
bd3f9e0dbe | ||
![]() |
58d784aa29 | ||
![]() |
ae8e143fe9 | ||
![]() |
d9782c508a | ||
![]() |
39bc84d81c | ||
![]() |
89eb54b7ff | ||
![]() |
8bb58061fd | ||
![]() |
8392809eb1 | ||
![]() |
bb033fe02a | ||
![]() |
23f29d1bda | ||
![]() |
efe9d5000e | ||
![]() |
090d0fe684 | ||
![]() |
2fcb3c8b93 | ||
![]() |
729d66aa11 | ||
![]() |
b10dbc747b | ||
![]() |
07a3a6fa9a | ||
![]() |
6e106b5732 | ||
![]() |
00b362b43b | ||
![]() |
b6b27b66c8 | ||
![]() |
3573d40027 | ||
![]() |
e4600ccfef | ||
![]() |
0ae39807a4 | ||
![]() |
686319e2e3 | ||
![]() |
d655f4c1be | ||
![]() |
4b28146ee7 | ||
![]() |
d8a3b257f2 | ||
![]() |
714873694d | ||
![]() |
d81216cad7 | ||
![]() |
9e29dc808f | ||
![]() |
ada07de204 | ||
![]() |
2556e29984 | ||
![]() |
df4c38cb61 | ||
![]() |
2808dd2000 | ||
![]() |
17ad5f0595 | ||
![]() |
b8411ae2fd | ||
![]() |
566833111c | ||
![]() |
ee96a03d60 | ||
![]() |
21bc099ccf | ||
![]() |
1ce03059a0 | ||
![]() |
9ed352d4c0 | ||
![]() |
a08c667230 | ||
![]() |
414c7c40c4 | ||
![]() |
1712e63e06 | ||
![]() |
272e27ae01 | ||
![]() |
9046223e7f | ||
![]() |
0066f20bdd | ||
![]() |
a5465dfd3e | ||
![]() |
8086bee146 | ||
![]() |
b11a3b80bc | ||
![]() |
554146f46e | ||
![]() |
7bc84dc971 | ||
![]() |
d89141100c | ||
![]() |
f9bf465578 | ||
![]() |
3933bd9a8e | ||
![]() |
db0659cb3d | ||
![]() |
1219535e56 | ||
![]() |
4a12a7cbc8 | ||
![]() |
08072d2c8d | ||
![]() |
1ebf417c11 | ||
![]() |
a2a7c61872 | ||
![]() |
61277452af | ||
![]() |
6be5e83e61 | ||
![]() |
c70c0129f8 | ||
![]() |
b5b8181851 | ||
![]() |
194a85d4c5 | ||
![]() |
a6edf00810 | ||
![]() |
738f5e8f68 | ||
![]() |
5a9462c9ab | ||
![]() |
5695121f38 | ||
![]() |
8bafdc4623 | ||
![]() |
36a6d724fe | ||
![]() |
9385ea0e7c | ||
![]() |
9f059ad4c3 | ||
![]() |
5c6c6f272c | ||
![]() |
65fa4b2ca4 | ||
![]() |
6788225cac | ||
![]() |
a5bb6786c8 | ||
![]() |
3b2b35aab7 | ||
![]() |
949f2523f9 | ||
![]() |
196c83939c | ||
![]() |
237645e975 | ||
![]() |
86162c2c20 | ||
![]() |
199c84195a | ||
![]() |
57ac4160b7 | ||
![]() |
91c648253a | ||
![]() |
5a80507006 | ||
![]() |
cc14727888 | ||
![]() |
94573a3a61 | ||
![]() |
0ba0fa5f6c | ||
![]() |
ad6eb92bbd | ||
![]() |
ce76041c03 | ||
![]() |
8f063c99d5 | ||
![]() |
557d119bee | ||
![]() |
e9f87e1952 | ||
![]() |
f3795846b5 | ||
![]() |
b8eaa3be85 | ||
![]() |
c496e599ef | ||
![]() |
27692a2f14 | ||
![]() |
e57cd437d4 | ||
![]() |
5a99460a4c | ||
![]() |
ba83d0ac68 | ||
![]() |
bcd6c0bf53 | ||
![]() |
b4225cb0fc | ||
![]() |
98b67da649 | ||
![]() |
0be8500c4f | ||
![]() |
97507d2880 | ||
![]() |
c4353405a5 | ||
![]() |
de6c3312ce | ||
![]() |
c66866d890 | ||
![]() |
b21a44ca4c | ||
![]() |
e04d4ff150 | ||
![]() |
2645494582 | ||
![]() |
77f4b68c8e | ||
![]() |
6602f6114c | ||
![]() |
3aece38e9d | ||
![]() |
9cfef51af3 | ||
![]() |
aee6bf7e7a | ||
![]() |
b8d92d3cec | ||
![]() |
0c4b42ac13 | ||
![]() |
91d5fbd56c | ||
![]() |
d4217007fe | ||
![]() |
35b82d51cf | ||
![]() |
0ed1e42aed | ||
![]() |
2b644ef7b7 | ||
![]() |
0ad6eac4f8 | ||
![]() |
accdb77315 | ||
![]() |
914152fcbd | ||
![]() |
2a00c547a1 | ||
![]() |
adeb8ee425 | ||
![]() |
d74074ad53 | ||
![]() |
41d3da245e | ||
![]() |
0565b5a6c8 | ||
![]() |
f3502beb94 | ||
![]() |
d6b9874b35 | ||
![]() |
1d01e2a077 | ||
![]() |
ce714cfd07 | ||
![]() |
50b24b37c2 | ||
![]() |
9db0473ed5 | ||
![]() |
5d8c80b170 | ||
![]() |
21ae63d46b | ||
![]() |
c8951a1d9c | ||
![]() |
5fa9190117 | ||
![]() |
77a87881c9 | ||
![]() |
7bf538f549 | ||
![]() |
a5757ab195 | ||
![]() |
d1d2217019 | ||
![]() |
1f7b3fa4ac | ||
![]() |
8709c3ae7b | ||
![]() |
5db4c001d1 | ||
![]() |
583ec51f9f | ||
![]() |
f602d32aaa | ||
![]() |
a6976e6d2d | ||
![]() |
f2b8aa28f3 | ||
![]() |
bfbefb0cd2 | ||
![]() |
31918bb990 | ||
![]() |
f110b5710a | ||
![]() |
1cc0b55650 | ||
![]() |
63a2c6cce5 |
219 changed files with 29904 additions and 20716 deletions
|
@ -25,4 +25,4 @@ docker-compose*
|
|||
rustfmt.toml
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
#*.md
|
||||
|
|
5
.envrc
Normal file
5
.envrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
use flake
|
||||
|
||||
PATH_add bin
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -31,7 +31,6 @@ modules.xml
|
|||
|
||||
### vscode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
@ -57,9 +56,18 @@ $RECYCLE.BIN/
|
|||
*.lnk
|
||||
|
||||
# Conduit
|
||||
Rocket.toml
|
||||
conduit.toml
|
||||
conduit.db
|
||||
|
||||
# Etc.
|
||||
**/*.rs.bk
|
||||
cached_target
|
||||
|
||||
# Nix artifacts
|
||||
/result*
|
||||
|
||||
# Direnv cache
|
||||
/.direnv
|
||||
|
||||
# Gitlab CI cache
|
||||
/.gitlab-ci.d
|
||||
|
|
453
.gitlab-ci.yml
453
.gitlab-ci.yml
|
@ -1,315 +1,180 @@
|
|||
stages:
|
||||
- build
|
||||
- build docker image
|
||||
- test
|
||||
- upload artifacts
|
||||
- ci
|
||||
- artifacts
|
||||
- publish
|
||||
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
FF_USE_FASTZIP: 1
|
||||
CACHE_COMPRESSION_LEVEL: fastest
|
||||
# Docker in Docker
|
||||
DOCKER_HOST: tcp://docker:2375/
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_DRIVER: overlay2
|
||||
# Makes some things print in color
|
||||
TERM: ansi
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Cargo: Compiling for different architectures #
|
||||
# --------------------------------------------------------------------- #
|
||||
before_script:
|
||||
# Enable nix-command and flakes
|
||||
- if command -v nix > /dev/null; then echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf; fi
|
||||
|
||||
.build-cargo-shared-settings:
|
||||
stage: "build"
|
||||
needs: []
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
- if: '$CI_COMMIT_BRANCH == "next"'
|
||||
- if: "$CI_COMMIT_TAG"
|
||||
- if: '($CI_MERGE_REQUEST_APPROVED == "true") || $BUILD_EVERYTHING' # Once MR is approved, test all builds. Or if BUILD_EVERYTHING is set.
|
||||
interruptible: true
|
||||
image: "registry.gitlab.com/jfowl/conduit-containers/rust-with-tools:latest"
|
||||
tags: ["docker"]
|
||||
services: ["docker:dind"]
|
||||
variables:
|
||||
SHARED_PATH: $CI_PROJECT_DIR/shared
|
||||
CARGO_PROFILE_RELEASE_LTO: "true"
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1"
|
||||
CARGO_INCREMENTAL: "false" # https://matklad.github.io/2021/09/04/fast-rust-builds.html#ci-workflow
|
||||
before_script:
|
||||
- 'echo "Building for target $TARGET"'
|
||||
- "rustup show && rustc --version && cargo --version" # Print version info for debugging
|
||||
# fix cargo and rustup mounts from this container (https://gitlab.com/gitlab-org/gitlab-foss/-/issues/41227)
|
||||
- "mkdir -p $SHARED_PATH/cargo"
|
||||
- "cp -r $CARGO_HOME/bin $SHARED_PATH/cargo"
|
||||
- "cp -r $RUSTUP_HOME $SHARED_PATH"
|
||||
- "export CARGO_HOME=$SHARED_PATH/cargo RUSTUP_HOME=$SHARED_PATH/rustup"
|
||||
# If provided, bring in caching through sccache, which uses an external S3 endpoint to store compilation results.
|
||||
- if [ -n "${SCCACHE_ENDPOINT}" ]; then export RUSTC_WRAPPER=/sccache; fi
|
||||
# Add our own binary cache
|
||||
- if command -v nix > /dev/null; then echo "extra-substituters = https://nix.computer.surgery/conduit" >> /etc/nix/nix.conf; fi
|
||||
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = conduit:ZGAf6P6LhNvnoJJ3Me3PRg7tlLSrPxcQ2RiE5LIppjo=" >> /etc/nix/nix.conf; fi
|
||||
|
||||
# Add crane binary cache
|
||||
- if command -v nix > /dev/null; then echo "extra-substituters = https://crane.cachix.org" >> /etc/nix/nix.conf; fi
|
||||
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = crane.cachix.org-1:8Scfpmn9w+hGdXH/Q9tTLiYAE/2dnJYRJP7kl80GuRk=" >> /etc/nix/nix.conf; fi
|
||||
|
||||
# Add nix-community binary cache
|
||||
- if command -v nix > /dev/null; then echo "extra-substituters = https://nix-community.cachix.org" >> /etc/nix/nix.conf; fi
|
||||
- if command -v nix > /dev/null; then echo "extra-trusted-public-keys = nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" >> /etc/nix/nix.conf; fi
|
||||
|
||||
# Install direnv and nix-direnv
|
||||
- if command -v nix > /dev/null; then nix-env -iA nixpkgs.direnv nixpkgs.nix-direnv; fi
|
||||
|
||||
# Allow .envrc
|
||||
- if command -v nix > /dev/null; then direnv allow; fi
|
||||
|
||||
# Set CARGO_HOME to a cacheable path
|
||||
- export CARGO_HOME="$(git rev-parse --show-toplevel)/.gitlab-ci.d/cargo"
|
||||
|
||||
ci:
|
||||
stage: ci
|
||||
image: nixos/nix:2.19.2
|
||||
script:
|
||||
# cross-compile conduit for target
|
||||
- 'time cross build --target="$TARGET" --locked --release'
|
||||
- 'mv "target/$TARGET/release/conduit" "conduit-$TARGET"'
|
||||
# print information about linking for debugging
|
||||
- "file conduit-$TARGET" # print file information
|
||||
- 'readelf --dynamic conduit-$TARGET | sed -e "/NEEDED/q1"' # ensure statically linked
|
||||
- direnv exec . engage
|
||||
cache:
|
||||
# https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci
|
||||
key: "cargo-cache-$TARGET"
|
||||
key: nix
|
||||
paths:
|
||||
- $SHARED_PATH/cargo/registry/index
|
||||
- $SHARED_PATH/cargo/registry/cache
|
||||
- $SHARED_PATH/cargo/git/db
|
||||
artifacts:
|
||||
expire_in: never
|
||||
- target
|
||||
- .gitlab-ci.d
|
||||
|
||||
build:release:cargo:x86_64-unknown-linux-musl-with-debug:
|
||||
extends: .build-cargo-shared-settings
|
||||
variables:
|
||||
CARGO_PROFILE_RELEASE_DEBUG: 2 # Enable debug info for flamegraph profiling
|
||||
TARGET: "x86_64-unknown-linux-musl"
|
||||
after_script:
|
||||
- "mv ./conduit-x86_64-unknown-linux-musl ./conduit-x86_64-unknown-linux-musl-with-debug"
|
||||
artifacts:
|
||||
name: "conduit-x86_64-unknown-linux-musl-with-debug"
|
||||
paths:
|
||||
- "conduit-x86_64-unknown-linux-musl-with-debug"
|
||||
expose_as: "Conduit for x86_64-unknown-linux-musl-with-debug"
|
||||
|
||||
build:release:cargo:x86_64-unknown-linux-musl:
|
||||
extends: .build-cargo-shared-settings
|
||||
variables:
|
||||
TARGET: "x86_64-unknown-linux-musl"
|
||||
artifacts:
|
||||
name: "conduit-x86_64-unknown-linux-musl"
|
||||
paths:
|
||||
- "conduit-x86_64-unknown-linux-musl"
|
||||
expose_as: "Conduit for x86_64-unknown-linux-musl"
|
||||
|
||||
build:release:cargo:arm-unknown-linux-musleabihf:
|
||||
extends: .build-cargo-shared-settings
|
||||
variables:
|
||||
TARGET: "arm-unknown-linux-musleabihf"
|
||||
artifacts:
|
||||
name: "conduit-arm-unknown-linux-musleabihf"
|
||||
paths:
|
||||
- "conduit-arm-unknown-linux-musleabihf"
|
||||
expose_as: "Conduit for arm-unknown-linux-musleabihf"
|
||||
|
||||
build:release:cargo:armv7-unknown-linux-musleabihf:
|
||||
extends: .build-cargo-shared-settings
|
||||
variables:
|
||||
TARGET: "armv7-unknown-linux-musleabihf"
|
||||
artifacts:
|
||||
name: "conduit-armv7-unknown-linux-musleabihf"
|
||||
paths:
|
||||
- "conduit-armv7-unknown-linux-musleabihf"
|
||||
expose_as: "Conduit for armv7-unknown-linux-musleabihf"
|
||||
|
||||
build:release:cargo:aarch64-unknown-linux-musl:
|
||||
extends: .build-cargo-shared-settings
|
||||
variables:
|
||||
TARGET: "aarch64-unknown-linux-musl"
|
||||
artifacts:
|
||||
name: "conduit-aarch64-unknown-linux-musl"
|
||||
paths:
|
||||
- "conduit-aarch64-unknown-linux-musl"
|
||||
expose_as: "Conduit for aarch64-unknown-linux-musl"
|
||||
|
||||
.cargo-debug-shared-settings:
|
||||
extends: ".build-cargo-shared-settings"
|
||||
rules:
|
||||
- when: "always"
|
||||
cache:
|
||||
key: "build_cache--$TARGET--$CI_COMMIT_BRANCH--debug"
|
||||
static:x86_64-unknown-linux-musl:
|
||||
stage: artifacts
|
||||
image: nixos/nix:2.19.2
|
||||
script:
|
||||
# cross-compile conduit for target
|
||||
- 'time time cross build --target="$TARGET" --locked'
|
||||
- 'mv "target/$TARGET/debug/conduit" "conduit-debug-$TARGET"'
|
||||
# print information about linking for debugging
|
||||
- "file conduit-debug-$TARGET" # print file information
|
||||
- 'readelf --dynamic conduit-debug-$TARGET | sed -e "/NEEDED/q1"' # ensure statically linked
|
||||
artifacts:
|
||||
expire_in: 4 weeks
|
||||
# Push artifacts and build requirements to binary cache
|
||||
- ./bin/nix-build-and-cache .#static-x86_64-unknown-linux-musl
|
||||
|
||||
build:debug:cargo:x86_64-unknown-linux-musl:
|
||||
extends: ".cargo-debug-shared-settings"
|
||||
variables:
|
||||
TARGET: "x86_64-unknown-linux-musl"
|
||||
# Make the output less difficult to find
|
||||
- cp result/bin/conduit conduit
|
||||
artifacts:
|
||||
name: "conduit-debug-x86_64-unknown-linux-musl"
|
||||
paths:
|
||||
- "conduit-debug-x86_64-unknown-linux-musl"
|
||||
expose_as: "Conduit DEBUG for x86_64-unknown-linux-musl"
|
||||
- conduit
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Create and publish docker image #
|
||||
# --------------------------------------------------------------------- #
|
||||
static:aarch64-unknown-linux-musl:
|
||||
stage: artifacts
|
||||
image: nixos/nix:2.19.2
|
||||
script:
|
||||
# Push artifacts and build requirements to binary cache
|
||||
- ./bin/nix-build-and-cache .#static-aarch64-unknown-linux-musl
|
||||
|
||||
.docker-shared-settings:
|
||||
stage: "build docker image"
|
||||
image: jdrouet/docker-with-buildx:stable
|
||||
tags: ["docker"]
|
||||
# Make the output less difficult to find
|
||||
- cp result/bin/conduit conduit
|
||||
artifacts:
|
||||
paths:
|
||||
- conduit
|
||||
|
||||
# Note that although we have an `oci-image-x86_64-unknown-linux-musl` output,
|
||||
# we don't build it because it would be largely redundant to this one since it's
|
||||
# all containerized anyway.
|
||||
oci-image:x86_64-unknown-linux-gnu:
|
||||
stage: artifacts
|
||||
image: nixos/nix:2.19.2
|
||||
script:
|
||||
# Push artifacts and build requirements to binary cache
|
||||
#
|
||||
# Since the OCI image package is based on the binary package, this has the
|
||||
# fun side effect of uploading the normal binary too. Conduit users who are
|
||||
# deploying with Nix can leverage this fact by adding our binary cache to
|
||||
# their systems.
|
||||
- ./bin/nix-build-and-cache .#oci-image
|
||||
|
||||
# Make the output less difficult to find
|
||||
- cp result oci-image-amd64.tar.gz
|
||||
artifacts:
|
||||
paths:
|
||||
- oci-image-amd64.tar.gz
|
||||
|
||||
oci-image:aarch64-unknown-linux-musl:
|
||||
stage: artifacts
|
||||
needs:
|
||||
# Wait for the static binary job to finish before starting so we don't have
|
||||
# to build that twice for no reason
|
||||
- static:aarch64-unknown-linux-musl
|
||||
image: nixos/nix:2.19.2
|
||||
script:
|
||||
# Push artifacts and build requirements to binary cache
|
||||
- ./bin/nix-build-and-cache .#oci-image-aarch64-unknown-linux-musl
|
||||
|
||||
# Make the output less difficult to find
|
||||
- cp result oci-image-arm64v8.tar.gz
|
||||
artifacts:
|
||||
paths:
|
||||
- oci-image-arm64v8.tar.gz
|
||||
|
||||
debian:x86_64-unknown-linux-gnu:
|
||||
stage: artifacts
|
||||
# See also `rust-toolchain.toml`
|
||||
image: rust:1.75.0
|
||||
script:
|
||||
- apt-get update && apt-get install -y --no-install-recommends libclang-dev
|
||||
- cargo install cargo-deb
|
||||
- cargo deb
|
||||
|
||||
# Make the output less difficult to find
|
||||
- mv target/debian/*.deb conduit.deb
|
||||
artifacts:
|
||||
paths:
|
||||
- conduit.deb
|
||||
cache:
|
||||
key: debian
|
||||
paths:
|
||||
- target
|
||||
- .gitlab-ci.d
|
||||
|
||||
.push-oci-image:
|
||||
stage: publish
|
||||
image: docker:25.0.0
|
||||
services:
|
||||
- docker:dind
|
||||
needs:
|
||||
- "build:release:cargo:x86_64-unknown-linux-musl"
|
||||
- "build:release:cargo:arm-unknown-linux-musleabihf"
|
||||
- "build:release:cargo:armv7-unknown-linux-musleabihf"
|
||||
- "build:release:cargo:aarch64-unknown-linux-musl"
|
||||
- docker:25.0.0-dind
|
||||
variables:
|
||||
PLATFORMS: "linux/arm/v6,linux/arm/v7,linux/arm64,linux/amd64"
|
||||
DOCKER_FILE: "docker/ci-binaries-packaging.Dockerfile"
|
||||
cache:
|
||||
paths:
|
||||
- docker_cache
|
||||
key: "$CI_JOB_NAME"
|
||||
IMAGE_SUFFIX_AMD64: amd64
|
||||
IMAGE_SUFFIX_ARM64V8: arm64v8
|
||||
script:
|
||||
- docker load -i oci-image-amd64.tar.gz
|
||||
- IMAGE_ID_AMD64=$(docker images -q conduit:next)
|
||||
- docker load -i oci-image-arm64v8.tar.gz
|
||||
- IMAGE_ID_ARM64V8=$(docker images -q conduit:next)
|
||||
# Tag and push the architecture specific images
|
||||
- docker tag $IMAGE_ID_AMD64 $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_AMD64
|
||||
- docker tag $IMAGE_ID_ARM64V8 $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_ARM64V8
|
||||
- docker push $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_AMD64
|
||||
- docker push $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_ARM64V8
|
||||
# Tag the multi-arch image
|
||||
- docker manifest create $IMAGE_NAME:$CI_COMMIT_SHA --amend $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_AMD64 --amend $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_ARM64V8
|
||||
- docker manifest push $IMAGE_NAME:$CI_COMMIT_SHA
|
||||
# Tag and push the git ref
|
||||
- docker manifest create $IMAGE_NAME:$CI_COMMIT_REF_NAME --amend $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_AMD64 --amend $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_ARM64V8
|
||||
- docker manifest push $IMAGE_NAME:$CI_COMMIT_REF_NAME
|
||||
# Tag git tags as 'latest'
|
||||
- |
|
||||
if [[ -n "$CI_COMMIT_TAG" ]]; then
|
||||
docker manifest create $IMAGE_NAME:latest --amend $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_AMD64 --amend $IMAGE_NAME:$CI_COMMIT_SHA-$IMAGE_SUFFIX_ARM64V8
|
||||
docker manifest push $IMAGE_NAME:latest
|
||||
fi
|
||||
dependencies:
|
||||
- oci-image:x86_64-unknown-linux-gnu
|
||||
- oci-image:aarch64-unknown-linux-musl
|
||||
only:
|
||||
- next
|
||||
- master
|
||||
- tags
|
||||
|
||||
oci-image:push-gitlab:
|
||||
extends: .push-oci-image
|
||||
variables:
|
||||
IMAGE_NAME: $CI_REGISTRY_IMAGE/matrix-conduit
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
||||
# Only log in to Dockerhub if the credentials are given:
|
||||
- if [ -n "${DOCKER_HUB}" ]; then docker login -u "$DOCKER_HUB_USER" -p "$DOCKER_HUB_PASSWORD" "$DOCKER_HUB"; fi
|
||||
script:
|
||||
# Prepare buildx to build multiarch stuff:
|
||||
- docker context create 'ci-context'
|
||||
- docker buildx create --name 'multiarch-builder' --use 'ci-context'
|
||||
# Copy binaries to their docker arch path
|
||||
- mkdir -p linux/ && mv ./conduit-x86_64-unknown-linux-musl linux/amd64
|
||||
- mkdir -p linux/arm/ && mv ./conduit-arm-unknown-linux-musleabihf linux/arm/v6
|
||||
- mkdir -p linux/arm/ && mv ./conduit-armv7-unknown-linux-musleabihf linux/arm/v7
|
||||
- mv ./conduit-aarch64-unknown-linux-musl linux/arm64
|
||||
- 'export CREATED=$(date -u +''%Y-%m-%dT%H:%M:%SZ'') && echo "Docker image creation date: $CREATED"'
|
||||
# Build and push image:
|
||||
- >
|
||||
docker buildx build
|
||||
--pull
|
||||
--push
|
||||
--cache-from=type=local,src=$CI_PROJECT_DIR/docker_cache
|
||||
--cache-to=type=local,dest=$CI_PROJECT_DIR/docker_cache
|
||||
--build-arg CREATED=$CREATED
|
||||
--build-arg VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml)
|
||||
--build-arg "GIT_REF=$CI_COMMIT_SHORT_SHA"
|
||||
--platform "$PLATFORMS"
|
||||
--tag "$TAG"
|
||||
--tag "$TAG-alpine"
|
||||
--tag "$TAG-commit-$CI_COMMIT_SHORT_SHA"
|
||||
--file "$DOCKER_FILE" .
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
|
||||
docker:next:gitlab:
|
||||
extends: .docker-shared-settings
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "next"'
|
||||
oci-image:push-dockerhub:
|
||||
extends: .push-oci-image
|
||||
variables:
|
||||
TAG: "$CI_REGISTRY_IMAGE/matrix-conduit:next"
|
||||
|
||||
docker:next:dockerhub:
|
||||
extends: .docker-shared-settings
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "next" && $DOCKER_HUB'
|
||||
variables:
|
||||
TAG: "$DOCKER_HUB_IMAGE/matrixconduit/matrix-conduit:next"
|
||||
|
||||
docker:master:gitlab:
|
||||
extends: .docker-shared-settings
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
variables:
|
||||
TAG: "$CI_REGISTRY_IMAGE/matrix-conduit:latest"
|
||||
|
||||
docker:master:dockerhub:
|
||||
extends: .docker-shared-settings
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master" && $DOCKER_HUB'
|
||||
variables:
|
||||
TAG: "$DOCKER_HUB_IMAGE/matrixconduit/matrix-conduit:latest"
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Run tests #
|
||||
# --------------------------------------------------------------------- #
|
||||
|
||||
test:cargo:
|
||||
stage: "test"
|
||||
needs: []
|
||||
image: "registry.gitlab.com/jfowl/conduit-containers/rust-with-tools:latest"
|
||||
tags: ["docker"]
|
||||
variables:
|
||||
CARGO_INCREMENTAL: "false" # https://matklad.github.io/2021/09/04/fast-rust-builds.html#ci-workflow
|
||||
interruptible: true
|
||||
IMAGE_NAME: matrixconduit/matrix-conduit
|
||||
before_script:
|
||||
- rustup component add clippy rustfmt
|
||||
# If provided, bring in caching through sccache, which uses an external S3 endpoint to store compilation results:
|
||||
- if [ -n "${SCCACHE_ENDPOINT}" ]; then export RUSTC_WRAPPER=/usr/local/cargo/bin/sccache; fi
|
||||
script:
|
||||
- rustc --version && cargo --version # Print version info for debugging
|
||||
- cargo fmt --all -- --check
|
||||
- "cargo test --color always --workspace --verbose --locked --no-fail-fast -- -Z unstable-options --format json | gitlab-report -p test > $CI_PROJECT_DIR/report.xml"
|
||||
- "cargo clippy --color always --verbose --message-format=json | gitlab-report -p clippy > $CI_PROJECT_DIR/gl-code-quality-report.json"
|
||||
artifacts:
|
||||
when: always
|
||||
reports:
|
||||
junit: report.xml
|
||||
codequality: gl-code-quality-report.json
|
||||
|
||||
test:sytest:
|
||||
stage: "test"
|
||||
allow_failure: true
|
||||
needs:
|
||||
- "build:debug:cargo:x86_64-unknown-linux-musl"
|
||||
image:
|
||||
name: "valkum/sytest-conduit:latest"
|
||||
entrypoint: [""]
|
||||
tags: ["docker"]
|
||||
variables:
|
||||
PLUGINS: "https://github.com/valkum/sytest_conduit/archive/master.tar.gz"
|
||||
before_script:
|
||||
- "mkdir -p /app"
|
||||
- "cp ./conduit-debug-x86_64-unknown-linux-musl /app/conduit"
|
||||
- "chmod +x /app/conduit"
|
||||
- "rm -rf /src && ln -s $CI_PROJECT_DIR/ /src"
|
||||
- "mkdir -p /work/server-0/database/ && mkdir -p /work/server-1/database/ && mkdir -p /work/server-2/database/"
|
||||
- "cd /"
|
||||
script:
|
||||
- "SYTEST_EXIT_CODE=0"
|
||||
- "/bootstrap.sh conduit || SYTEST_EXIT_CODE=1"
|
||||
- 'perl /sytest/tap-to-junit-xml.pl --puretap --input /logs/results.tap --output $CI_PROJECT_DIR/sytest.xml "Sytest" && cp /logs/results.tap $CI_PROJECT_DIR/results.tap'
|
||||
- "exit $SYTEST_EXIT_CODE"
|
||||
artifacts:
|
||||
when: always
|
||||
paths:
|
||||
- "$CI_PROJECT_DIR/sytest.xml"
|
||||
- "$CI_PROJECT_DIR/results.tap"
|
||||
reports:
|
||||
junit: "$CI_PROJECT_DIR/sytest.xml"
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Store binaries as package so they have download urls #
|
||||
# --------------------------------------------------------------------- #
|
||||
|
||||
publish:package:
|
||||
stage: "upload artifacts"
|
||||
needs:
|
||||
- "build:release:cargo:x86_64-unknown-linux-musl"
|
||||
- "build:release:cargo:arm-unknown-linux-musleabihf"
|
||||
- "build:release:cargo:armv7-unknown-linux-musleabihf"
|
||||
- "build:release:cargo:aarch64-unknown-linux-musl"
|
||||
# - "build:cargo-deb:x86_64-unknown-linux-gnu"
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "master"'
|
||||
- if: '$CI_COMMIT_BRANCH == "next"'
|
||||
- if: "$CI_COMMIT_TAG"
|
||||
image: curlimages/curl:latest
|
||||
tags: ["docker"]
|
||||
variables:
|
||||
GIT_STRATEGY: "none" # Don't need a clean copy of the code, we just operate on artifacts
|
||||
script:
|
||||
- 'BASE_URL="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/conduit-${CI_COMMIT_REF_SLUG}/build-${CI_PIPELINE_ID}"'
|
||||
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file conduit-x86_64-unknown-linux-musl "${BASE_URL}/conduit-x86_64-unknown-linux-musl"'
|
||||
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file conduit-arm-unknown-linux-musleabihf "${BASE_URL}/conduit-arm-unknown-linux-musleabihf"'
|
||||
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file conduit-armv7-unknown-linux-musleabihf "${BASE_URL}/conduit-armv7-unknown-linux-musleabihf"'
|
||||
- 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file conduit-aarch64-unknown-linux-musl "${BASE_URL}/conduit-aarch64-unknown-linux-musl"'
|
||||
|
||||
# Avoid duplicate pipelines
|
||||
# See: https://docs.gitlab.com/ee/ci/yaml/workflow.html#switch-between-branch-pipelines-and-merge-request-pipelines
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
|
||||
when: never
|
||||
- if: "$CI_COMMIT_BRANCH"
|
||||
- docker login -u $DOCKER_HUB_USER -p $DOCKER_HUB_PASSWORD
|
5
.gitlab/CODEOWNERS
Normal file
5
.gitlab/CODEOWNERS
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Nix things
|
||||
.envrc @CobaltCause
|
||||
flake.lock @CobaltCause
|
||||
flake.nix @CobaltCause
|
||||
nix/ @CobaltCause
|
37
.gitlab/setup-buildx-remote-builders.sh
Normal file
37
.gitlab/setup-buildx-remote-builders.sh
Normal file
|
@ -0,0 +1,37 @@
|
|||
#!/bin/sh
|
||||
set -eux
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# #
|
||||
# Configures docker buildx to use a remote server for arm building. #
|
||||
# Expects $SSH_PRIVATE_KEY to be a valid ssh ed25519 private key with #
|
||||
# access to the server $ARM_SERVER_USER@$ARM_SERVER_IP #
|
||||
# #
|
||||
# This is expected to only be used in the official CI/CD pipeline! #
|
||||
# #
|
||||
# Requirements: openssh-client, docker buildx #
|
||||
# Inspired by: https://depot.dev/blog/building-arm-containers #
|
||||
# #
|
||||
# --------------------------------------------------------------------- #
|
||||
|
||||
cat "$BUILD_SERVER_SSH_PRIVATE_KEY" | ssh-add -
|
||||
|
||||
# Test server connections:
|
||||
ssh "$ARM_SERVER_USER@$ARM_SERVER_IP" "uname -a"
|
||||
ssh "$AMD_SERVER_USER@$AMD_SERVER_IP" "uname -a"
|
||||
|
||||
# Connect remote arm64 server for all arm builds:
|
||||
docker buildx create \
|
||||
--name "multi" \
|
||||
--driver "docker-container" \
|
||||
--platform "linux/arm64,linux/arm/v7" \
|
||||
"ssh://$ARM_SERVER_USER@$ARM_SERVER_IP"
|
||||
|
||||
# Connect remote amd64 server for adm64 builds:
|
||||
docker buildx create --append \
|
||||
--name "multi" \
|
||||
--driver "docker-container" \
|
||||
--platform "linux/amd64" \
|
||||
"ssh://$AMD_SERVER_USER@$AMD_SERVER_IP"
|
||||
|
||||
docker buildx use multi
|
11
.vscode/extensions.json
vendored
Normal file
11
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"bungcip.better-toml",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"eamodio.gitlens",
|
||||
"serayuzgur.crates",
|
||||
"vadimcn.vscode-lldb",
|
||||
"timonwong.shellcheck"
|
||||
]
|
||||
}
|
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug conduit",
|
||||
"sourceLanguages": ["rust"],
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--bin=conduit",
|
||||
"--package=conduit"
|
||||
],
|
||||
"filter": {
|
||||
"name": "conduit",
|
||||
"kind": "bin"
|
||||
}
|
||||
},
|
||||
"args": [],
|
||||
"env": {
|
||||
"RUST_BACKTRACE": "1",
|
||||
"CONDUIT_CONFIG": "",
|
||||
"CONDUIT_SERVER_NAME": "localhost",
|
||||
"CONDUIT_DATABASE_PATH": "/tmp",
|
||||
"CONDUIT_ADDRESS": "0.0.0.0",
|
||||
"CONDUIT_PORT": "6167"
|
||||
},
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"rust-analyzer.procMacro.enable": true
|
||||
}
|
134
CODE_OF_CONDUCT.md
Normal file
134
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,134 @@
|
|||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement over email at
|
||||
coc@koesters.xyz or over Matrix at @timo:conduit.rs.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
3174
Cargo.lock
generated
3174
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
137
Cargo.toml
137
Cargo.toml
|
@ -1,3 +1,14 @@
|
|||
# Keep alphabetically sorted
|
||||
[workspace.lints.rust]
|
||||
explicit_outlives_requirements = "warn"
|
||||
unused_qualifications = "warn"
|
||||
|
||||
# Keep alphabetically sorted
|
||||
[workspace.lints.clippy]
|
||||
cloned_instead_of_copied = "warn"
|
||||
dbg_macro = "warn"
|
||||
str_to_string = "warn"
|
||||
|
||||
[package]
|
||||
name = "conduit"
|
||||
description = "A Matrix homeserver written in Rust"
|
||||
|
@ -6,99 +17,119 @@ authors = ["timokoesters <timo@koesters.xyz>"]
|
|||
homepage = "https://conduit.rs"
|
||||
repository = "https://gitlab.com/famedly/conduit"
|
||||
readme = "README.md"
|
||||
version = "0.3.0"
|
||||
rust-version = "1.56"
|
||||
version = "0.7.0-alpha"
|
||||
edition = "2021"
|
||||
|
||||
# See also `rust-toolchain.toml`
|
||||
rust-version = "1.75.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
# Used to handle requests
|
||||
# TODO: This can become optional as soon as proper configs are supported
|
||||
# rocket = { git = "https://github.com/SergioBenitez/Rocket.git", rev = "801e04bd5369eb39e126c75f6d11e1e9597304d8", features = ["tls"] } # Used to handle requests
|
||||
rocket = { version = "0.5.0-rc.1", features = ["tls"] } # Used to handle requests
|
||||
# Web framework
|
||||
axum = { version = "0.6.18", default-features = false, features = ["form", "headers", "http1", "http2", "json", "matched-path"], optional = true }
|
||||
axum-server = { version = "0.5.1", features = ["tls-rustls"] }
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
tower-http = { version = "0.4.1", features = ["add-extension", "cors", "sensitive-headers", "trace", "util"] }
|
||||
|
||||
# Used for matrix spec type definitions and helpers
|
||||
#ruma = { version = "0.4.0", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "f7a10a7e471b59d3096be2695c2a05d407d80df1", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
|
||||
#ruma = { git = "https://github.com/timokoesters/ruma", rev = "50c1db7e0a3a21fc794b0cce3b64285a4c750c71", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
|
||||
#ruma = { path = "../ruma/crates/ruma", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
|
||||
ruma = { git = "https://github.com/ruma/ruma", rev = "1a1c61ee1e8f0936e956a3b69c931ce12ee28475", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
|
||||
#ruma = { git = "https://github.com/timokoesters/ruma", rev = "4ec9c69bb7e09391add2382b3ebac97b6e8f4c64", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
|
||||
#ruma = { path = "../ruma/crates/ruma", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
|
||||
|
||||
# Used for long polling and federation sender, should be the same as rocket::tokio
|
||||
tokio = "1.11.0"
|
||||
# Async runtime and utilities
|
||||
tokio = { version = "1.28.1", features = ["fs", "macros", "signal", "sync"] }
|
||||
# Used for storing data permanently
|
||||
sled = { version = "0.34.6", features = ["compression", "no_metrics"], optional = true }
|
||||
#sled = { version = "0.34.7", features = ["compression", "no_metrics"], optional = true }
|
||||
#sled = { git = "https://github.com/spacejam/sled.git", rev = "e4640e0773595229f398438886f19bca6f7326a2", features = ["compression"] }
|
||||
persy = { version = "1.2" , optional = true, features=["background_ops"] }
|
||||
persy = { version = "1.4.4", optional = true, features = ["background_ops"] }
|
||||
|
||||
# Used for the http request / response body type for Ruma endpoints used with reqwest
|
||||
bytes = "1.1.0"
|
||||
# Used for rocket<->ruma conversions
|
||||
http = "0.2.4"
|
||||
bytes = "1.4.0"
|
||||
http = "0.2.9"
|
||||
# Used to find data directory for default db path
|
||||
directories = "3.0.2"
|
||||
directories = "4.0.1"
|
||||
# Used for ruma wrapper
|
||||
serde_json = { version = "1.0.70", features = ["raw_value"] }
|
||||
serde_json = { version = "1.0.96", features = ["raw_value"] }
|
||||
# Used for appservice registration files
|
||||
serde_yaml = "0.8.20"
|
||||
serde_yaml = "0.9.21"
|
||||
# Used for pdu definition
|
||||
serde = { version = "1.0.130", features = ["rc"] }
|
||||
serde = { version = "1.0.163", features = ["rc"] }
|
||||
# Used for secure identifiers
|
||||
rand = "0.8.4"
|
||||
rand = "0.8.5"
|
||||
# Used to hash passwords
|
||||
rust-argon2 = "0.8.3"
|
||||
rust-argon2 = "1.0.0"
|
||||
# Used to send requests
|
||||
reqwest = { default-features = false, features = ["rustls-tls", "socks"], git = "https://github.com/timokoesters/reqwest", rev = "57b7cf4feb921573dfafad7d34b9ac6e44ead0bd" }
|
||||
hyper = "0.14.26"
|
||||
reqwest = { version = "0.11.18", default-features = false, features = ["rustls-tls-native-roots", "socks"] }
|
||||
# Used for conduit::Error type
|
||||
thiserror = "1.0.28"
|
||||
thiserror = "1.0.40"
|
||||
# Used to generate thumbnails for images
|
||||
image = { version = "0.23.14", default-features = false, features = ["jpeg", "png", "gif"] }
|
||||
image = { version = "0.24.6", default-features = false, features = ["jpeg", "png", "gif"] }
|
||||
# Used to encode server public key
|
||||
base64 = "0.13.0"
|
||||
base64 = "0.21.2"
|
||||
# Used when hashing the state
|
||||
ring = "0.16.20"
|
||||
ring = "0.17.7"
|
||||
# Used when querying the SRV record of other servers
|
||||
trust-dns-resolver = "0.20.3"
|
||||
trust-dns-resolver = "0.22.0"
|
||||
# Used to find matching events for appservices
|
||||
regex = "1.5.4"
|
||||
regex = "1.8.1"
|
||||
# jwt jsonwebtokens
|
||||
jsonwebtoken = "7.2.0"
|
||||
jsonwebtoken = "9.2.0"
|
||||
# Performance measurements
|
||||
tracing = { version = "0.1.26", features = ["release_max_level_warn"] }
|
||||
tracing-subscriber = "0.2.20"
|
||||
tracing-flame = "0.1.0"
|
||||
opentelemetry = { version = "0.16.0", features = ["rt-tokio"] }
|
||||
opentelemetry-jaeger = { version = "0.15.0", features = ["rt-tokio"] }
|
||||
tracing = { version = "0.1.37", features = [] }
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
tracing-flame = "0.2.0"
|
||||
opentelemetry = { version = "0.18.0", features = ["rt-tokio"] }
|
||||
opentelemetry-jaeger = { version = "0.17.0", features = ["rt-tokio"] }
|
||||
tracing-opentelemetry = "0.18.0"
|
||||
lru-cache = "0.1.2"
|
||||
rusqlite = { version = "0.25.3", optional = true, features = ["bundled"] }
|
||||
parking_lot = { version = "0.11.2", optional = true }
|
||||
crossbeam = { version = "0.8.1", optional = true }
|
||||
num_cpus = "1.13.0"
|
||||
rusqlite = { version = "0.29.0", optional = true, features = ["bundled"] }
|
||||
parking_lot = { version = "0.12.1", optional = true }
|
||||
# crossbeam = { version = "0.8.2", optional = true }
|
||||
num_cpus = "1.15.0"
|
||||
threadpool = "1.8.1"
|
||||
heed = { git = "https://github.com/timokoesters/heed.git", rev = "f6f825da7fb2c758867e05ad973ef800a6fe1d5d", optional = true }
|
||||
rocksdb = { version = "0.17.0", default-features = false, features = ["multi-threaded-cf", "zstd"], optional = true }
|
||||
# heed = { git = "https://github.com/timokoesters/heed.git", rev = "f6f825da7fb2c758867e05ad973ef800a6fe1d5d", optional = true }
|
||||
# Used for ruma wrapper
|
||||
serde_html_form = "0.2.0"
|
||||
|
||||
thread_local = "1.1.3"
|
||||
rocksdb = { version = "0.21.0", default-features = true, features = ["multi-threaded-cf", "zstd"], optional = true }
|
||||
|
||||
thread_local = "1.1.7"
|
||||
# used for TURN server authentication
|
||||
hmac = "0.11.0"
|
||||
sha-1 = "0.9.8"
|
||||
hmac = "0.12.1"
|
||||
sha-1 = "0.10.1"
|
||||
# used for conduit's CLI and admin room command parsing
|
||||
clap = { version = "3.0.10", default-features = false, features = ["std", "derive"] }
|
||||
maplit = "1.0.2"
|
||||
clap = { version = "4.3.0", default-features = false, features = ["std", "derive", "help", "usage", "error-context"] }
|
||||
futures-util = { version = "0.3.28", default-features = false }
|
||||
# Used for reading the configuration from conduit.toml & environment variables
|
||||
figment = { version = "0.10.8", features = ["env", "toml"] }
|
||||
|
||||
tikv-jemalloc-ctl = { version = "0.4.2", features = ["use_std"], optional = true }
|
||||
tikv-jemallocator = { version = "0.4.1", features = ["unprefixed_malloc_on_supported_platforms"], optional = true }
|
||||
tikv-jemalloc-ctl = { version = "0.5.0", features = ["use_std"], optional = true }
|
||||
tikv-jemallocator = { version = "0.5.0", features = ["unprefixed_malloc_on_supported_platforms"], optional = true }
|
||||
lazy_static = "1.4.0"
|
||||
async-trait = "0.1.68"
|
||||
|
||||
sd-notify = { version = "0.4.1", optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { version = "0.26.2", features = ["resource"] }
|
||||
|
||||
[features]
|
||||
default = ["conduit_bin", "backend_sqlite", "backend_rocksdb", "jemalloc"]
|
||||
backend_sled = ["sled"]
|
||||
default = ["conduit_bin", "backend_sqlite", "backend_rocksdb", "systemd"]
|
||||
#backend_sled = ["sled"]
|
||||
backend_persy = ["persy", "parking_lot"]
|
||||
backend_sqlite = ["sqlite"]
|
||||
backend_heed = ["heed", "crossbeam"]
|
||||
#backend_heed = ["heed", "crossbeam"]
|
||||
backend_rocksdb = ["rocksdb"]
|
||||
jemalloc = ["tikv-jemalloc-ctl", "tikv-jemallocator"]
|
||||
sqlite = ["rusqlite", "parking_lot", "tokio/signal"]
|
||||
conduit_bin = [] # TODO: add rocket to this when it is optional
|
||||
conduit_bin = ["axum"]
|
||||
systemd = ["sd-notify"]
|
||||
|
||||
[[bin]]
|
||||
name = "conduit"
|
||||
|
@ -121,7 +152,7 @@ instead of a server that has high scalability."""
|
|||
section = "net"
|
||||
priority = "optional"
|
||||
assets = [
|
||||
["debian/README.Debian", "usr/share/doc/matrix-conduit/", "644"],
|
||||
["debian/README.md", "usr/share/doc/matrix-conduit/README.Debian", "644"],
|
||||
["README.md", "usr/share/doc/matrix-conduit/", "644"],
|
||||
["target/release/conduit", "usr/sbin/matrix-conduit", "755"],
|
||||
]
|
||||
|
|
23
Cross.toml
23
Cross.toml
|
@ -1,23 +0,0 @@
|
|||
[build.env]
|
||||
# CI uses an S3 endpoint to store sccache artifacts, so their config needs to
|
||||
# be available in the cross container as well
|
||||
passthrough = [
|
||||
"RUSTC_WRAPPER",
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"SCCACHE_BUCKET",
|
||||
"SCCACHE_ENDPOINT",
|
||||
"SCCACHE_S3_USE_SSL",
|
||||
]
|
||||
|
||||
[target.aarch64-unknown-linux-musl]
|
||||
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-aarch64-unknown-linux-musl:latest"
|
||||
|
||||
[target.arm-unknown-linux-musleabihf]
|
||||
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-arm-unknown-linux-musleabihf:latest"
|
||||
|
||||
[target.armv7-unknown-linux-musleabihf]
|
||||
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-armv7-unknown-linux-musleabihf:latest"
|
||||
|
||||
[target.x86_64-unknown-linux-musl]
|
||||
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-x86_64-unknown-linux-musl:latest"
|
205
DEPLOY.md
205
DEPLOY.md
|
@ -2,62 +2,113 @@
|
|||
|
||||
> ## Getting help
|
||||
>
|
||||
> If you run into any problems while setting up Conduit, write an email to `timo@koesters.xyz`, ask us
|
||||
> If you run into any problems while setting up Conduit, write an email to `conduit@koesters.xyz`, ask us
|
||||
> in `#conduit:fachschaften.org` or [open an issue on GitLab](https://gitlab.com/famedly/conduit/-/issues/new).
|
||||
|
||||
## Installing Conduit
|
||||
|
||||
Although you might be able to compile Conduit for Windows, we do recommend running it on a linux server. We therefore
|
||||
Although you might be able to compile Conduit for Windows, we do recommend running it on a Linux server. We therefore
|
||||
only offer Linux binaries.
|
||||
|
||||
You may simply download the binary that fits your machine. Run `uname -m` to see what you need. Now copy the right url:
|
||||
You may simply download the binary that fits your machine. Run `uname -m` to see what you need. Now copy the appropriate url:
|
||||
|
||||
| CPU Architecture | Download stable version | Download development version |
|
||||
| ------------------------------------------- | ------------------------------ | ---------------------------- |
|
||||
| x84_64 / amd64 (Most servers and computers) | [Download][x84_64-musl-master] | [Download][x84_64-musl-next] |
|
||||
| armv6 | [Download][armv6-musl-master] | [Download][armv6-musl-next] |
|
||||
| armv7 (e.g. Raspberry Pi by default) | [Download][armv7-musl-master] | [Download][armv7-musl-next] |
|
||||
| armv8 / aarch64 | [Download][armv8-musl-master] | [Download][armv8-musl-next] |
|
||||
**Stable versions:**
|
||||
|
||||
[x84_64-musl-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/conduit-x86_64-unknown-linux-musl?job=build:release:cargo:x86_64-unknown-linux-musl
|
||||
[armv6-musl-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/conduit-arm-unknown-linux-musleabihf?job=build:release:cargo:arm-unknown-linux-musleabihf
|
||||
[armv7-musl-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/conduit-armv7-unknown-linux-musleabihf?job=build:release:cargo:armv7-unknown-linux-musleabihf
|
||||
[armv8-musl-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/conduit-aarch64-unknown-linux-musl?job=build:release:cargo:aarch64-unknown-linux-musl
|
||||
[x84_64-musl-next]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/next/raw/conduit-x86_64-unknown-linux-musl?job=build:release:cargo:x86_64-unknown-linux-musl
|
||||
[armv6-musl-next]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/next/raw/conduit-arm-unknown-linux-musleabihf?job=build:release:cargo:arm-unknown-linux-musleabihf
|
||||
[armv7-musl-next]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/next/raw/conduit-armv7-unknown-linux-musleabihf?job=build:release:cargo:armv7-unknown-linux-musleabihf
|
||||
[armv8-musl-next]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/next/raw/conduit-aarch64-unknown-linux-musl?job=build:release:cargo:aarch64-unknown-linux-musl
|
||||
| CPU Architecture | Download stable version |
|
||||
| ------------------------------------------- | --------------------------------------------------------------- |
|
||||
| x84_64 / amd64 (Most servers and computers) | [Binary][x84_64-glibc-master] / [.deb][x84_64-glibc-master-deb] |
|
||||
| armv7 (e.g. Raspberry Pi by default) | [Binary][armv7-glibc-master] / [.deb][armv7-glibc-master-deb] |
|
||||
| armv8 / aarch64 | [Binary][armv8-glibc-master] / [.deb][armv8-glibc-master-deb] |
|
||||
|
||||
These builds were created on and linked against the glibc version shipped with Debian bullseye.
|
||||
If you use a system with an older glibc version (e.g. RHEL8), you might need to compile Conduit yourself.
|
||||
|
||||
[x84_64-glibc-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/build-output/linux_amd64/conduit?job=docker:master
|
||||
[armv7-glibc-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/build-output/linux_arm_v7/conduit?job=docker:master
|
||||
[armv8-glibc-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/build-output/linux_arm64/conduit?job=docker:master
|
||||
[x84_64-glibc-master-deb]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/build-output/linux_amd64/conduit.deb?job=docker:master
|
||||
[armv7-glibc-master-deb]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/build-output/linux_arm_v7/conduit.deb?job=docker:master
|
||||
[armv8-glibc-master-deb]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/build-output/linux_arm64/conduit.deb?job=docker:master
|
||||
|
||||
**Latest versions:**
|
||||
|
||||
| Target | Type | Download |
|
||||
|-|-|-|
|
||||
| `x86_64-unknown-linux-gnu` | Dynamically linked Debian package | [link](https://gitlab.com/api/v4/projects/famedly%2Fconduit/jobs/artifacts/next/raw/conduit.deb?job=debian:x86_64-unknown-linux-gnu) |
|
||||
| `x86_64-unknown-linux-musl` | Statically linked binary | [link](https://gitlab.com/api/v4/projects/famedly%2Fconduit/jobs/artifacts/next/raw/conduit?job=static:x86_64-unknown-linux-musl) |
|
||||
| `aarch64-unknown-linux-musl` | Statically linked binary | [link](https://gitlab.com/api/v4/projects/famedly%2Fconduit/jobs/artifacts/next/raw/conduit?job=static:aarch64-unknown-linux-musl) |
|
||||
| `x86_64-unknown-linux-musl` | OCI image | [link](https://gitlab.com/api/v4/projects/famedly%2Fconduit/jobs/artifacts/next/raw/oci-image-amd64.tar.gz?job=oci-image:x86_64-unknown-linux-musl) |
|
||||
| `aarch64-unknown-linux-musl` | OCI image | [link](https://gitlab.com/api/v4/projects/famedly%2Fconduit/jobs/artifacts/next/raw/oci-image-arm64v8.tar.gz?job=oci-image:aarch64-unknown-linux-musl) |
|
||||
|
||||
```bash
|
||||
$ sudo wget -O /usr/local/bin/matrix-conduit <url>
|
||||
$ sudo chmod +x /usr/local/bin/matrix-conduit
|
||||
```
|
||||
|
||||
Alternatively, you may compile the binary yourself
|
||||
Alternatively, you may compile the binary yourself. First, install any dependencies:
|
||||
|
||||
```bash
|
||||
# Debian
|
||||
$ sudo apt install libclang-dev build-essential
|
||||
```
|
||||
|
||||
# RHEL
|
||||
$ sudo dnf install clang
|
||||
```
|
||||
Then, `cd` into the source tree of conduit-next and run:
|
||||
```bash
|
||||
$ cargo build --release
|
||||
```
|
||||
|
||||
Note that this currently requires Rust 1.50.
|
||||
If you want to cross compile Conduit to another architecture, read the guide below.
|
||||
|
||||
If you want to cross compile Conduit to another architecture, read the [Cross-Compile Guide](cross/README.md).
|
||||
<details>
|
||||
<summary>Cross compilation</summary>
|
||||
|
||||
As easiest way to compile conduit for another platform [cross-rs](https://github.com/cross-rs/cross) is recommended, so install it first.
|
||||
|
||||
In order to use RockDB as storage backend append `-latomic` to linker flags.
|
||||
|
||||
For example, to build a binary for Raspberry Pi Zero W (ARMv6) you need `arm-unknown-linux-gnueabihf` as compilation
|
||||
target.
|
||||
|
||||
```bash
|
||||
git clone https://gitlab.com/famedly/conduit.git
|
||||
cd conduit
|
||||
export RUSTFLAGS='-C link-arg=-lgcc -Clink-arg=-latomic -Clink-arg=-static-libgcc'
|
||||
cross build --release --no-default-features --features conduit_bin,backend_rocksdb,jemalloc --target=arm-unknown-linux-gnueabihf
|
||||
```
|
||||
</details>
|
||||
|
||||
## Adding a Conduit user
|
||||
|
||||
While Conduit can run as any user it is usually better to use dedicated users for different services. This also allows
|
||||
you to make sure that the file permissions are correctly set up.
|
||||
|
||||
In Debian you can use this command to create a Conduit user:
|
||||
In Debian or RHEL, you can use this command to create a Conduit user:
|
||||
|
||||
```bash
|
||||
sudo adduser --system conduit --no-create-home
|
||||
sudo adduser --system conduit --group --disabled-login --no-create-home
|
||||
```
|
||||
|
||||
## Forwarding ports in the firewall or the router
|
||||
|
||||
Conduit uses the ports 443 and 8448 both of which need to be open in the firewall.
|
||||
|
||||
If Conduit runs behind a router or in a container and has a different public IP address than the host system these public ports need to be forwarded directly or indirectly to the port mentioned in the config.
|
||||
|
||||
## Optional: Avoid port 8448
|
||||
|
||||
If Conduit runs behind Cloudflare reverse proxy, which doesn't support port 8448 on free plans, [delegation](https://matrix-org.github.io/synapse/latest/delegate.html) can be set up to have federation traffic routed to port 443:
|
||||
```apache
|
||||
# .well-known delegation on Apache
|
||||
<Files "/.well-known/matrix/server">
|
||||
ErrorDocument 200 '{"m.server": "your.server.name:443"}'
|
||||
Header always set Content-Type application/json
|
||||
Header always set Access-Control-Allow-Origin *
|
||||
</Files>
|
||||
```
|
||||
[SRV DNS record](https://spec.matrix.org/latest/server-server-api/#resolving-server-names) delegation is also [possible](https://www.cloudflare.com/en-gb/learning/dns/dns-records/dns-srv-record/).
|
||||
|
||||
## Setting up a systemd service
|
||||
|
||||
Now we'll set up a systemd service for Conduit, so it's easy to start/stop Conduit and set it to autostart when your
|
||||
|
@ -72,7 +123,7 @@ After=network.target
|
|||
[Service]
|
||||
Environment="CONDUIT_CONFIG=/etc/matrix-conduit/conduit.toml"
|
||||
User=conduit
|
||||
Group=nogroup
|
||||
Group=conduit
|
||||
Restart=always
|
||||
ExecStart=/usr/local/bin/matrix-conduit
|
||||
|
||||
|
@ -89,28 +140,35 @@ $ sudo systemctl daemon-reload
|
|||
## Creating the Conduit configuration file
|
||||
|
||||
Now we need to create the Conduit's config file in `/etc/matrix-conduit/conduit.toml`. Paste this in **and take a moment
|
||||
to read it. You need to change at least the server name.**
|
||||
to read it. You need to change at least the server name.**
|
||||
You can also choose to use a different database backend, but right now only `rocksdb` and `sqlite` are recommended.
|
||||
|
||||
```toml
|
||||
[global]
|
||||
# The server_name is the name of this server. It is used as a suffix for user
|
||||
# The server_name is the pretty name of this server. It is used as a suffix for user
|
||||
# and room ids. Examples: matrix.org, conduit.rs
|
||||
# The Conduit server needs to be reachable at https://your.server.name/ on port
|
||||
# 443 (client-server) and 8448 (federation) OR you can create /.well-known
|
||||
# files to redirect requests. See
|
||||
|
||||
# The Conduit server needs all /_matrix/ requests to be reachable at
|
||||
# https://your.server.name/ on port 443 (client-server) and 8448 (federation).
|
||||
|
||||
# If that's not possible for you, you can create /.well-known files to redirect
|
||||
# requests. See
|
||||
# https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client
|
||||
# and https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
|
||||
# and
|
||||
# https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
|
||||
# for more information
|
||||
|
||||
# YOU NEED TO EDIT THIS
|
||||
#server_name = "your.server.name"
|
||||
|
||||
# This is the only directory where Conduit will save its data
|
||||
database_path = "/var/lib/matrix-conduit/conduit_db"
|
||||
database_path = "/var/lib/matrix-conduit/"
|
||||
database_backend = "rocksdb"
|
||||
|
||||
# The port Conduit will be running on. You need to set up a reverse proxy in
|
||||
# your web server (e.g. apache or nginx), so all requests to /_matrix on port
|
||||
# 443 and 8448 will be forwarded to the Conduit instance running on this port
|
||||
# Docker users: Don't change this, you'll need to map an external port to this.
|
||||
port = 6167
|
||||
|
||||
# Max size for uploads
|
||||
|
@ -119,47 +177,51 @@ max_request_size = 20_000_000 # in bytes
|
|||
# Enables registration. If set to false, no users can register on this server.
|
||||
allow_registration = true
|
||||
|
||||
# Disable encryption, so no new encrypted rooms can be created
|
||||
# Note: existing rooms will continue to work
|
||||
allow_encryption = true
|
||||
allow_federation = true
|
||||
allow_check_for_updates = true
|
||||
|
||||
# Server to get public keys from. You probably shouldn't change this
|
||||
trusted_servers = ["matrix.org"]
|
||||
|
||||
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time
|
||||
#workers = 4 # default: cpu core count * 2
|
||||
#log = "warn,state_res=warn,rocket=off,_=off,sled=off"
|
||||
|
||||
address = "127.0.0.1" # This makes sure Conduit can only be reached using the reverse proxy
|
||||
|
||||
# The total amount of memory that the database will use.
|
||||
#db_cache_capacity_mb = 200
|
||||
#address = "0.0.0.0" # If Conduit is running in a container, make sure the reverse proxy (ie. Traefik) can reach it.
|
||||
```
|
||||
|
||||
## Setting the correct file permissions
|
||||
|
||||
As we are using a Conduit specific user we need to allow it to read the config. To do that you can run this command on
|
||||
Debian:
|
||||
Debian or RHEL:
|
||||
|
||||
```bash
|
||||
sudo chown -R conduit:nogroup /etc/matrix-conduit
|
||||
sudo chown -R root:root /etc/matrix-conduit
|
||||
sudo chmod 755 /etc/matrix-conduit
|
||||
```
|
||||
|
||||
If you use the default database path you also need to run this:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/matrix-conduit/conduit_db
|
||||
sudo chown -R conduit:nogroup /var/lib/matrix-conduit/conduit_db
|
||||
sudo mkdir -p /var/lib/matrix-conduit/
|
||||
sudo chown -R conduit:conduit /var/lib/matrix-conduit/
|
||||
sudo chmod 700 /var/lib/matrix-conduit/
|
||||
```
|
||||
|
||||
## Setting up the Reverse Proxy
|
||||
|
||||
This depends on whether you use Apache, Nginx or another web server.
|
||||
This depends on whether you use Apache, Caddy, Nginx or another web server.
|
||||
|
||||
### Apache
|
||||
|
||||
Create `/etc/apache2/sites-enabled/050-conduit.conf` and copy-and-paste this:
|
||||
|
||||
```apache
|
||||
# Requires mod_proxy and mod_proxy_http
|
||||
#
|
||||
# On Apache instance compiled from source,
|
||||
# paste into httpd-ssl.conf or httpd.conf
|
||||
|
||||
Listen 8448
|
||||
|
||||
<VirtualHost *:443 *:8448>
|
||||
|
@ -167,7 +229,7 @@ Listen 8448
|
|||
ServerName your.server.name # EDIT THIS
|
||||
|
||||
AllowEncodedSlashes NoDecode
|
||||
ProxyPass /_matrix/ http://127.0.0.1:6167/_matrix/ nocanon
|
||||
ProxyPass /_matrix/ http://127.0.0.1:6167/_matrix/ timeout=300 nocanon
|
||||
ProxyPassReverse /_matrix/ http://127.0.0.1:6167/_matrix/
|
||||
|
||||
</VirtualHost>
|
||||
|
@ -176,7 +238,27 @@ ProxyPassReverse /_matrix/ http://127.0.0.1:6167/_matrix/
|
|||
**You need to make some edits again.** When you are done, run
|
||||
|
||||
```bash
|
||||
# Debian
|
||||
$ sudo systemctl reload apache2
|
||||
|
||||
# Installed from source
|
||||
$ sudo apachectl -k graceful
|
||||
```
|
||||
|
||||
### Caddy
|
||||
|
||||
Create `/etc/caddy/conf.d/conduit_caddyfile` and enter this (substitute for your server name).
|
||||
|
||||
```caddy
|
||||
your.server.name, your.server.name:8448 {
|
||||
reverse_proxy /_matrix/* 127.0.0.1:6167
|
||||
}
|
||||
```
|
||||
|
||||
That's it! Just start or enable the service and you're set.
|
||||
|
||||
```bash
|
||||
$ sudo systemctl enable caddy
|
||||
```
|
||||
|
||||
### Nginx
|
||||
|
@ -192,10 +274,15 @@ server {
|
|||
server_name your.server.name; # EDIT THIS
|
||||
merge_slashes off;
|
||||
|
||||
# Nginx defaults to only allow 1MB uploads
|
||||
# Increase this to allow posting large files such as videos
|
||||
client_max_body_size 20M;
|
||||
|
||||
location /_matrix/ {
|
||||
proxy_pass http://127.0.0.1:6167$request_uri;
|
||||
proxy_pass http://127.0.0.1:6167;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 5m;
|
||||
}
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/your.server.name/fullchain.pem; # EDIT THIS
|
||||
|
@ -213,11 +300,21 @@ $ sudo systemctl reload nginx
|
|||
|
||||
## SSL Certificate
|
||||
|
||||
The easiest way to get an SSL certificate, if you don't have one already, is to install `certbot` and run this:
|
||||
If you chose Caddy as your web proxy SSL certificates are handled automatically and you can skip this step.
|
||||
|
||||
The easiest way to get an SSL certificate, if you don't have one already, is to [install](https://certbot.eff.org/instructions) `certbot` and run this:
|
||||
|
||||
```bash
|
||||
# To use ECC for the private key,
|
||||
# paste into /etc/letsencrypt/cli.ini:
|
||||
# key-type = ecdsa
|
||||
# elliptic-curve = secp384r1
|
||||
|
||||
$ sudo certbot -d your.server.name
|
||||
```
|
||||
[Automated renewal](https://eff-certbot.readthedocs.io/en/stable/using.html#automated-renewals) is usually preconfigured.
|
||||
|
||||
If using Cloudflare, configure instead the edge and origin certificates in dashboard. In case you’re already running a website on the same Apache server, you can just copy-and-paste the SSL configuration from your main virtual host on port 443 into the above-mentioned vhost.
|
||||
|
||||
## You're done!
|
||||
|
||||
|
@ -241,8 +338,20 @@ You can also use these commands as a quick health check.
|
|||
|
||||
```bash
|
||||
$ curl https://your.server.name/_matrix/client/versions
|
||||
|
||||
# If using port 8448
|
||||
$ curl https://your.server.name:8448/_matrix/client/versions
|
||||
```
|
||||
|
||||
- To check if your server can talk with other homeservers, you can use the [Matrix Federation Tester](https://federationtester.matrix.org/)
|
||||
- If you want to set up an appservice, take a look at the [Appservice Guide](APPSERVICES.md).
|
||||
- To check if your server can talk with other homeservers, you can use the [Matrix Federation Tester](https://federationtester.matrix.org/).
|
||||
If you can register but cannot join federated rooms check your config again and also check if the port 8448 is open and forwarded correctly.
|
||||
|
||||
# What's next?
|
||||
|
||||
## Audio/Video calls
|
||||
|
||||
For Audio/Video call functionality see the [TURN Guide](TURN.md).
|
||||
|
||||
## Appservices
|
||||
|
||||
If you want to set up an appservice, take a look at the [Appservice Guide](APPSERVICES.md).
|
||||
|
|
80
Dockerfile
80
Dockerfile
|
@ -1,80 +0,0 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM docker.io/rust:1.58-bullseye AS builder
|
||||
WORKDIR /usr/src/conduit
|
||||
|
||||
# Install required packages to build Conduit and it's dependencies
|
||||
RUN apt update && apt -y install libclang-dev
|
||||
|
||||
# == Build dependencies without our own code separately for caching ==
|
||||
#
|
||||
# Need a fake main.rs since Cargo refuses to build anything otherwise.
|
||||
#
|
||||
# See https://github.com/rust-lang/cargo/issues/2644 for a Cargo feature
|
||||
# request that would allow just dependencies to be compiled, presumably
|
||||
# regardless of whether source files are available.
|
||||
RUN mkdir src && touch src/lib.rs && echo 'fn main() {}' > src/main.rs
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
RUN cargo build --release && rm -r src
|
||||
|
||||
# Copy over actual Conduit sources
|
||||
COPY src src
|
||||
|
||||
# main.rs and lib.rs need their timestamp updated for this to work correctly since
|
||||
# otherwise the build with the fake main.rs from above is newer than the
|
||||
# source files (COPY preserves timestamps).
|
||||
#
|
||||
# Builds conduit and places the binary at /usr/src/conduit/target/release/conduit
|
||||
RUN touch src/main.rs && touch src/lib.rs && cargo build --release
|
||||
|
||||
# ---------------------------------------------------------------------------------------------------------------
|
||||
# Stuff below this line actually ends up in the resulting docker image
|
||||
# ---------------------------------------------------------------------------------------------------------------
|
||||
FROM docker.io/debian:bullseye-slim AS runner
|
||||
|
||||
# Standard port on which Conduit launches.
|
||||
# You still need to map the port when using the docker command or docker-compose.
|
||||
EXPOSE 6167
|
||||
|
||||
# Note from @jfowl: I would like to remove the config file in the future and just have the Docker version be configured with envs.
|
||||
ENV CONDUIT_CONFIG="/srv/conduit/conduit.toml" \
|
||||
CONDUIT_PORT=6167
|
||||
|
||||
# Conduit needs:
|
||||
# ca-certificates: for https
|
||||
# iproute2 & wget: for the healthcheck script
|
||||
RUN apt update && apt -y install \
|
||||
ca-certificates \
|
||||
iproute2 \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Created directory for the database and media files
|
||||
RUN mkdir -p /srv/conduit/.local/share/conduit
|
||||
|
||||
# Test if Conduit is still alive, uses the same endpoint as Element
|
||||
COPY ./docker/healthcheck.sh /srv/conduit/healthcheck.sh
|
||||
HEALTHCHECK --start-period=5s --interval=5s CMD ./healthcheck.sh
|
||||
|
||||
# Copy over the actual Conduit binary from the builder stage
|
||||
COPY --from=builder /usr/src/conduit/target/release/conduit /srv/conduit/conduit
|
||||
|
||||
# Improve security: Don't run stuff as root, that does not need to run as root
|
||||
# Add 'conduit' user and group (100:82). The UID:GID choice is to be compatible
|
||||
# with previous, Alpine-based containers, where the user and group were both
|
||||
# named 'www-data'.
|
||||
RUN set -x ; \
|
||||
groupadd -r -g 82 conduit ; \
|
||||
useradd -r -M -d /srv/conduit -o -u 100 -g conduit conduit && exit 0 ; exit 1
|
||||
|
||||
# Change ownership of Conduit files to conduit user and group and make the healthcheck executable:
|
||||
RUN chown -cR conduit:conduit /srv/conduit && \
|
||||
chmod +x /srv/conduit/healthcheck.sh
|
||||
|
||||
# Change user to conduit, no root permissions afterwards:
|
||||
USER conduit
|
||||
# Set container home directory
|
||||
WORKDIR /srv/conduit
|
||||
|
||||
# Run Conduit and print backtraces on panics
|
||||
ENV RUST_BACKTRACE=1
|
||||
ENTRYPOINT [ "/srv/conduit/conduit" ]
|
34
README.md
34
README.md
|
@ -1,7 +1,12 @@
|
|||
# Conduit
|
||||
|
||||
### A Matrix homeserver written in Rust
|
||||
|
||||
#### What is Matrix?
|
||||
[Matrix](https://matrix.org) is an open network for secure and decentralized
|
||||
communication. Users from every Matrix homeserver can chat with users from all
|
||||
other Matrix servers. You can even use bridges (also called Matrix appservices)
|
||||
to communicate with users outside of Matrix, like a community on Discord.
|
||||
|
||||
#### What is the goal?
|
||||
|
||||
An efficient Matrix homeserver that's easy to set up and just works. You can install
|
||||
|
@ -11,29 +16,28 @@ friends or company.
|
|||
#### Can I try it out?
|
||||
|
||||
Yes! You can test our Conduit instance by opening a Matrix client (<https://app.element.io> or Element Android for
|
||||
example) and registering on the `conduit.rs` homeserver.
|
||||
example) and registering on the `conduit.rs` homeserver. The registration token is "for_testing_only". Don't share personal information.
|
||||
|
||||
It is hosted on a ODROID HC 2 with 2GB RAM and a SAMSUNG Exynos 5422 CPU, which
|
||||
was used in the Samsung Galaxy S5. It joined many big rooms including Matrix
|
||||
HQ.
|
||||
Server hosting for conduit.rs is donated by the Matrix.org Foundation.
|
||||
|
||||
#### What is the current status?
|
||||
|
||||
As of 2021-09-01, Conduit is Beta, meaning you can join and participate in most
|
||||
Conduit is Beta, meaning you can join and participate in most
|
||||
Matrix rooms, but not all features are supported and you might run into bugs
|
||||
from time to time.
|
||||
|
||||
There are still a few important features missing:
|
||||
|
||||
- E2EE verification over federation
|
||||
- Outgoing read receipts, typing, presence over federation
|
||||
- E2EE emoji comparison over federation (E2EE chat works)
|
||||
- Outgoing read receipts, typing, presence over federation (incoming works)
|
||||
|
||||
Check out the [Conduit 1.0 Release Milestone](https://gitlab.com/famedly/conduit/-/milestones/3).
|
||||
|
||||
#### How can I deploy my own?
|
||||
|
||||
- Simple install (this was tested the most): [DEPLOY.md](DEPLOY.md)
|
||||
- Debian package: [debian/README.Debian](debian/README.Debian)
|
||||
- Debian package: [debian/README.md](debian/README.md)
|
||||
- Nix/NixOS: [nix/README.md](nix/README.md)
|
||||
- Docker: [docker/README.md](docker/README.md)
|
||||
|
||||
If you want to connect an Appservice to Conduit, take a look at [APPSERVICES.md](APPSERVICES.md).
|
||||
|
@ -49,12 +53,20 @@ If you want to connect an Appservice to Conduit, take a look at [APPSERVICES.md]
|
|||
|
||||
#### Thanks to
|
||||
|
||||
Thanks to Famedly, Prototype Fund (DLR and German BMBF) and all other individuals for financially supporting this project.
|
||||
Thanks to FUTO, Famedly, Prototype Fund (DLR and German BMBF) and all individuals for financially supporting this project.
|
||||
|
||||
Thanks to the contributors to Conduit and all libraries we use, for example:
|
||||
|
||||
- Ruma: A clean library for the Matrix Spec in Rust
|
||||
- Rocket: A flexible web framework
|
||||
- axum: A modular web framework
|
||||
|
||||
#### Contact
|
||||
|
||||
If you run into any question, feel free to
|
||||
- Ask us in `#conduit:fachschaften.org` on Matrix
|
||||
- Write an E-Mail to `conduit@koesters.xyz`
|
||||
- Send an direct message to `timokoesters@fachschaften.org` on Matrix
|
||||
- [Open an issue on GitLab](https://gitlab.com/famedly/conduit/-/issues/new)
|
||||
|
||||
#### Donate
|
||||
|
||||
|
|
25
TURN.md
Normal file
25
TURN.md
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Setting up TURN/STURN
|
||||
|
||||
## General instructions
|
||||
|
||||
* It is assumed you have a [Coturn server](https://github.com/coturn/coturn) up and running. See [Synapse reference implementation](https://github.com/matrix-org/synapse/blob/develop/docs/turn-howto.md).
|
||||
|
||||
## Edit/Add a few settings to your existing conduit.toml
|
||||
|
||||
```
|
||||
# Refer to your Coturn settings.
|
||||
# `your.turn.url` has to match the REALM setting of your Coturn as well as `transport`.
|
||||
turn_uris = ["turn:your.turn.url?transport=udp", "turn:your.turn.url?transport=tcp"]
|
||||
|
||||
# static-auth-secret of your turnserver
|
||||
turn_secret = "ADD SECRET HERE"
|
||||
|
||||
# If you have your TURN server configured to use a username and password
|
||||
# you can provide these information too. In this case comment out `turn_secret above`!
|
||||
#turn_username = ""
|
||||
#turn_password = ""
|
||||
```
|
||||
|
||||
## Apply settings
|
||||
|
||||
Restart Conduit.
|
37
bin/complement
Executable file
37
bin/complement
Executable file
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Path to Complement's source code
|
||||
COMPLEMENT_SRC="$1"
|
||||
|
||||
# A `.jsonl` file to write test logs to
|
||||
LOG_FILE="$2"
|
||||
|
||||
# A `.jsonl` file to write test results to
|
||||
RESULTS_FILE="$3"
|
||||
|
||||
OCI_IMAGE="complement-conduit:dev"
|
||||
|
||||
env \
|
||||
-C "$(git rev-parse --show-toplevel)" \
|
||||
docker build \
|
||||
--tag "$OCI_IMAGE" \
|
||||
--file complement/Dockerfile \
|
||||
.
|
||||
|
||||
# It's okay (likely, even) that `go test` exits nonzero
|
||||
set +o pipefail
|
||||
env \
|
||||
-C "$COMPLEMENT_SRC" \
|
||||
COMPLEMENT_BASE_IMAGE="$OCI_IMAGE" \
|
||||
go test -json ./tests | tee "$LOG_FILE"
|
||||
set -o pipefail
|
||||
|
||||
# Post-process the results into an easy-to-compare format
|
||||
cat "$LOG_FILE" | jq -c '
|
||||
select(
|
||||
(.Action == "pass" or .Action == "fail" or .Action == "skip")
|
||||
and .Test != null
|
||||
) | {Action: .Action, Test: .Test}
|
||||
' | sort > "$RESULTS_FILE"
|
31
bin/nix-build-and-cache
Executable file
31
bin/nix-build-and-cache
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# The first argument must be the desired installable
|
||||
INSTALLABLE="$1"
|
||||
|
||||
# Build the installable and forward any other arguments too
|
||||
nix build "$@"
|
||||
|
||||
if [ ! -z ${ATTIC_TOKEN+x} ]; then
|
||||
|
||||
nix run --inputs-from . attic -- login \
|
||||
conduit \
|
||||
https://nix.computer.surgery/conduit \
|
||||
"$ATTIC_TOKEN"
|
||||
|
||||
push_args=(
|
||||
# Attic and its build dependencies
|
||||
"$(nix path-info --inputs-from . attic)"
|
||||
"$(nix path-info --inputs-from . attic --derivation)"
|
||||
|
||||
# The target installable and its build dependencies
|
||||
"$(nix path-info "$INSTALLABLE" --derivation)"
|
||||
"$(nix path-info "$INSTALLABLE")"
|
||||
)
|
||||
|
||||
nix run --inputs-from . attic -- push conduit "${push_args[@]}"
|
||||
else
|
||||
echo "\$ATTIC_TOKEN is unset, skipping uploading to the binary cache"
|
||||
fi
|
45
complement/Dockerfile
Normal file
45
complement/Dockerfile
Normal file
|
@ -0,0 +1,45 @@
|
|||
FROM rust:1.75.0
|
||||
|
||||
WORKDIR /workdir
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libclang-dev
|
||||
|
||||
COPY Cargo.toml Cargo.toml
|
||||
COPY Cargo.lock Cargo.lock
|
||||
COPY src src
|
||||
RUN cargo build --release \
|
||||
&& mv target/release/conduit conduit \
|
||||
&& rm -rf target
|
||||
|
||||
# Install caddy
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y \
|
||||
debian-keyring \
|
||||
debian-archive-keyring \
|
||||
apt-transport-https \
|
||||
curl \
|
||||
&& curl -1sLf 'https://dl.cloudsmith.io/public/caddy/testing/gpg.key' \
|
||||
| gpg --dearmor -o /usr/share/keyrings/caddy-testing-archive-keyring.gpg \
|
||||
&& curl -1sLf 'https://dl.cloudsmith.io/public/caddy/testing/debian.deb.txt' \
|
||||
| tee /etc/apt/sources.list.d/caddy-testing.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y caddy
|
||||
|
||||
COPY conduit-example.toml conduit.toml
|
||||
COPY complement/caddy.json caddy.json
|
||||
|
||||
ENV SERVER_NAME=localhost
|
||||
ENV CONDUIT_CONFIG=/workdir/conduit.toml
|
||||
|
||||
RUN sed -i "s/port = 6167/port = 8008/g" conduit.toml
|
||||
RUN echo "log = \"warn,_=off,sled=off\"" >> conduit.toml
|
||||
RUN sed -i "s/address = \"127.0.0.1\"/address = \"0.0.0.0\"/g" conduit.toml
|
||||
|
||||
EXPOSE 8008 8448
|
||||
|
||||
CMD uname -a && \
|
||||
sed -i "s/#server_name = \"your.server.name\"/server_name = \"${SERVER_NAME}\"/g" conduit.toml && \
|
||||
sed -i "s/your.server.name/${SERVER_NAME}/g" caddy.json && \
|
||||
caddy start --config caddy.json > /dev/null && \
|
||||
/workdir/conduit
|
11
complement/README.md
Normal file
11
complement/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Complement
|
||||
|
||||
## What's that?
|
||||
|
||||
Have a look at [its repository](https://github.com/matrix-org/complement).
|
||||
|
||||
## How do I use it with Conduit?
|
||||
|
||||
The script at [`../bin/complement`](../bin/complement) has automation for this.
|
||||
It takes a few command line arguments, you can read the script to find out what
|
||||
those are.
|
72
complement/caddy.json
Normal file
72
complement/caddy.json
Normal file
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"logging": {
|
||||
"logs": {
|
||||
"default": {
|
||||
"level": "WARN"
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps": {
|
||||
"http": {
|
||||
"https_port": 8448,
|
||||
"servers": {
|
||||
"srv0": {
|
||||
"listen": [":8448"],
|
||||
"routes": [{
|
||||
"match": [{
|
||||
"host": ["your.server.name"]
|
||||
}],
|
||||
"handle": [{
|
||||
"handler": "subroute",
|
||||
"routes": [{
|
||||
"handle": [{
|
||||
"handler": "reverse_proxy",
|
||||
"upstreams": [{
|
||||
"dial": "127.0.0.1:8008"
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}],
|
||||
"terminal": true
|
||||
}],
|
||||
"tls_connection_policies": [{
|
||||
"match": {
|
||||
"sni": ["your.server.name"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"pki": {
|
||||
"certificate_authorities": {
|
||||
"local": {
|
||||
"name": "Complement CA",
|
||||
"root": {
|
||||
"certificate": "/complement/ca/ca.crt",
|
||||
"private_key": "/complement/ca/ca.key"
|
||||
},
|
||||
"intermediate": {
|
||||
"certificate": "/complement/ca/ca.crt",
|
||||
"private_key": "/complement/ca/ca.key"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"automation": {
|
||||
"policies": [{
|
||||
"subjects": ["your.server.name"],
|
||||
"issuers": [{
|
||||
"module": "internal"
|
||||
}],
|
||||
"on_demand": true
|
||||
}, {
|
||||
"issuers": [{
|
||||
"module": "internal",
|
||||
"ca": "local"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,10 @@
|
|||
# =============================================================================
|
||||
# This is the official example config for Conduit.
|
||||
# If you use it for your server, you will need to adjust it to your own needs.
|
||||
# At the very least, change the server_name field!
|
||||
# =============================================================================
|
||||
|
||||
|
||||
[global]
|
||||
# The server_name is the pretty name of this server. It is used as a suffix for user
|
||||
# and room ids. Examples: matrix.org, conduit.rs
|
||||
|
@ -16,7 +23,7 @@
|
|||
#server_name = "your.server.name"
|
||||
|
||||
# This is the only directory where Conduit will save its data
|
||||
database_path = "/var/lib/conduit/"
|
||||
database_path = "/var/lib/matrix-conduit/"
|
||||
database_backend = "rocksdb"
|
||||
|
||||
# The port Conduit will be running on. You need to set up a reverse proxy in
|
||||
|
@ -31,24 +38,20 @@ max_request_size = 20_000_000 # in bytes
|
|||
# Enables registration. If set to false, no users can register on this server.
|
||||
allow_registration = true
|
||||
|
||||
# Disable encryption, so no new encrypted rooms can be created
|
||||
# Note: existing rooms will continue to work
|
||||
#allow_encryption = false
|
||||
#allow_federation = false
|
||||
allow_federation = true
|
||||
allow_check_for_updates = true
|
||||
|
||||
# Enable jaeger to support monitoring and troubleshooting through jaeger
|
||||
#allow_jaeger = false
|
||||
# Enable the display name lightning bolt on registration.
|
||||
enable_lightning_bolt = true
|
||||
|
||||
# Servers listed here will be used to gather public keys of other servers.
|
||||
# Generally, copying this exactly should be enough. (Currently, Conduit doesn't
|
||||
# support batched key requests, so this list should only contain Synapse
|
||||
# servers.)
|
||||
trusted_servers = ["matrix.org"]
|
||||
|
||||
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time
|
||||
#log = "info,state_res=warn,rocket=off,_=off,sled=off"
|
||||
#workers = 4 # default: cpu core count * 2
|
||||
#log = "warn,state_res=warn,rocket=off,_=off,sled=off"
|
||||
|
||||
address = "127.0.0.1" # This makes sure Conduit can only be reached using the reverse proxy
|
||||
#address = "0.0.0.0" # If Conduit is running in a container, make sure the reverse proxy (ie. Traefik) can reach it.
|
||||
|
||||
proxy = "none" # more examples can be found at src/database/proxy.rs:6
|
||||
|
||||
# The total amount of memory that the database will use.
|
||||
#db_cache_capacity_mb = 200
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
## Cross compilation
|
||||
|
||||
The `cross` folder contains a set of convenience scripts (`build.sh` and `test.sh`) for cross-compiling Conduit.
|
||||
|
||||
Currently supported targets are
|
||||
|
||||
- aarch64-unknown-linux-musl
|
||||
- arm-unknown-linux-musleabihf
|
||||
- armv7-unknown-linux-musleabihf
|
||||
- x86\_64-unknown-linux-musl
|
||||
|
||||
### Install prerequisites
|
||||
#### Docker
|
||||
[Installation guide](https://docs.docker.com/get-docker/).
|
||||
```sh
|
||||
$ sudo apt install docker
|
||||
$ sudo systemctl start docker
|
||||
$ sudo usermod -aG docker $USER
|
||||
$ newgrp docker
|
||||
```
|
||||
|
||||
#### Cross
|
||||
[Installation guide](https://github.com/rust-embedded/cross/#installation).
|
||||
```sh
|
||||
$ cargo install cross
|
||||
```
|
||||
|
||||
### Buiding Conduit
|
||||
```sh
|
||||
$ TARGET=armv7-unknown-linux-musleabihf ./cross/build.sh --release
|
||||
```
|
||||
The cross-compiled binary is at `target/armv7-unknown-linux-musleabihf/release/conduit`
|
||||
|
||||
### Testing Conduit
|
||||
```sh
|
||||
$ TARGET=armv7-unknown-linux-musleabihf ./cross/test.sh --release
|
||||
```
|
18
debian/README.Debian → debian/README.md
vendored
18
debian/README.Debian → debian/README.md
vendored
|
@ -1,28 +1,36 @@
|
|||
Conduit for Debian
|
||||
==================
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Information about downloading, building and deploying the Debian package, see
|
||||
the "Installing Conduit" section in [DEPLOY.md](../DEPLOY.md).
|
||||
All following sections until "Setting up the Reverse Proxy" be ignored because
|
||||
this is handled automatically by the packaging.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
When installed, Debconf generates the configuration of the homeserver
|
||||
(host)name, the address and port it listens on. This configuration ends up in
|
||||
/etc/matrix-conduit/conduit.toml.
|
||||
`/etc/matrix-conduit/conduit.toml`.
|
||||
|
||||
You can tweak more detailed settings by uncommenting and setting the variables
|
||||
in /etc/matrix-conduit/conduit.toml. This involves settings such as the maximum
|
||||
in `/etc/matrix-conduit/conduit.toml`. This involves settings such as the maximum
|
||||
file size for download/upload, enabling federation, etc.
|
||||
|
||||
Running
|
||||
-------
|
||||
|
||||
The package uses the matrix-conduit.service systemd unit file to start and
|
||||
The package uses the `matrix-conduit.service` systemd unit file to start and
|
||||
stop Conduit. It loads the configuration file mentioned above to set up the
|
||||
environment before running the server.
|
||||
|
||||
This package assumes by default that Conduit will be placed behind a reverse
|
||||
proxy such as Apache or nginx. This default deployment entails just listening
|
||||
on 127.0.0.1 and the free port 6167 and is reachable via a client using the URL
|
||||
http://localhost:6167.
|
||||
on `127.0.0.1` and the free port `6167` and is reachable via a client using the URL
|
||||
<http://localhost:6167>.
|
||||
|
||||
At a later stage this packaging may support also setting up TLS and running
|
||||
stand-alone. In this case, however, you need to set up some certificates and
|
1
debian/matrix-conduit.service
vendored
1
debian/matrix-conduit.service
vendored
|
@ -3,6 +3,7 @@ Description=Conduit Matrix homeserver
|
|||
After=network.target
|
||||
|
||||
[Service]
|
||||
DynamicUser=yes
|
||||
User=_matrix-conduit
|
||||
Group=_matrix-conduit
|
||||
Type=simple
|
||||
|
|
50
debian/postinst
vendored
50
debian/postinst
vendored
|
@ -5,7 +5,7 @@ set -e
|
|||
|
||||
CONDUIT_CONFIG_PATH=/etc/matrix-conduit
|
||||
CONDUIT_CONFIG_FILE="${CONDUIT_CONFIG_PATH}/conduit.toml"
|
||||
CONDUIT_DATABASE_PATH=/var/lib/matrix-conduit/conduit_db
|
||||
CONDUIT_DATABASE_PATH=/var/lib/matrix-conduit/
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
|
@ -19,11 +19,11 @@ case "$1" in
|
|||
_matrix-conduit
|
||||
fi
|
||||
|
||||
# Create the database path if it does not exist yet.
|
||||
if [ ! -d "$CONDUIT_DATABASE_PATH" ]; then
|
||||
mkdir -p "$CONDUIT_DATABASE_PATH"
|
||||
chown _matrix-conduit "$CONDUIT_DATABASE_PATH"
|
||||
fi
|
||||
# Create the database path if it does not exist yet and fix up ownership
|
||||
# and permissions.
|
||||
mkdir -p "$CONDUIT_DATABASE_PATH"
|
||||
chown _matrix-conduit "$CONDUIT_DATABASE_PATH"
|
||||
chmod 700 "$CONDUIT_DATABASE_PATH"
|
||||
|
||||
if [ ! -e "$CONDUIT_CONFIG_FILE" ]; then
|
||||
# Write the debconf values in the config.
|
||||
|
@ -36,18 +36,24 @@ case "$1" in
|
|||
mkdir -p "$CONDUIT_CONFIG_PATH"
|
||||
cat > "$CONDUIT_CONFIG_FILE" << EOF
|
||||
[global]
|
||||
# The server_name is the name of this server. It is used as a suffix for user
|
||||
# and room ids. Examples: matrix.org, conduit.rs
|
||||
# The Conduit server needs to be reachable at https://your.server.name/ on port
|
||||
# 443 (client-server) and 8448 (federation) OR you can create /.well-known
|
||||
# files to redirect requests. See
|
||||
# The server_name is the pretty name of this server. It is used as a suffix for
|
||||
# user and room ids. Examples: matrix.org, conduit.rs
|
||||
|
||||
# The Conduit server needs all /_matrix/ requests to be reachable at
|
||||
# https://your.server.name/ on port 443 (client-server) and 8448 (federation).
|
||||
|
||||
# If that's not possible for you, you can create /.well-known files to redirect
|
||||
# requests. See
|
||||
# https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client
|
||||
# and https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
|
||||
# for more information.
|
||||
# and
|
||||
# https://matrix.org/docs/spec/server_server/r0.1.4#get-well-known-matrix-server
|
||||
# for more information
|
||||
|
||||
server_name = "${CONDUIT_SERVER_NAME}"
|
||||
|
||||
# This is the only directory where Conduit will save its data.
|
||||
database_path = "${CONDUIT_DATABASE_PATH}"
|
||||
database_backend = "rocksdb"
|
||||
|
||||
# The address Conduit will be listening on.
|
||||
# By default the server listens on address 0.0.0.0. Change this to 127.0.0.1 to
|
||||
|
@ -56,7 +62,8 @@ address = "${CONDUIT_ADDRESS}"
|
|||
|
||||
# The port Conduit will be running on. You need to set up a reverse proxy in
|
||||
# your web server (e.g. apache or nginx), so all requests to /_matrix on port
|
||||
# 443 and 8448 will be forwarded to the Conduit instance running on this port.
|
||||
# 443 and 8448 will be forwarded to the Conduit instance running on this port
|
||||
# Docker users: Don't change this, you'll need to map an external port to this.
|
||||
port = ${CONDUIT_PORT}
|
||||
|
||||
# Max size for uploads
|
||||
|
@ -65,20 +72,13 @@ max_request_size = 20_000_000 # in bytes
|
|||
# Enables registration. If set to false, no users can register on this server.
|
||||
allow_registration = true
|
||||
|
||||
# Disable encryption, so no new encrypted rooms can be created.
|
||||
# Note: Existing rooms will continue to work.
|
||||
#allow_encryption = false
|
||||
#allow_federation = false
|
||||
allow_federation = true
|
||||
allow_check_for_updates = true
|
||||
|
||||
# Enable jaeger to support monitoring and troubleshooting through jaeger.
|
||||
#allow_jaeger = false
|
||||
trusted_servers = ["matrix.org"]
|
||||
|
||||
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time
|
||||
#log = "info,state_res=warn,rocket=off,_=off,sled=off"
|
||||
#workers = 4 # default: cpu core count * 2
|
||||
|
||||
# The total amount of memory that the database will use.
|
||||
#db_cache_capacity_mb = 200
|
||||
#log = "warn,state_res=warn,rocket=off,_=off,sled=off"
|
||||
EOF
|
||||
fi
|
||||
;;
|
||||
|
|
10
default.nix
Normal file
10
default.nix
Normal file
|
@ -0,0 +1,10 @@
|
|||
(import
|
||||
(
|
||||
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
|
||||
fetchTarball {
|
||||
url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}
|
||||
)
|
||||
{ src = ./.; }
|
||||
).defaultNix
|
153
docker/README.md
153
docker/README.md
|
@ -4,7 +4,36 @@
|
|||
|
||||
## Docker
|
||||
|
||||
### Build & Dockerfile
|
||||
To run Conduit with Docker you can either build the image yourself or pull it from a registry.
|
||||
|
||||
|
||||
### Use a registry
|
||||
|
||||
OCI images for Conduit are available in the registries listed below. We recommend using the image tagged as `latest` from GitLab's own registry.
|
||||
|
||||
| Registry | Image | Size | Notes |
|
||||
| --------------- | --------------------------------------------------------------- | ----------------------------- | ---------------------- |
|
||||
| GitLab Registry | [registry.gitlab.com/famedly/conduit/matrix-conduit:latest][gl] | ![Image Size][shield-latest] | Stable image. |
|
||||
| Docker Hub | [docker.io/matrixconduit/matrix-conduit:latest][dh] | ![Image Size][shield-latest] | Stable image. |
|
||||
| GitLab Registry | [registry.gitlab.com/famedly/conduit/matrix-conduit:next][gl] | ![Image Size][shield-next] | Development version. |
|
||||
| Docker Hub | [docker.io/matrixconduit/matrix-conduit:next][dh] | ![Image Size][shield-next] | Development version. |
|
||||
|
||||
|
||||
[dh]: https://hub.docker.com/r/matrixconduit/matrix-conduit
|
||||
[gl]: https://gitlab.com/famedly/conduit/container_registry/2497937
|
||||
[shield-latest]: https://img.shields.io/docker/image-size/matrixconduit/matrix-conduit/latest
|
||||
[shield-next]: https://img.shields.io/docker/image-size/matrixconduit/matrix-conduit/next
|
||||
|
||||
|
||||
Use
|
||||
```bash
|
||||
docker image pull <link>
|
||||
```
|
||||
to pull it to your machine.
|
||||
|
||||
|
||||
|
||||
### Build using a dockerfile
|
||||
|
||||
The Dockerfile provided by Conduit has two stages, each of which creates an image.
|
||||
|
||||
|
@ -19,24 +48,27 @@ docker build --tag matrixconduit/matrix-conduit:latest .
|
|||
|
||||
which also will tag the resulting image as `matrixconduit/matrix-conduit:latest`.
|
||||
|
||||
|
||||
|
||||
### Run
|
||||
|
||||
After building the image you can simply run it with
|
||||
When you have the image you can simply run it with
|
||||
|
||||
```bash
|
||||
docker run -d -p 8448:6167 -v ~/conduit.toml:/srv/conduit/conduit.toml -v db:/srv/conduit/.local/share/conduit matrixconduit/matrix-conduit:latest
|
||||
docker run -d -p 8448:6167 \
|
||||
-v db:/var/lib/matrix-conduit/ \
|
||||
-e CONDUIT_SERVER_NAME="your.server.name" \
|
||||
-e CONDUIT_DATABASE_BACKEND="rocksdb" \
|
||||
-e CONDUIT_ALLOW_REGISTRATION=true \
|
||||
-e CONDUIT_ALLOW_FEDERATION=true \
|
||||
-e CONDUIT_MAX_REQUEST_SIZE="20_000_000" \
|
||||
-e CONDUIT_TRUSTED_SERVERS="[\"matrix.org\"]" \
|
||||
-e CONDUIT_MAX_CONCURRENT_REQUESTS="100" \
|
||||
-e CONDUIT_LOG="warn,rocket=off,_=off,sled=off" \
|
||||
--name conduit <link>
|
||||
```
|
||||
|
||||
or you can skip the build step and pull the image from one of the following registries:
|
||||
|
||||
| Registry | Image | Size |
|
||||
| --------------- | --------------------------------------------------------------- | --------------------- |
|
||||
| Docker Hub | [matrixconduit/matrix-conduit:latest][dh] | ![Image Size][shield] |
|
||||
| GitLab Registry | [registry.gitlab.com/famedly/conduit/matrix-conduit:latest][gl] | ![Image Size][shield] |
|
||||
|
||||
[dh]: https://hub.docker.com/r/matrixconduit/matrix-conduit
|
||||
[gl]: https://gitlab.com/famedly/conduit/container_registry/2497937
|
||||
[shield]: https://img.shields.io/docker/image-size/matrixconduit/matrix-conduit/latest
|
||||
or you can use [docker-compose](#docker-compose).
|
||||
|
||||
The `-d` flag lets the container run in detached mode. You now need to supply a `conduit.toml` config file, an example can be found [here](../conduit-example.toml).
|
||||
You can pass in different env vars to change config values on the fly. You can even configure Conduit completely by using env vars, but for that you need
|
||||
|
@ -44,10 +76,20 @@ to pass `-e CONDUIT_CONFIG=""` into your container. For an overview of possible
|
|||
|
||||
If you just want to test Conduit for a short time, you can use the `--rm` flag, which will clean up everything related to your container after you stop it.
|
||||
|
||||
## Docker-compose
|
||||
### Docker-compose
|
||||
|
||||
If the docker command is not for you or your setup, you can also use one of the provided `docker-compose` files. Depending on your proxy setup, use the [`docker-compose.traefik.yml`](docker-compose.traefik.yml) and [`docker-compose.override.traefik.yml`](docker-compose.override.traefik.yml) for Traefik (don't forget to remove `.traefik` from the filenames) or the normal [`docker-compose.yml`](../docker-compose.yml) for every other reverse proxy. Additional info about deploying
|
||||
Conduit can be found [here](../DEPLOY.md).
|
||||
If the `docker run` command is not for you or your setup, you can also use one of the provided `docker-compose` files.
|
||||
|
||||
Depending on your proxy setup, you can use one of the following files;
|
||||
- If you already have a `traefik` instance set up, use [`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml)
|
||||
- If you don't have a `traefik` instance set up (or any other reverse proxy), use [`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)
|
||||
- For any other reverse proxy, use [`docker-compose.yml`](docker-compose.yml)
|
||||
|
||||
When picking the traefik-related compose file, rename it so it matches `docker-compose.yml`, and
|
||||
rename the override file to `docker-compose.override.yml`. Edit the latter with the values you want
|
||||
for your server.
|
||||
|
||||
Additional info about deploying Conduit can be found [here](../DEPLOY.md).
|
||||
|
||||
### Build
|
||||
|
||||
|
@ -71,17 +113,23 @@ docker-compose up -d
|
|||
|
||||
### Use Traefik as Proxy
|
||||
|
||||
As a container user, you probably know about Traefik. It is a easy to use reverse proxy for making containerized app and services available through the web. With the
|
||||
two provided files, [`docker-compose.traefik.yml`](docker-compose.traefik.yml) and [`docker-compose.override.traefik.yml`](docker-compose.override.traefik.yml), it is
|
||||
equally easy to deploy and use Conduit, with a little caveat. If you already took a look at the files, then you should have seen the `well-known` service, and that is
|
||||
the little caveat. Traefik is simply a proxy and loadbalancer and is not able to serve any kind of content, but for Conduit to federate, we need to either expose ports
|
||||
`443` and `8448` or serve two endpoints `.well-known/matrix/client` and `.well-known/matrix/server`.
|
||||
As a container user, you probably know about Traefik. It is a easy to use reverse proxy for making
|
||||
containerized app and services available through the web. With the two provided files,
|
||||
[`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml) (or
|
||||
[`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)) and
|
||||
[`docker-compose.override.yml`](docker-compose.override.yml), it is equally easy to deploy
|
||||
and use Conduit, with a little caveat. If you already took a look at the files, then you should have
|
||||
seen the `well-known` service, and that is the little caveat. Traefik is simply a proxy and
|
||||
loadbalancer and is not able to serve any kind of content, but for Conduit to federate, we need to
|
||||
either expose ports `443` and `8448` or serve two endpoints `.well-known/matrix/client` and
|
||||
`.well-known/matrix/server`.
|
||||
|
||||
With the service `well-known` we use a single `nginx` container that will serve those two files.
|
||||
|
||||
So...step by step:
|
||||
|
||||
1. Copy [`docker-compose.traefik.yml`](docker-compose.traefik.yml) and [`docker-compose.override.traefik.yml`](docker-compose.override.traefik.yml) from the repository and remove `.traefik` from the filenames.
|
||||
1. Copy [`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml) (or
|
||||
[`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)) and [`docker-compose.override.yml`](docker-compose.override.yml) from the repository and remove `.for-traefik` (or `.with-traefik`) from the filename.
|
||||
2. Open both files and modify/adjust them to your needs. Meaning, change the `CONDUIT_SERVER_NAME` and the volume host mappings according to your needs.
|
||||
3. Create the `conduit.toml` config file, an example can be found [here](../conduit-example.toml), or set `CONDUIT_CONFIG=""` and configure Conduit per env vars.
|
||||
4. Uncomment the `element-web` service if you want to host your own Element Web Client and create a `element_config.json`.
|
||||
|
@ -96,12 +144,12 @@ So...step by step:
|
|||
|
||||
location /.well-known/matrix/server {
|
||||
return 200 '{"m.server": "<SUBDOMAIN>.<DOMAIN>:443"}';
|
||||
add_header Content-Type application/json;
|
||||
types { } default_type "application/json; charset=utf-8";
|
||||
}
|
||||
|
||||
location /.well-known/matrix/client {
|
||||
return 200 '{"m.homeserver": {"base_url": "https://<SUBDOMAIN>.<DOMAIN>"}}';
|
||||
add_header Content-Type application/json;
|
||||
types { } default_type "application/json; charset=utf-8";
|
||||
add_header "Access-Control-Allow-Origin" *;
|
||||
}
|
||||
|
||||
|
@ -112,4 +160,59 @@ So...step by step:
|
|||
```
|
||||
|
||||
6. Run `docker-compose up -d`
|
||||
7. Connect to your homeserver with your preferred client and create a user. You should do this immediatly after starting Conduit, because the first created user is the admin.
|
||||
7. Connect to your homeserver with your preferred client and create a user. You should do this immediately after starting Conduit, because the first created user is the admin.
|
||||
|
||||
|
||||
|
||||
|
||||
## Voice communication
|
||||
|
||||
In order to make or receive calls, a TURN server is required. Conduit suggests using [Coturn](https://github.com/coturn/coturn) for this purpose, which is also available as a Docker image. Before proceeding with the software installation, it is essential to have the necessary configurations in place.
|
||||
|
||||
### Configuration
|
||||
|
||||
Create a configuration file called `coturn.conf` containing:
|
||||
|
||||
```conf
|
||||
use-auth-secret
|
||||
static-auth-secret=<a secret key>
|
||||
realm=<your server domain>
|
||||
```
|
||||
A common way to generate a suitable alphanumeric secret key is by using `pwgen -s 64 1`.
|
||||
|
||||
These same values need to be set in conduit. You can either modify conduit.toml to include these lines:
|
||||
```
|
||||
turn_uris = ["turn:<your server domain>?transport=udp", "turn:<your server domain>?transport=tcp"]
|
||||
turn_secret = "<secret key from coturn configuration>"
|
||||
```
|
||||
or append the following to the docker environment variables dependig on which configuration method you used earlier:
|
||||
```yml
|
||||
CONDUIT_TURN_URIS: '["turn:<your server domain>?transport=udp", "turn:<your server domain>?transport=tcp"]'
|
||||
CONDUIT_TURN_SECRET: "<secret key from coturn configuration>"
|
||||
```
|
||||
Restart Conduit to apply these changes.
|
||||
|
||||
### Run
|
||||
Run the [Coturn](https://hub.docker.com/r/coturn/coturn) image using
|
||||
```bash
|
||||
docker run -d --network=host -v $(pwd)/coturn.conf:/etc/coturn/turnserver.conf coturn/coturn
|
||||
```
|
||||
|
||||
or docker-compose. For the latter, paste the following section into a file called `docker-compose.yml`
|
||||
and run `docker-compose up -d` in the same directory.
|
||||
|
||||
```yml
|
||||
version: 3
|
||||
services:
|
||||
turn:
|
||||
container_name: coturn-server
|
||||
image: docker.io/coturn/coturn
|
||||
restart: unless-stopped
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./coturn.conf:/etc/coturn/turnserver.conf
|
||||
```
|
||||
|
||||
To understand why the host networking mode is used and explore alternative configuration options, please visit the following link: https://github.com/coturn/coturn/blob/master/docker/coturn/README.md.
|
||||
For security recommendations see Synapse's [Coturn documentation](https://github.com/matrix-org/synapse/blob/develop/docs/setup/turn/coturn.md#configuration).
|
||||
|
||||
|
|
|
@ -7,16 +7,21 @@
|
|||
# Credit's for the original Dockerfile: Weasy666.
|
||||
# ---------------------------------------------------------------------------------------------------------
|
||||
|
||||
FROM docker.io/alpine:3.15.0 AS runner
|
||||
FROM docker.io/alpine:3.16.0@sha256:4ff3ca91275773af45cb4b0834e12b7eb47d1c18f770a0b151381cd227f4c253 AS runner
|
||||
|
||||
|
||||
# Standard port on which Conduit launches.
|
||||
# You still need to map the port when using the docker command or docker-compose.
|
||||
EXPOSE 6167
|
||||
|
||||
# Note from @jfowl: I would like to remove the config file in the future and just have the Docker version be configured with envs.
|
||||
ENV CONDUIT_CONFIG="/srv/conduit/conduit.toml" \
|
||||
CONDUIT_PORT=6167
|
||||
# Users are expected to mount a volume to this directory:
|
||||
ARG DEFAULT_DB_PATH=/var/lib/matrix-conduit
|
||||
|
||||
ENV CONDUIT_PORT=6167 \
|
||||
CONDUIT_ADDRESS="0.0.0.0" \
|
||||
CONDUIT_DATABASE_PATH=${DEFAULT_DB_PATH} \
|
||||
CONDUIT_CONFIG=''
|
||||
# └─> Set no config file to do all configuration with env vars
|
||||
|
||||
# Conduit needs:
|
||||
# ca-certificates: for https
|
||||
|
@ -25,7 +30,6 @@ RUN apk add --no-cache \
|
|||
ca-certificates \
|
||||
iproute2
|
||||
|
||||
|
||||
ARG CREATED
|
||||
ARG VERSION
|
||||
ARG GIT_REF
|
||||
|
@ -44,37 +48,37 @@ LABEL org.opencontainers.image.created=${CREATED} \
|
|||
org.opencontainers.image.documentation="https://gitlab.com/famedly/conduit" \
|
||||
org.opencontainers.image.ref.name=""
|
||||
|
||||
# Created directory for the database and media files
|
||||
RUN mkdir -p /srv/conduit/.local/share/conduit
|
||||
|
||||
# Test if Conduit is still alive, uses the same endpoint as Element
|
||||
COPY ./docker/healthcheck.sh /srv/conduit/healthcheck.sh
|
||||
HEALTHCHECK --start-period=5s --interval=5s CMD ./healthcheck.sh
|
||||
|
||||
|
||||
# Depending on the target platform (e.g. "linux/arm/v7", "linux/arm64/v8", or "linux/amd64")
|
||||
# copy the matching binary into this docker image
|
||||
ARG TARGETPLATFORM
|
||||
COPY ./$TARGETPLATFORM /srv/conduit/conduit
|
||||
|
||||
|
||||
# Improve security: Don't run stuff as root, that does not need to run as root:
|
||||
# Add www-data user and group with UID 82, as used by alpine
|
||||
# https://git.alpinelinux.org/aports/tree/main/nginx/nginx.pre-install
|
||||
# Most distros also use 1000:1000 for the first real user, so this should resolve volume mounting problems.
|
||||
ARG USER_ID=1000
|
||||
ARG GROUP_ID=1000
|
||||
RUN set -x ; \
|
||||
addgroup -Sg 82 www-data 2>/dev/null ; \
|
||||
adduser -S -D -H -h /srv/conduit -G www-data -g www-data www-data 2>/dev/null ; \
|
||||
addgroup www-data www-data 2>/dev/null && exit 0 ; exit 1
|
||||
deluser --remove-home www-data ; \
|
||||
addgroup -S -g ${GROUP_ID} conduit 2>/dev/null ; \
|
||||
adduser -S -u ${USER_ID} -D -H -h /srv/conduit -G conduit -g conduit conduit 2>/dev/null ; \
|
||||
addgroup conduit conduit 2>/dev/null && exit 0 ; exit 1
|
||||
|
||||
# Change ownership of Conduit files to www-data user and group
|
||||
RUN chown -cR www-data:www-data /srv/conduit
|
||||
RUN chmod +x /srv/conduit/healthcheck.sh
|
||||
# Change ownership of Conduit files to conduit user and group
|
||||
RUN chown -cR conduit:conduit /srv/conduit && \
|
||||
chmod +x /srv/conduit/healthcheck.sh && \
|
||||
mkdir -p ${DEFAULT_DB_PATH} && \
|
||||
chown -cR conduit:conduit ${DEFAULT_DB_PATH}
|
||||
|
||||
# Change user to www-data
|
||||
USER www-data
|
||||
# Change user to conduit
|
||||
USER conduit
|
||||
# Set container home directory
|
||||
WORKDIR /srv/conduit
|
||||
|
||||
# Run Conduit and print backtraces on panics
|
||||
ENV RUST_BACKTRACE=1
|
||||
ENTRYPOINT [ "/srv/conduit/conduit" ]
|
||||
|
||||
# Depending on the target platform (e.g. "linux/arm/v7", "linux/arm64/v8", or "linux/amd64")
|
||||
# copy the matching binary into this docker image
|
||||
ARG TARGETPLATFORM
|
||||
COPY --chown=conduit:conduit ./$TARGETPLATFORM /srv/conduit/conduit
|
||||
|
|
69
docker/docker-compose.for-traefik.yml
Normal file
69
docker/docker-compose.for-traefik.yml
Normal file
|
@ -0,0 +1,69 @@
|
|||
# Conduit - Behind Traefik Reverse Proxy
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
homeserver:
|
||||
### If you already built the Conduit image with 'docker build' or want to use the Docker Hub image,
|
||||
### then you are ready to go.
|
||||
image: matrixconduit/matrix-conduit:latest
|
||||
### If you want to build a fresh image from the sources, then comment the image line and uncomment the
|
||||
### build lines. If you want meaningful labels in your built Conduit image, you should run docker-compose like this:
|
||||
### CREATED=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VERSION=$(grep -m1 -o '[0-9].[0-9].[0-9]' Cargo.toml) docker-compose up -d
|
||||
# build:
|
||||
# context: .
|
||||
# args:
|
||||
# CREATED: '2021-03-16T08:18:27Z'
|
||||
# VERSION: '0.1.0'
|
||||
# LOCAL: 'false'
|
||||
# GIT_REF: origin/master
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- db:/var/lib/matrix-conduit/
|
||||
networks:
|
||||
- proxy
|
||||
environment:
|
||||
CONDUIT_SERVER_NAME: your.server.name # EDIT THIS
|
||||
CONDUIT_DATABASE_PATH: /var/lib/matrix-conduit/
|
||||
CONDUIT_DATABASE_BACKEND: rocksdb
|
||||
CONDUIT_PORT: 6167
|
||||
CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
|
||||
CONDUIT_ALLOW_REGISTRATION: 'true'
|
||||
CONDUIT_ALLOW_FEDERATION: 'true'
|
||||
CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
|
||||
#CONDUIT_MAX_CONCURRENT_REQUESTS: 100
|
||||
#CONDUIT_LOG: warn,rocket=off,_=off,sled=off
|
||||
CONDUIT_ADDRESS: 0.0.0.0
|
||||
CONDUIT_CONFIG: '' # Ignore this
|
||||
|
||||
# We need some way to server the client and server .well-known json. The simplest way is to use a nginx container
|
||||
# to serve those two as static files. If you want to use a different way, delete or comment the below service, here
|
||||
# and in the docker-compose override file.
|
||||
well-known:
|
||||
image: nginx:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nginx/matrix.conf:/etc/nginx/conf.d/matrix.conf # the config to serve the .well-known/matrix files
|
||||
- ./nginx/www:/var/www/ # location of the client and server .well-known-files
|
||||
### Uncomment if you want to use your own Element-Web App.
|
||||
### Note: You need to provide a config.json for Element and you also need a second
|
||||
### Domain or Subdomain for the communication between Element and Conduit
|
||||
### Config-Docs: https://github.com/vector-im/element-web/blob/develop/docs/config.md
|
||||
# element-web:
|
||||
# image: vectorim/element-web:latest
|
||||
# restart: unless-stopped
|
||||
# volumes:
|
||||
# - ./element_config.json:/app/config.json
|
||||
# networks:
|
||||
# - proxy
|
||||
# depends_on:
|
||||
# - homeserver
|
||||
|
||||
volumes:
|
||||
db:
|
||||
|
||||
networks:
|
||||
# This is the network Traefik listens to, if your network has a different
|
||||
# name, don't forget to change it here and in the docker-compose.override.yml
|
||||
proxy:
|
||||
external: true
|
|
@ -33,10 +33,11 @@ services:
|
|||
# CONDUIT_PORT: 6167
|
||||
# CONDUIT_CONFIG: '/srv/conduit/conduit.toml' # if you want to configure purely by env vars, set this to an empty string ''
|
||||
# Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging
|
||||
# CONDUIT_LOG: info # default is: "info,rocket=off,_=off,sled=off"
|
||||
# CONDUIT_LOG: info # default is: "warn,_=off,sled=off"
|
||||
# CONDUIT_ALLOW_JAEGER: 'false'
|
||||
# CONDUIT_ALLOW_ENCRYPTION: 'false'
|
||||
# CONDUIT_ALLOW_FEDERATION: 'false'
|
||||
# CONDUIT_ALLOW_ENCRYPTION: 'true'
|
||||
# CONDUIT_ALLOW_FEDERATION: 'true'
|
||||
# CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||
# CONDUIT_DATABASE_PATH: /srv/conduit/.local/share/conduit
|
||||
# CONDUIT_WORKERS: 10
|
||||
# CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
|
||||
|
@ -65,11 +66,33 @@ services:
|
|||
# depends_on:
|
||||
# - homeserver
|
||||
|
||||
traefik:
|
||||
image: "traefik:latest"
|
||||
container_name: "traefik"
|
||||
restart: "unless-stopped"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||
# - "./traefik_config:/etc/traefik"
|
||||
- "acme:/etc/traefik/acme"
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
||||
# middleware redirect
|
||||
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
|
||||
# global redirect to https
|
||||
- "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
|
||||
- "traefik.http.routers.redirs.entrypoints=http"
|
||||
- "traefik.http.routers.redirs.middlewares=redirect-to-https"
|
||||
|
||||
networks:
|
||||
- proxy
|
||||
|
||||
volumes:
|
||||
db:
|
||||
acme:
|
||||
|
||||
networks:
|
||||
# This is the network Traefik listens to, if your network has a different
|
||||
# name, don't forget to change it here and in the docker-compose.override.yml
|
||||
proxy:
|
||||
external: true
|
||||
proxy:
|
|
@ -20,27 +20,22 @@ services:
|
|||
ports:
|
||||
- 8448:6167
|
||||
volumes:
|
||||
- db:/srv/conduit/.local/share/conduit
|
||||
### Uncomment if you want to use conduit.toml to configure Conduit
|
||||
### Note: Set env vars will override conduit.toml values
|
||||
# - ./conduit.toml:/srv/conduit/conduit.toml
|
||||
- db:/var/lib/matrix-conduit/
|
||||
environment:
|
||||
CONDUIT_SERVER_NAME: localhost:6167 # replace with your own name
|
||||
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
|
||||
CONDUIT_SERVER_NAME: your.server.name # EDIT THIS
|
||||
CONDUIT_DATABASE_PATH: /var/lib/matrix-conduit/
|
||||
CONDUIT_DATABASE_BACKEND: rocksdb
|
||||
CONDUIT_PORT: 6167
|
||||
CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
|
||||
CONDUIT_ALLOW_REGISTRATION: 'true'
|
||||
### Uncomment and change values as desired
|
||||
# CONDUIT_ADDRESS: 0.0.0.0
|
||||
# CONDUIT_PORT: 6167
|
||||
# CONDUIT_CONFIG: '/srv/conduit/conduit.toml' # if you want to configure purely by env vars, set this to an empty string ''
|
||||
# Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging
|
||||
# CONDUIT_LOG: info # default is: "info,rocket=off,_=off,sled=off"
|
||||
# CONDUIT_ALLOW_JAEGER: 'false'
|
||||
# CONDUIT_ALLOW_ENCRYPTION: 'false'
|
||||
# CONDUIT_ALLOW_FEDERATION: 'false'
|
||||
# CONDUIT_DATABASE_PATH: /srv/conduit/.local/share/conduit
|
||||
# CONDUIT_WORKERS: 10
|
||||
# CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
|
||||
|
||||
CONDUIT_ALLOW_FEDERATION: 'true'
|
||||
CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
|
||||
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
|
||||
#CONDUIT_MAX_CONCURRENT_REQUESTS: 100
|
||||
#CONDUIT_LOG: warn,rocket=off,_=off,sled=off
|
||||
CONDUIT_ADDRESS: 0.0.0.0
|
||||
CONDUIT_CONFIG: '' # Ignore this
|
||||
#
|
||||
### Uncomment if you want to use your own Element-Web App.
|
||||
### Note: You need to provide a config.json for Element and you also need a second
|
||||
### Domain or Subdomain for the communication between Element and Conduit
|
||||
|
@ -56,4 +51,4 @@ services:
|
|||
# - homeserver
|
||||
|
||||
volumes:
|
||||
db:
|
||||
db:
|
|
@ -6,9 +6,14 @@ if [ -z "${CONDUIT_PORT}" ]; then
|
|||
CONDUIT_PORT=$(ss -tlpn | grep conduit | grep -m1 -o ':[0-9]*' | grep -m1 -o '[0-9]*')
|
||||
fi
|
||||
|
||||
# If CONDUIT_ADDRESS is not set try to get the address from the process list
|
||||
if [ -z "${CONDUIT_ADDRESS}" ]; then
|
||||
CONDUIT_ADDRESS=$(ss -tlpn | awk -F ' +|:' '/conduit/ { print $4 }')
|
||||
fi
|
||||
|
||||
# The actual health check.
|
||||
# We try to first get a response on HTTP and when that fails on HTTPS and when that fails, we exit with code 1.
|
||||
# TODO: Change this to a single wget call. Do we have a config value that we can check for that?
|
||||
wget --no-verbose --tries=1 --spider "http://localhost:${CONDUIT_PORT}/_matrix/client/versions" || \
|
||||
wget --no-verbose --tries=1 --spider "https://localhost:${CONDUIT_PORT}/_matrix/client/versions" || \
|
||||
wget --no-verbose --tries=1 --spider "http://${CONDUIT_ADDRESS}:${CONDUIT_PORT}/_matrix/client/versions" || \
|
||||
wget --no-verbose --tries=1 --spider "https://${CONDUIT_ADDRESS}:${CONDUIT_PORT}/_matrix/client/versions" || \
|
||||
exit 1
|
||||
|
|
64
engage.toml
Normal file
64
engage.toml
Normal file
|
@ -0,0 +1,64 @@
|
|||
interpreter = ["bash", "-euo", "pipefail", "-c"]
|
||||
|
||||
[[task]]
|
||||
name = "engage"
|
||||
group = "versions"
|
||||
script = "engage --version"
|
||||
|
||||
[[task]]
|
||||
name = "rustc"
|
||||
group = "versions"
|
||||
script = "rustc --version"
|
||||
|
||||
[[task]]
|
||||
name = "cargo"
|
||||
group = "versions"
|
||||
script = "cargo --version"
|
||||
|
||||
[[task]]
|
||||
name = "cargo-fmt"
|
||||
group = "versions"
|
||||
script = "cargo fmt --version"
|
||||
|
||||
[[task]]
|
||||
name = "rustdoc"
|
||||
group = "versions"
|
||||
script = "rustdoc --version"
|
||||
|
||||
[[task]]
|
||||
name = "cargo-clippy"
|
||||
group = "versions"
|
||||
script = "cargo clippy -- --version"
|
||||
|
||||
[[task]]
|
||||
name = "cargo-fmt"
|
||||
group = "lints"
|
||||
script = "cargo fmt --check -- --color=always"
|
||||
|
||||
[[task]]
|
||||
name = "cargo-doc"
|
||||
group = "lints"
|
||||
script = """
|
||||
RUSTDOCFLAGS="-D warnings" cargo doc \
|
||||
--workspace \
|
||||
--no-deps \
|
||||
--document-private-items \
|
||||
--color always
|
||||
"""
|
||||
|
||||
[[task]]
|
||||
name = "cargo-clippy"
|
||||
group = "lints"
|
||||
script = "cargo clippy --workspace --all-targets --color=always -- -D warnings"
|
||||
|
||||
[[task]]
|
||||
name = "cargo"
|
||||
group = "tests"
|
||||
script = """
|
||||
cargo test \
|
||||
--workspace \
|
||||
--all-targets \
|
||||
--color=always \
|
||||
-- \
|
||||
--color=always
|
||||
"""
|
263
flake.lock
Normal file
263
flake.lock
Normal file
|
@ -0,0 +1,263 @@
|
|||
{
|
||||
"nodes": {
|
||||
"attic": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705617092,
|
||||
"narHash": "sha256-n9PK4O4X4S1JkwpkMuYm1wHZYJzRqif8g3RuVIPD+rY=",
|
||||
"owner": "zhaofengli",
|
||||
"repo": "attic",
|
||||
"rev": "fbe252a5c21febbe920c025560cbd63b20e24f3b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "zhaofengli",
|
||||
"ref": "main",
|
||||
"repo": "attic",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"attic",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1702918879,
|
||||
"narHash": "sha256-tWJqzajIvYcaRWxn+cLUB9L9Pv4dQ3Bfit/YjU5ze3g=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "7195c00c272fdd92fc74e7d5a0a2844b9fadb2fb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane_2": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1706473964,
|
||||
"narHash": "sha256-Fq6xleee/TsX6NbtoRuI96bBuDHMU57PrcK9z1QEKbk=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "c798790eabec3e3da48190ae3698ac227aab770c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"ref": "master",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705559032,
|
||||
"narHash": "sha256-Cb+Jd1+Gz4Wi+8elPnUIHnqQmE1qjDRZ+PsJaPaAffY=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "e132ea0eb0c799a2109a91688e499d7bf4962801",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat_2": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1705309234,
|
||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-filter": {
|
||||
"locked": {
|
||||
"lastModified": 1705332318,
|
||||
"narHash": "sha256-kcw1yFeJe9N4PjQji9ZeX47jg0p9A0DuU4djKvg1a7I=",
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"rev": "3449dc925982ad46246cfc36469baf66e1b64f17",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "nix-filter",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1702539185,
|
||||
"narHash": "sha256-KnIRG5NMdLIpEkZTnN5zovNYc0hhXjAgv6pfd5Z4c7U=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "aa9d4729cbc99dabacb50e3994dcefb3ea0f7447",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1702780907,
|
||||
"narHash": "sha256-blbrBBXjjZt6OKTcYX1jpe9SRof2P9ZYWPzq22tzXAA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1e2e384c5b7c50dbf8e9c441a9e58d85f408b01f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-23.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1705496572,
|
||||
"narHash": "sha256-rPIe9G5EBLXdBdn9ilGc0nq082lzQd0xGGe092R/5QE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "842d9d80cfd4560648c785f8a4e6f3b096790e19",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"attic": "attic",
|
||||
"crane": "crane_2",
|
||||
"fenix": "fenix",
|
||||
"flake-compat": "flake-compat_2",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nix-filter": "nix-filter",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1705523001,
|
||||
"narHash": "sha256-TWq5vJ6m+9HGSDMsQAmz1TMegMi79R3TTyKjnPWsQp8=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "9d9b34354d2f13e33568c9c55b226dd014a146a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
259
flake.nix
Normal file
259
flake.nix
Normal file
|
@ -0,0 +1,259 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
nix-filter.url = "github:numtide/nix-filter";
|
||||
flake-compat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
crane = {
|
||||
url = "github:ipetkov/crane?ref=master";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
attic.url = "github:zhaofengli/attic?ref=main";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{ self
|
||||
, nixpkgs
|
||||
, flake-utils
|
||||
, nix-filter
|
||||
|
||||
, fenix
|
||||
, crane
|
||||
, ...
|
||||
}: flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgsHost = nixpkgs.legacyPackages.${system};
|
||||
|
||||
# Nix-accessible `Cargo.toml`
|
||||
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
|
||||
|
||||
# The Rust toolchain to use
|
||||
toolchain = fenix.packages.${system}.fromToolchainFile {
|
||||
file = ./rust-toolchain.toml;
|
||||
|
||||
# See also `rust-toolchain.toml`
|
||||
sha256 = "sha256-SXRtAuO4IqNOQq+nLbrsDFbVk+3aVA8NNpSZsKlVH/8=";
|
||||
};
|
||||
|
||||
builder = pkgs:
|
||||
((crane.mkLib pkgs).overrideToolchain toolchain).buildPackage;
|
||||
|
||||
nativeBuildInputs = pkgs: [
|
||||
# bindgen needs the build platform's libclang. Apparently due to
|
||||
# "splicing weirdness", pkgs.rustPlatform.bindgenHook on its own doesn't
|
||||
# quite do the right thing here.
|
||||
pkgs.buildPackages.rustPlatform.bindgenHook
|
||||
];
|
||||
|
||||
env = pkgs: {
|
||||
ROCKSDB_INCLUDE_DIR = "${pkgs.rocksdb}/include";
|
||||
ROCKSDB_LIB_DIR = "${pkgs.rocksdb}/lib";
|
||||
}
|
||||
// pkgs.lib.optionalAttrs pkgs.stdenv.hostPlatform.isStatic {
|
||||
ROCKSDB_STATIC = "";
|
||||
}
|
||||
// {
|
||||
CARGO_BUILD_RUSTFLAGS = let inherit (pkgs) lib stdenv; in
|
||||
lib.concatStringsSep " " ([]
|
||||
++ lib.optionals
|
||||
# This disables PIE for static builds, which isn't great in terms
|
||||
# of security. Unfortunately, my hand is forced because nixpkgs'
|
||||
# `libstdc++.a` is built without `-fPIE`, which precludes us from
|
||||
# leaving PIE enabled.
|
||||
stdenv.hostPlatform.isStatic
|
||||
["-C" "relocation-model=static"]
|
||||
++ lib.optionals
|
||||
(stdenv.buildPlatform.config != stdenv.hostPlatform.config)
|
||||
["-l" "c"]
|
||||
++ lib.optionals
|
||||
# This check has to match the one [here][0]. We only need to set
|
||||
# these flags when using a different linker. Don't ask me why,
|
||||
# though, because I don't know. All I know is it breaks otherwise.
|
||||
#
|
||||
# [0]: https://github.com/NixOS/nixpkgs/blob/612f97239e2cc474c13c9dafa0df378058c5ad8d/pkgs/build-support/rust/lib/default.nix#L36-L39
|
||||
(
|
||||
# Nixpkgs doesn't check for x86_64 here but we do, because I
|
||||
# observed a failure building statically for x86_64 without
|
||||
# including it here. Linkers are weird.
|
||||
(stdenv.hostPlatform.isAarch64 || stdenv.hostPlatform.isx86_64)
|
||||
&& stdenv.hostPlatform.isStatic
|
||||
&& !stdenv.isDarwin
|
||||
&& !stdenv.cc.bintools.isLLVM
|
||||
)
|
||||
[
|
||||
"-l"
|
||||
"stdc++"
|
||||
"-L"
|
||||
"${stdenv.cc.cc.lib}/${stdenv.hostPlatform.config}/lib"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
# What follows is stolen from [here][0]. Its purpose is to properly
|
||||
# configure compilers and linkers for various stages of the build, and
|
||||
# even covers the case of build scripts that need native code compiled and
|
||||
# run on the build platform (I think).
|
||||
#
|
||||
# [0]: https://github.com/NixOS/nixpkgs/blob/612f97239e2cc474c13c9dafa0df378058c5ad8d/pkgs/build-support/rust/lib/default.nix#L64-L78
|
||||
// (
|
||||
let
|
||||
inherit (pkgs.rust.lib) envVars;
|
||||
in
|
||||
pkgs.lib.optionalAttrs
|
||||
(pkgs.stdenv.targetPlatform.rust.rustcTarget
|
||||
!= pkgs.stdenv.hostPlatform.rust.rustcTarget)
|
||||
(
|
||||
let
|
||||
inherit (pkgs.stdenv.targetPlatform.rust) cargoEnvVarTarget;
|
||||
in
|
||||
{
|
||||
"CC_${cargoEnvVarTarget}" = envVars.ccForTarget;
|
||||
"CXX_${cargoEnvVarTarget}" = envVars.cxxForTarget;
|
||||
"CARGO_TARGET_${cargoEnvVarTarget}_LINKER" =
|
||||
envVars.linkerForTarget;
|
||||
}
|
||||
)
|
||||
// (
|
||||
let
|
||||
inherit (pkgs.stdenv.hostPlatform.rust) cargoEnvVarTarget rustcTarget;
|
||||
in
|
||||
{
|
||||
"CC_${cargoEnvVarTarget}" = envVars.ccForHost;
|
||||
"CXX_${cargoEnvVarTarget}" = envVars.cxxForHost;
|
||||
"CARGO_TARGET_${cargoEnvVarTarget}_LINKER" = envVars.linkerForHost;
|
||||
CARGO_BUILD_TARGET = rustcTarget;
|
||||
}
|
||||
)
|
||||
// (
|
||||
let
|
||||
inherit (pkgs.stdenv.buildPlatform.rust) cargoEnvVarTarget;
|
||||
in
|
||||
{
|
||||
"CC_${cargoEnvVarTarget}" = envVars.ccForBuild;
|
||||
"CXX_${cargoEnvVarTarget}" = envVars.cxxForBuild;
|
||||
"CARGO_TARGET_${cargoEnvVarTarget}_LINKER" = envVars.linkerForBuild;
|
||||
HOST_CC = "${pkgs.buildPackages.stdenv.cc}/bin/cc";
|
||||
HOST_CXX = "${pkgs.buildPackages.stdenv.cc}/bin/c++";
|
||||
}
|
||||
));
|
||||
|
||||
package = pkgs: builder pkgs {
|
||||
src = nix-filter {
|
||||
root = ./.;
|
||||
include = [
|
||||
"src"
|
||||
"Cargo.toml"
|
||||
"Cargo.lock"
|
||||
];
|
||||
};
|
||||
|
||||
# This is redundant with CI
|
||||
doCheck = false;
|
||||
|
||||
env = env pkgs;
|
||||
nativeBuildInputs = nativeBuildInputs pkgs;
|
||||
|
||||
meta.mainProgram = cargoToml.package.name;
|
||||
};
|
||||
|
||||
mkOciImage = pkgs: package:
|
||||
pkgs.dockerTools.buildImage {
|
||||
name = package.pname;
|
||||
tag = "next";
|
||||
copyToRoot = [
|
||||
pkgs.dockerTools.caCertificates
|
||||
];
|
||||
config = {
|
||||
# Use the `tini` init system so that signals (e.g. ctrl+c/SIGINT)
|
||||
# are handled as expected
|
||||
Entrypoint = [
|
||||
"${pkgs.lib.getExe' pkgs.tini "tini"}"
|
||||
"--"
|
||||
];
|
||||
Cmd = [
|
||||
"${pkgs.lib.getExe package}"
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
packages = {
|
||||
default = package pkgsHost;
|
||||
oci-image = mkOciImage pkgsHost self.packages.${system}.default;
|
||||
}
|
||||
//
|
||||
builtins.listToAttrs
|
||||
(builtins.concatLists
|
||||
(builtins.map
|
||||
(crossSystem:
|
||||
let
|
||||
binaryName = "static-${crossSystem}";
|
||||
pkgsCrossStatic =
|
||||
(import nixpkgs {
|
||||
inherit system;
|
||||
crossSystem = {
|
||||
config = crossSystem;
|
||||
};
|
||||
}).pkgsStatic;
|
||||
in
|
||||
[
|
||||
# An output for a statically-linked binary
|
||||
{
|
||||
name = binaryName;
|
||||
value = package pkgsCrossStatic;
|
||||
}
|
||||
|
||||
# An output for an OCI image based on that binary
|
||||
{
|
||||
name = "oci-image-${crossSystem}";
|
||||
value = mkOciImage
|
||||
pkgsCrossStatic
|
||||
self.packages.${system}.${binaryName};
|
||||
}
|
||||
]
|
||||
)
|
||||
[
|
||||
"x86_64-unknown-linux-musl"
|
||||
"aarch64-unknown-linux-musl"
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
devShells.default = pkgsHost.mkShell {
|
||||
env = env pkgsHost // {
|
||||
# Rust Analyzer needs to be able to find the path to default crate
|
||||
# sources, and it can read this environment variable to do so. The
|
||||
# `rust-src` component is required in order for this to work.
|
||||
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
|
||||
};
|
||||
|
||||
# Development tools
|
||||
nativeBuildInputs = nativeBuildInputs pkgsHost ++ [
|
||||
# Always use nightly rustfmt because most of its options are unstable
|
||||
#
|
||||
# This needs to come before `toolchain` in this list, otherwise
|
||||
# `$PATH` will have stable rustfmt instead.
|
||||
fenix.packages.${system}.latest.rustfmt
|
||||
|
||||
toolchain
|
||||
] ++ (with pkgsHost; [
|
||||
engage
|
||||
|
||||
# Needed for Complement
|
||||
go
|
||||
olm
|
||||
|
||||
# Needed for our script for Complement
|
||||
jq
|
||||
]);
|
||||
};
|
||||
});
|
||||
}
|
198
nix/README.md
Normal file
198
nix/README.md
Normal file
|
@ -0,0 +1,198 @@
|
|||
# Conduit for Nix/NixOS
|
||||
|
||||
This guide assumes you have a recent version of Nix (^2.4) installed.
|
||||
|
||||
Since Conduit ships as a Nix flake, you'll first need to [enable
|
||||
flakes][enable_flakes].
|
||||
|
||||
You can now use the usual Nix commands to interact with Conduit's flake. For
|
||||
example, `nix run gitlab:famedly/conduit` will run Conduit (though you'll need
|
||||
to provide configuration and such manually as usual).
|
||||
|
||||
If your NixOS configuration is defined as a flake, you can depend on this flake
|
||||
to provide a more up-to-date version than provided by `nixpkgs`. In your flake,
|
||||
add the following to your `inputs`:
|
||||
|
||||
```nix
|
||||
conduit = {
|
||||
url = "gitlab:famedly/conduit";
|
||||
|
||||
# Assuming you have an input for nixpkgs called `nixpkgs`. If you experience
|
||||
# build failures while using this, try commenting/deleting this line. This
|
||||
# will probably also require you to always build from source.
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
```
|
||||
|
||||
Next, make sure you're passing your flake inputs to the `specialArgs` argument
|
||||
of `nixpkgs.lib.nixosSystem` [as explained here][specialargs]. This guide will
|
||||
assume you've named the group `flake-inputs`.
|
||||
|
||||
Now you can configure Conduit and a reverse proxy for it. Add the following to
|
||||
a new Nix file and include it in your configuration:
|
||||
|
||||
```nix
|
||||
{ config
|
||||
, pkgs
|
||||
, flake-inputs
|
||||
, ...
|
||||
}:
|
||||
|
||||
let
|
||||
# You'll need to edit these values
|
||||
|
||||
# The hostname that will appear in your user and room IDs
|
||||
server_name = "example.com";
|
||||
|
||||
# The hostname that Conduit actually runs on
|
||||
#
|
||||
# This can be the same as `server_name` if you want. This is only necessary
|
||||
# when Conduit is running on a different machine than the one hosting your
|
||||
# root domain. This configuration also assumes this is all running on a single
|
||||
# machine, some tweaks will need to be made if this is not the case.
|
||||
matrix_hostname = "matrix.${server_name}";
|
||||
|
||||
# An admin email for TLS certificate notifications
|
||||
admin_email = "admin@${server_name}";
|
||||
|
||||
# These ones you can leave alone
|
||||
|
||||
# Build a dervation that stores the content of `${server_name}/.well-known/matrix/server`
|
||||
well_known_server = pkgs.writeText "well-known-matrix-server" ''
|
||||
{
|
||||
"m.server": "${matrix_hostname}"
|
||||
}
|
||||
'';
|
||||
|
||||
# Build a dervation that stores the content of `${server_name}/.well-known/matrix/client`
|
||||
well_known_client = pkgs.writeText "well-known-matrix-client" ''
|
||||
{
|
||||
"m.homeserver": {
|
||||
"base_url": "https://${matrix_hostname}"
|
||||
}
|
||||
}
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
# Configure Conduit itself
|
||||
services.matrix-conduit = {
|
||||
enable = true;
|
||||
|
||||
# This causes NixOS to use the flake defined in this repository instead of
|
||||
# the build of Conduit built into nixpkgs.
|
||||
package = flake-inputs.conduit.packages.${pkgs.system}.default;
|
||||
|
||||
settings.global = {
|
||||
inherit server_name;
|
||||
};
|
||||
};
|
||||
|
||||
# Configure automated TLS acquisition/renewal
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults = {
|
||||
email = admin_email;
|
||||
};
|
||||
};
|
||||
|
||||
# ACME data must be readable by the NGINX user
|
||||
users.users.nginx.extraGroups = [
|
||||
"acme"
|
||||
];
|
||||
|
||||
# Configure NGINX as a reverse proxy
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedProxySettings = true;
|
||||
|
||||
virtualHosts = {
|
||||
"${matrix_hostname}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
|
||||
listen = [
|
||||
{
|
||||
addr = "0.0.0.0";
|
||||
port = 443;
|
||||
ssl = true;
|
||||
}
|
||||
{
|
||||
addr = "[::]";
|
||||
port = 443;
|
||||
ssl = true;
|
||||
} {
|
||||
addr = "0.0.0.0";
|
||||
port = 8448;
|
||||
ssl = true;
|
||||
}
|
||||
{
|
||||
addr = "[::]";
|
||||
port = 8448;
|
||||
ssl = true;
|
||||
}
|
||||
];
|
||||
|
||||
locations."/_matrix/" = {
|
||||
proxyPass = "http://backend_conduit$request_uri";
|
||||
proxyWebsockets = true;
|
||||
extraConfig = ''
|
||||
proxy_set_header Host $host;
|
||||
proxy_buffering off;
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = ''
|
||||
merge_slashes off;
|
||||
'';
|
||||
};
|
||||
|
||||
"${server_name}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
|
||||
locations."=/.well-known/matrix/server" = {
|
||||
# Use the contents of the derivation built previously
|
||||
alias = "${well_known_server}";
|
||||
|
||||
extraConfig = ''
|
||||
# Set the header since by default NGINX thinks it's just bytes
|
||||
default_type application/json;
|
||||
'';
|
||||
};
|
||||
|
||||
locations."=/.well-known/matrix/client" = {
|
||||
# Use the contents of the derivation built previously
|
||||
alias = "${well_known_client}";
|
||||
|
||||
extraConfig = ''
|
||||
# Set the header since by default NGINX thinks it's just bytes
|
||||
default_type application/json;
|
||||
|
||||
# https://matrix.org/docs/spec/client_server/r0.4.0#web-browser-clients
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
upstreams = {
|
||||
"backend_conduit" = {
|
||||
servers = {
|
||||
"[::1]:${toString config.services.matrix-conduit.settings.global.port}" = { };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Open firewall ports for HTTP, HTTPS, and Matrix federation
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 8448 ];
|
||||
networking.firewall.allowedUDPPorts = [ 80 443 8448 ];
|
||||
}
|
||||
```
|
||||
|
||||
Now you can rebuild your system configuration and you should be good to go!
|
||||
|
||||
[enable_flakes]: https://nixos.wiki/wiki/Flakes#Enable_flakes
|
||||
|
||||
[specialargs]: https://nixos.wiki/wiki/Flakes#Using_nix_flakes_with_NixOS
|
22
rust-toolchain.toml
Normal file
22
rust-toolchain.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
# This is the authoritiative configuration of this project's Rust toolchain.
|
||||
#
|
||||
# Other files that need upkeep when this changes:
|
||||
#
|
||||
# * `.gitlab-ci.yml`
|
||||
# * `Cargo.toml`
|
||||
# * `flake.nix`
|
||||
#
|
||||
# Search in those files for `rust-toolchain.toml` to find the relevant places.
|
||||
# If you're having trouble making the relevant changes, bug a maintainer.
|
||||
|
||||
[toolchain]
|
||||
channel = "1.75.0"
|
||||
components = [
|
||||
# For rust-analyzer
|
||||
"rust-src",
|
||||
]
|
||||
targets = [
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-unknown-linux-musl",
|
||||
]
|
|
@ -1,11 +1,11 @@
|
|||
use crate::{utils, Error, Result};
|
||||
use crate::{services, utils, Error, Result};
|
||||
use bytes::BytesMut;
|
||||
use ruma::api::{IncomingResponse, OutgoingRequest, SendAccessToken};
|
||||
use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
|
||||
use std::{fmt::Debug, mem, time::Duration};
|
||||
use tracing::warn;
|
||||
|
||||
#[tracing::instrument(skip(request))]
|
||||
pub(crate) async fn send_request<T: OutgoingRequest>(
|
||||
globals: &crate::database::globals::Globals,
|
||||
registration: serde_yaml::Value,
|
||||
request: T,
|
||||
) -> Result<T::IncomingResponse>
|
||||
|
@ -16,7 +16,11 @@ where
|
|||
let hs_token = registration.get("hs_token").unwrap().as_str().unwrap();
|
||||
|
||||
let mut http_request = request
|
||||
.try_into_http_request::<BytesMut>(destination, SendAccessToken::IfRequired(""))
|
||||
.try_into_http_request::<BytesMut>(
|
||||
destination,
|
||||
SendAccessToken::IfRequired(hs_token),
|
||||
&[MatrixVersion::V1_0],
|
||||
)
|
||||
.unwrap()
|
||||
.map(|body| body.freeze());
|
||||
|
||||
|
@ -41,7 +45,23 @@ where
|
|||
*reqwest_request.timeout_mut() = Some(Duration::from_secs(30));
|
||||
|
||||
let url = reqwest_request.url().clone();
|
||||
let mut response = globals.default_client().execute(reqwest_request).await?;
|
||||
let mut response = match services()
|
||||
.globals
|
||||
.default_client()
|
||||
.execute(reqwest_request)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Could not send request to appservice {:?} at {}: {}",
|
||||
registration.get("id"),
|
||||
destination,
|
||||
e
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
// reqwest::Response -> http::Response conversion
|
||||
let status = response.status();
|
|
@ -1,36 +1,23 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::{
|
||||
database::{admin::make_user_admin, DatabaseGuard},
|
||||
pdu::PduBuilder,
|
||||
utils, ConduitResult, Error, Ruma,
|
||||
};
|
||||
use crate::{api::client_server, services, utils, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::{
|
||||
account::{
|
||||
change_password, deactivate, get_3pids, get_username_availability, register,
|
||||
whoami, ThirdPartyIdRemovalStatus,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
account::{
|
||||
change_password, deactivate, get_3pids, get_username_availability, register,
|
||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||
whoami, ThirdPartyIdRemovalStatus,
|
||||
},
|
||||
error::ErrorKind,
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
EventType,
|
||||
},
|
||||
events::{room::message::RoomMessageEventContent, GlobalAccountDataEventType},
|
||||
push, UserId,
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use register::RegistrationKind;
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, post};
|
||||
|
||||
const GUEST_NAME_LENGTH: usize = 10;
|
||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||
|
||||
/// # `GET /_matrix/client/r0/register/available`
|
||||
///
|
||||
|
@ -42,29 +29,25 @@ const GUEST_NAME_LENGTH: usize = 10;
|
|||
/// - No user or appservice on this server already claimed this username
|
||||
///
|
||||
/// Note: This will not reserve the username, so the username might become invalid when trying to register
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/register/available", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_register_available_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_username_availability::Request<'_>>,
|
||||
) -> ConduitResult<get_username_availability::Response> {
|
||||
body: Ruma<get_username_availability::v3::Request>,
|
||||
) -> Result<get_username_availability::v3::Response> {
|
||||
// Validate user id
|
||||
let user_id =
|
||||
UserId::parse_with_server_name(body.username.to_lowercase(), db.globals.server_name())
|
||||
.ok()
|
||||
.filter(|user_id| {
|
||||
!user_id.is_historical() && user_id.server_name() == db.globals.server_name()
|
||||
})
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidUsername,
|
||||
"Username is invalid.",
|
||||
))?;
|
||||
let user_id = UserId::parse_with_server_name(
|
||||
body.username.to_lowercase(),
|
||||
services().globals.server_name(),
|
||||
)
|
||||
.ok()
|
||||
.filter(|user_id| {
|
||||
!user_id.is_historical() && user_id.server_name() == services().globals.server_name()
|
||||
})
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidUsername,
|
||||
"Username is invalid.",
|
||||
))?;
|
||||
|
||||
// Check if username is creative enough
|
||||
if db.users.exists(&user_id)? {
|
||||
if services().users.exists(&user_id)? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UserInUse,
|
||||
"Desired user ID is already taken.",
|
||||
|
@ -74,7 +57,7 @@ pub async fn get_register_available_route(
|
|||
// TODO add check for appservice namespaces
|
||||
|
||||
// If no if check is true we have an username that's available to be used.
|
||||
Ok(get_username_availability::Response { available: true }.into())
|
||||
Ok(get_username_availability::v3::Response { available: true })
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/register`
|
||||
|
@ -90,16 +73,11 @@ pub async fn get_register_available_route(
|
|||
/// - If type is not guest and no username is given: Always fails after UIAA check
|
||||
/// - Creates a new account and populates it with default account data
|
||||
/// - If `inhibit_login` is false: Creates a device and returns device id and access_token
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/register", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn register_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<register::Request<'_>>,
|
||||
) -> ConduitResult<register::Response> {
|
||||
if !db.globals.allow_registration() && !body.from_appservice {
|
||||
pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<register::v3::Response> {
|
||||
if !services().globals.allow_registration()
|
||||
&& !body.from_appservice
|
||||
&& services().globals.config.registration_token.is_none()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Registration has been disabled.",
|
||||
|
@ -108,43 +86,49 @@ pub async fn register_route(
|
|||
|
||||
let is_guest = body.kind == RegistrationKind::Guest;
|
||||
|
||||
let mut missing_username = false;
|
||||
|
||||
// Validate user id
|
||||
let user_id = UserId::parse_with_server_name(
|
||||
if is_guest {
|
||||
utils::random_string(GUEST_NAME_LENGTH)
|
||||
} else {
|
||||
body.username.clone().unwrap_or_else(|| {
|
||||
// If the user didn't send a username field, that means the client is just trying
|
||||
// the get an UIAA error to see available flows
|
||||
missing_username = true;
|
||||
// Just give the user a random name. He won't be able to register with it anyway.
|
||||
utils::random_string(GUEST_NAME_LENGTH)
|
||||
let user_id = match (&body.username, is_guest) {
|
||||
(Some(username), false) => {
|
||||
let proposed_user_id = UserId::parse_with_server_name(
|
||||
username.to_lowercase(),
|
||||
services().globals.server_name(),
|
||||
)
|
||||
.ok()
|
||||
.filter(|user_id| {
|
||||
!user_id.is_historical()
|
||||
&& user_id.server_name() == services().globals.server_name()
|
||||
})
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidUsername,
|
||||
"Username is invalid.",
|
||||
))?;
|
||||
if services().users.exists(&proposed_user_id)? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UserInUse,
|
||||
"Desired user ID is already taken.",
|
||||
));
|
||||
}
|
||||
proposed_user_id
|
||||
}
|
||||
.to_lowercase(),
|
||||
db.globals.server_name(),
|
||||
)
|
||||
.ok()
|
||||
.filter(|user_id| !user_id.is_historical() && user_id.server_name() == db.globals.server_name())
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidUsername,
|
||||
"Username is invalid.",
|
||||
))?;
|
||||
|
||||
// Check if username is creative enough
|
||||
if db.users.exists(&user_id)? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UserInUse,
|
||||
"Desired user ID is already taken.",
|
||||
));
|
||||
}
|
||||
_ => loop {
|
||||
let proposed_user_id = UserId::parse_with_server_name(
|
||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||
services().globals.server_name(),
|
||||
)
|
||||
.unwrap();
|
||||
if !services().users.exists(&proposed_user_id)? {
|
||||
break proposed_user_id;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// UIAA
|
||||
let mut uiaainfo = UiaaInfo {
|
||||
flows: vec![AuthFlow {
|
||||
stages: vec![AuthType::Dummy],
|
||||
stages: if services().globals.config.registration_token.is_some() {
|
||||
vec![AuthType::RegistrationToken]
|
||||
} else {
|
||||
vec![AuthType::Dummy]
|
||||
},
|
||||
}],
|
||||
completed: Vec::new(),
|
||||
params: Default::default(),
|
||||
|
@ -152,16 +136,14 @@ pub async fn register_route(
|
|||
auth_error: None,
|
||||
};
|
||||
|
||||
if !body.from_appservice {
|
||||
if !body.from_appservice && !is_guest {
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = db.uiaa.try_auth(
|
||||
&UserId::parse_with_server_name("", db.globals.server_name())
|
||||
let (worked, uiaainfo) = services().uiaa.try_auth(
|
||||
&UserId::parse_with_server_name("", services().globals.server_name())
|
||||
.expect("we know this is valid"),
|
||||
"".into(),
|
||||
auth,
|
||||
&uiaainfo,
|
||||
&db.users,
|
||||
&db.globals,
|
||||
)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
|
@ -169,8 +151,8 @@ pub async fn register_route(
|
|||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
db.uiaa.create(
|
||||
&UserId::parse_with_server_name("", db.globals.server_name())
|
||||
services().uiaa.create(
|
||||
&UserId::parse_with_server_name("", services().globals.server_name())
|
||||
.expect("we know this is valid"),
|
||||
"".into(),
|
||||
&uiaainfo,
|
||||
|
@ -182,13 +164,6 @@ pub async fn register_route(
|
|||
}
|
||||
}
|
||||
|
||||
if missing_username {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::MissingParam,
|
||||
"Missing username field.",
|
||||
));
|
||||
}
|
||||
|
||||
let password = if is_guest {
|
||||
None
|
||||
} else {
|
||||
|
@ -196,34 +171,42 @@ pub async fn register_route(
|
|||
};
|
||||
|
||||
// Create user
|
||||
db.users.create(&user_id, password)?;
|
||||
services().users.create(&user_id, password)?;
|
||||
|
||||
// Default to pretty displayname
|
||||
let displayname = format!("{} ⚡️", user_id.localpart());
|
||||
db.users
|
||||
let mut displayname = user_id.localpart().to_owned();
|
||||
|
||||
// If enabled append lightning bolt to display name (default true)
|
||||
if services().globals.enable_lightning_bolt() {
|
||||
displayname.push_str(" ⚡️");
|
||||
}
|
||||
|
||||
services()
|
||||
.users
|
||||
.set_displayname(&user_id, Some(displayname.clone()))?;
|
||||
|
||||
// Initial account data
|
||||
db.account_data.update(
|
||||
services().account_data.update(
|
||||
None,
|
||||
&user_id,
|
||||
EventType::PushRules,
|
||||
&ruma::events::push_rules::PushRulesEvent {
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||
content: ruma::events::push_rules::PushRulesEventContent {
|
||||
global: push::Ruleset::server_default(&user_id),
|
||||
},
|
||||
},
|
||||
&db.globals,
|
||||
})
|
||||
.expect("to json always works"),
|
||||
)?;
|
||||
|
||||
// Inhibit login does not work for guests
|
||||
if !is_guest && body.inhibit_login {
|
||||
return Ok(register::Response {
|
||||
return Ok(register::v3::Response {
|
||||
access_token: None,
|
||||
user_id,
|
||||
device_id: None,
|
||||
}
|
||||
.into());
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Generate new device id if the user didn't specify one
|
||||
|
@ -238,31 +221,40 @@ pub async fn register_route(
|
|||
let token = utils::random_string(TOKEN_LENGTH);
|
||||
|
||||
// Create device for this account
|
||||
db.users.create_device(
|
||||
services().users.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&token,
|
||||
body.initial_device_display_name.clone(),
|
||||
)?;
|
||||
|
||||
info!("{} registered on this server", user_id);
|
||||
info!("New user {} registered on this server.", user_id);
|
||||
if !body.from_appservice && !is_guest {
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"New user {user_id} registered on this server."
|
||||
)));
|
||||
}
|
||||
|
||||
// If this is the first real user, grant them admin privileges
|
||||
// Note: the server user, @conduit:servername, is generated first
|
||||
if db.users.count()? == 2 {
|
||||
make_user_admin(&db, &user_id, displayname).await?;
|
||||
if services().users.count()? == 2 {
|
||||
services()
|
||||
.admin
|
||||
.make_user_admin(&user_id, displayname)
|
||||
.await?;
|
||||
|
||||
warn!("Granting {} admin privileges as the first user", user_id);
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(register::Response {
|
||||
Ok(register::v3::Response {
|
||||
access_token: Some(token),
|
||||
user_id,
|
||||
device_id: Some(device_id),
|
||||
}
|
||||
.into())
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/password`
|
||||
|
@ -279,15 +271,9 @@ pub async fn register_route(
|
|||
/// - Deletes device metadata (device id, device display name, last seen ip, last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/account/password", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn change_password_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<change_password::Request<'_>>,
|
||||
) -> ConduitResult<change_password::Response> {
|
||||
body: Ruma<change_password::v3::Request>,
|
||||
) -> Result<change_password::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
|
@ -302,45 +288,48 @@ pub async fn change_password_route(
|
|||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = db.uiaa.try_auth(
|
||||
sender_user,
|
||||
sender_device,
|
||||
auth,
|
||||
&uiaainfo,
|
||||
&db.users,
|
||||
&db.globals,
|
||||
)?;
|
||||
let (worked, uiaainfo) =
|
||||
services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
db.uiaa
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
db.users
|
||||
services()
|
||||
.users
|
||||
.set_password(sender_user, Some(&body.new_password))?;
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
for id in db
|
||||
for id in services()
|
||||
.users
|
||||
.all_device_ids(sender_user)
|
||||
.filter_map(|id| id.ok())
|
||||
.filter(|id| id != sender_device)
|
||||
{
|
||||
db.users.remove_device(sender_user, &id)?;
|
||||
services().users.remove_device(sender_user, &id)?;
|
||||
}
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
info!("User {} changed their password.", sender_user);
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"User {sender_user} changed their password."
|
||||
)));
|
||||
|
||||
Ok(change_password::Response {}.into())
|
||||
Ok(change_password::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET _matrix/client/r0/account/whoami`
|
||||
|
@ -348,17 +337,15 @@ pub async fn change_password_route(
|
|||
/// Get user_id of the sender user.
|
||||
///
|
||||
/// Note: Also works for Application Services
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/account/whoami", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(body))]
|
||||
pub async fn whoami_route(body: Ruma<whoami::Request>) -> ConduitResult<whoami::Response> {
|
||||
pub async fn whoami_route(body: Ruma<whoami::v3::Request>) -> Result<whoami::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
Ok(whoami::Response {
|
||||
let device_id = body.sender_device.as_ref().cloned();
|
||||
|
||||
Ok(whoami::v3::Response {
|
||||
user_id: sender_user.clone(),
|
||||
}
|
||||
.into())
|
||||
device_id,
|
||||
is_guest: services().users.is_deactivated(sender_user)? && !body.from_appservice,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/account/deactivate`
|
||||
|
@ -371,15 +358,9 @@ pub async fn whoami_route(body: Ruma<whoami::Request>) -> ConduitResult<whoami::
|
|||
/// - Forgets all to-device events
|
||||
/// - Triggers device list updates
|
||||
/// - Removes ability to log in again
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/account/deactivate", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn deactivate_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<deactivate::Request<'_>>,
|
||||
) -> ConduitResult<deactivate::Response> {
|
||||
body: Ruma<deactivate::v3::Request>,
|
||||
) -> Result<deactivate::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
|
@ -394,103 +375,79 @@ pub async fn deactivate_route(
|
|||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = db.uiaa.try_auth(
|
||||
sender_user,
|
||||
sender_device,
|
||||
auth,
|
||||
&uiaainfo,
|
||||
&db.users,
|
||||
&db.globals,
|
||||
)?;
|
||||
let (worked, uiaainfo) =
|
||||
services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
db.uiaa
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
// Leave all joined rooms and reject all invitations
|
||||
// TODO: work over federation invites
|
||||
let all_rooms = db
|
||||
.rooms
|
||||
.rooms_joined(sender_user)
|
||||
.chain(
|
||||
db.rooms
|
||||
.rooms_invited(sender_user)
|
||||
.map(|t| t.map(|(r, _)| r)),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for room_id in all_rooms {
|
||||
let room_id = room_id?;
|
||||
let event = RoomMemberEventContent {
|
||||
membership: MembershipState::Leave,
|
||||
displayname: None,
|
||||
avatar_url: None,
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
blurhash: None,
|
||||
reason: None,
|
||||
join_authorized_via_users_server: None,
|
||||
};
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
db.rooms.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomMember,
|
||||
content: to_raw_value(&event).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some(sender_user.to_string()),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
}
|
||||
// Make the user leave all rooms before deactivation
|
||||
client_server::leave_all_rooms(sender_user).await?;
|
||||
|
||||
// Remove devices and mark account as deactivated
|
||||
db.users.deactivate_account(sender_user)?;
|
||||
services().users.deactivate_account(sender_user)?;
|
||||
|
||||
info!("{} deactivated their account", sender_user);
|
||||
info!("User {} deactivated their account.", sender_user);
|
||||
services()
|
||||
.admin
|
||||
.send_message(RoomMessageEventContent::notice_plain(format!(
|
||||
"User {sender_user} deactivated their account."
|
||||
)));
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(deactivate::Response {
|
||||
Ok(deactivate::v3::Response {
|
||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||
}
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET _matrix/client/r0/account/3pid`
|
||||
/// # `GET _matrix/client/v3/account/3pid`
|
||||
///
|
||||
/// Get a list of third party identifiers associated with this account.
|
||||
///
|
||||
/// - Currently always returns empty list
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/account/3pid", data = "<body>")
|
||||
)]
|
||||
pub async fn third_party_route(
|
||||
body: Ruma<get_3pids::Request>,
|
||||
) -> ConduitResult<get_3pids::Response> {
|
||||
body: Ruma<get_3pids::v3::Request>,
|
||||
) -> Result<get_3pids::v3::Response> {
|
||||
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_3pids::Response::new(Vec::new()).into())
|
||||
Ok(get_3pids::v3::Response::new(Vec::new()))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an email address to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier as a contact option.
|
||||
pub async fn request_3pid_management_token_via_email_route(
|
||||
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::ThreepidDenied,
|
||||
"Third party identifier is not allowed",
|
||||
))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||
///
|
||||
/// "This API should be used to request validation tokens when adding an phone number to an account"
|
||||
///
|
||||
/// - 403 signals that The homeserver does not allow the third party identifier as a contact option.
|
||||
pub async fn request_3pid_management_token_via_msisdn_route(
|
||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::ThreepidDenied,
|
||||
"Third party identifier is not allowed",
|
||||
))
|
||||
}
|
|
@ -1,49 +1,46 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Database, Error, Ruma};
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
use rand::seq::SliceRandom;
|
||||
use regex::Regex;
|
||||
use ruma::{
|
||||
api::{
|
||||
appservice,
|
||||
client::{
|
||||
alias::{create_alias, delete_alias, get_alias},
|
||||
error::ErrorKind,
|
||||
r0::alias::{create_alias, delete_alias, get_alias},
|
||||
},
|
||||
federation,
|
||||
},
|
||||
RoomAliasId,
|
||||
OwnedRoomAliasId,
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{delete, get, put};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/directory/room/{roomAlias}`
|
||||
///
|
||||
/// Creates a new room alias on this server.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/directory/room/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn create_alias_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<create_alias::Request<'_>>,
|
||||
) -> ConduitResult<create_alias::Response> {
|
||||
if body.room_alias.server_name() != db.globals.server_name() {
|
||||
body: Ruma<create_alias::v3::Request>,
|
||||
) -> Result<create_alias::v3::Response> {
|
||||
if body.room_alias.server_name() != services().globals.server_name() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Alias is from another server.",
|
||||
));
|
||||
}
|
||||
|
||||
if db.rooms.id_from_alias(&body.room_alias)?.is_some() {
|
||||
if services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&body.room_alias)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(Error::Conflict("Alias already exists."));
|
||||
}
|
||||
|
||||
db.rooms
|
||||
.set_alias(&body.room_alias, Some(&body.room_id), &db.globals)?;
|
||||
services()
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(&body.room_alias, &body.room_id)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(create_alias::Response::new().into())
|
||||
Ok(create_alias::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/directory/room/{roomAlias}`
|
||||
|
@ -52,29 +49,21 @@ pub async fn create_alias_route(
|
|||
///
|
||||
/// - TODO: additional access control checks
|
||||
/// - TODO: Update canonical alias event
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
delete("/_matrix/client/r0/directory/room/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_alias_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_alias::Request<'_>>,
|
||||
) -> ConduitResult<delete_alias::Response> {
|
||||
if body.room_alias.server_name() != db.globals.server_name() {
|
||||
body: Ruma<delete_alias::v3::Request>,
|
||||
) -> Result<delete_alias::v3::Response> {
|
||||
if body.room_alias.server_name() != services().globals.server_name() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Alias is from another server.",
|
||||
));
|
||||
}
|
||||
|
||||
db.rooms.set_alias(&body.room_alias, None, &db.globals)?;
|
||||
services().rooms.alias.remove_alias(&body.room_alias)?;
|
||||
|
||||
// TODO: update alt_aliases?
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_alias::Response::new().into())
|
||||
Ok(delete_alias::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/directory/room/{roomAlias}`
|
||||
|
@ -82,40 +71,37 @@ pub async fn delete_alias_route(
|
|||
/// Resolve an alias locally or over federation.
|
||||
///
|
||||
/// - TODO: Suggest more servers to join via
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/directory/room/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_alias_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_alias::Request<'_>>,
|
||||
) -> ConduitResult<get_alias::Response> {
|
||||
get_alias_helper(&db, &body.room_alias).await
|
||||
body: Ruma<get_alias::v3::Request>,
|
||||
) -> Result<get_alias::v3::Response> {
|
||||
get_alias_helper(body.body.room_alias).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_alias_helper(
|
||||
db: &Database,
|
||||
room_alias: &RoomAliasId,
|
||||
) -> ConduitResult<get_alias::Response> {
|
||||
if room_alias.server_name() != db.globals.server_name() {
|
||||
let response = db
|
||||
room_alias: OwnedRoomAliasId,
|
||||
) -> Result<get_alias::v3::Response> {
|
||||
if room_alias.server_name() != services().globals.server_name() {
|
||||
let response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
room_alias.server_name(),
|
||||
federation::query::get_room_information::v1::Request { room_alias },
|
||||
federation::query::get_room_information::v1::Request {
|
||||
room_alias: room_alias.to_owned(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(get_alias::Response::new(response.room_id, response.servers).into());
|
||||
let mut servers = response.servers;
|
||||
servers.shuffle(&mut rand::thread_rng());
|
||||
|
||||
return Ok(get_alias::v3::Response::new(response.room_id, servers));
|
||||
}
|
||||
|
||||
let mut room_id = None;
|
||||
match db.rooms.id_from_alias(room_alias)? {
|
||||
match services().rooms.alias.resolve_local_alias(&room_alias)? {
|
||||
Some(r) => room_id = Some(r),
|
||||
None => {
|
||||
for (_id, registration) in db.appservice.all()? {
|
||||
for (_id, registration) in services().appservice.all()? {
|
||||
let aliases = registration
|
||||
.get("namespaces")
|
||||
.and_then(|ns| ns.get("aliases"))
|
||||
|
@ -130,19 +116,26 @@ pub(crate) async fn get_alias_helper(
|
|||
if aliases
|
||||
.iter()
|
||||
.any(|aliases| aliases.is_match(room_alias.as_str()))
|
||||
&& db
|
||||
&& services()
|
||||
.sending
|
||||
.send_appservice_request(
|
||||
&db.globals,
|
||||
registration,
|
||||
appservice::query::query_room_alias::v1::Request { room_alias },
|
||||
appservice::query::query_room_alias::v1::Request {
|
||||
room_alias: room_alias.clone(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
room_id = Some(db.rooms.id_from_alias(room_alias)?.ok_or_else(|| {
|
||||
Error::bad_config("Appservice lied to us. Room does not exist.")
|
||||
})?);
|
||||
room_id = Some(
|
||||
services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&room_alias)?
|
||||
.ok_or_else(|| {
|
||||
Error::bad_config("Appservice lied to us. Room does not exist.")
|
||||
})?,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -159,5 +152,8 @@ pub(crate) async fn get_alias_helper(
|
|||
}
|
||||
};
|
||||
|
||||
Ok(get_alias::Response::new(room_id, vec![db.globals.server_name().to_owned()]).into())
|
||||
Ok(get_alias::v3::Response::new(
|
||||
room_id,
|
||||
vec![services().globals.server_name().to_owned()],
|
||||
))
|
||||
}
|
362
src/api/client_server/backup.rs
Normal file
362
src/api/client_server/backup.rs
Normal file
|
@ -0,0 +1,362 @@
|
|||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::api::client::{
|
||||
backup::{
|
||||
add_backup_keys, add_backup_keys_for_room, add_backup_keys_for_session,
|
||||
create_backup_version, delete_backup_keys, delete_backup_keys_for_room,
|
||||
delete_backup_keys_for_session, delete_backup_version, get_backup_info, get_backup_keys,
|
||||
get_backup_keys_for_room, get_backup_keys_for_session, get_latest_backup_info,
|
||||
update_backup_version,
|
||||
},
|
||||
error::ErrorKind,
|
||||
};
|
||||
|
||||
/// # `POST /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
/// Creates a new backup.
|
||||
pub async fn create_backup_version_route(
|
||||
body: Ruma<create_backup_version::v3::Request>,
|
||||
) -> Result<create_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let version = services()
|
||||
.key_backups
|
||||
.create_backup(sender_user, &body.algorithm)?;
|
||||
|
||||
Ok(create_backup_version::v3::Response { version })
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
|
||||
///
|
||||
/// Update information about an existing backup. Only `auth_data` can be modified.
|
||||
pub async fn update_backup_version_route(
|
||||
body: Ruma<update_backup_version::v3::Request>,
|
||||
) -> Result<update_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
services()
|
||||
.key_backups
|
||||
.update_backup(sender_user, &body.version, &body.algorithm)?;
|
||||
|
||||
Ok(update_backup_version::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
/// Get information about the latest backup version.
|
||||
pub async fn get_latest_backup_info_route(
|
||||
body: Ruma<get_latest_backup_info::v3::Request>,
|
||||
) -> Result<get_latest_backup_info::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let (version, algorithm) = services()
|
||||
.key_backups
|
||||
.get_latest_backup(sender_user)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Key backup does not exist.",
|
||||
))?;
|
||||
|
||||
Ok(get_latest_backup_info::v3::Response {
|
||||
algorithm,
|
||||
count: (services().key_backups.count_keys(sender_user, &version)? as u32).into(),
|
||||
etag: services().key_backups.get_etag(sender_user, &version)?,
|
||||
version,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
/// Get information about an existing backup.
|
||||
pub async fn get_backup_info_route(
|
||||
body: Ruma<get_backup_info::v3::Request>,
|
||||
) -> Result<get_backup_info::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let algorithm = services()
|
||||
.key_backups
|
||||
.get_backup(sender_user, &body.version)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Key backup does not exist.",
|
||||
))?;
|
||||
|
||||
Ok(get_backup_info::v3::Response {
|
||||
algorithm,
|
||||
count: (services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)? as u32)
|
||||
.into(),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
version: body.version.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/version/{version}`
|
||||
///
|
||||
/// Delete an existing key backup.
|
||||
///
|
||||
/// - Deletes both information about the backup, as well as all key data related to the backup
|
||||
pub async fn delete_backup_version_route(
|
||||
body: Ruma<delete_backup_version::v3::Request>,
|
||||
) -> Result<delete_backup_version::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.key_backups
|
||||
.delete_backup(sender_user, &body.version)?;
|
||||
|
||||
Ok(delete_backup_version::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Add the received backup keys to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
pub async fn add_backup_keys_route(
|
||||
body: Ruma<add_backup_keys::v3::Request>,
|
||||
) -> Result<add_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services()
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
for (room_id, room) in &body.rooms {
|
||||
for (session_id, key_data) in &room.sessions {
|
||||
services().key_backups.add_key(
|
||||
sender_user,
|
||||
&body.version,
|
||||
room_id,
|
||||
session_id,
|
||||
key_data,
|
||||
)?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(add_backup_keys::v3::Response {
|
||||
count: (services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)? as u32)
|
||||
.into(),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Add the received backup keys to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
pub async fn add_backup_keys_for_room_route(
|
||||
body: Ruma<add_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<add_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services()
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
for (session_id, key_data) in &body.sessions {
|
||||
services().key_backups.add_key(
|
||||
sender_user,
|
||||
&body.version,
|
||||
&body.room_id,
|
||||
session_id,
|
||||
key_data,
|
||||
)?
|
||||
}
|
||||
|
||||
Ok(add_backup_keys_for_room::v3::Response {
|
||||
count: (services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)? as u32)
|
||||
.into(),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Add the received backup key to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
pub async fn add_backup_keys_for_session_route(
|
||||
body: Ruma<add_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<add_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= services()
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
services().key_backups.add_key(
|
||||
sender_user,
|
||||
&body.version,
|
||||
&body.room_id,
|
||||
&body.session_id,
|
||||
&body.session_data,
|
||||
)?;
|
||||
|
||||
Ok(add_backup_keys_for_session::v3::Response {
|
||||
count: (services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)? as u32)
|
||||
.into(),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Retrieves all keys from the backup.
|
||||
pub async fn get_backup_keys_route(
|
||||
body: Ruma<get_backup_keys::v3::Request>,
|
||||
) -> Result<get_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let rooms = services().key_backups.get_all(sender_user, &body.version)?;
|
||||
|
||||
Ok(get_backup_keys::v3::Response { rooms })
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Retrieves all keys from the backup for a given room.
|
||||
pub async fn get_backup_keys_for_room_route(
|
||||
body: Ruma<get_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<get_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let sessions = services()
|
||||
.key_backups
|
||||
.get_room(sender_user, &body.version, &body.room_id)?;
|
||||
|
||||
Ok(get_backup_keys_for_room::v3::Response { sessions })
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Retrieves a key from the backup.
|
||||
pub async fn get_backup_keys_for_session_route(
|
||||
body: Ruma<get_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<get_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let key_data = services()
|
||||
.key_backups
|
||||
.get_session(sender_user, &body.version, &body.room_id, &body.session_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Backup key not found for this user's session.",
|
||||
))?;
|
||||
|
||||
Ok(get_backup_keys_for_session::v3::Response { key_data })
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Delete the keys from the backup.
|
||||
pub async fn delete_backup_keys_route(
|
||||
body: Ruma<delete_backup_keys::v3::Request>,
|
||||
) -> Result<delete_backup_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.key_backups
|
||||
.delete_all_keys(sender_user, &body.version)?;
|
||||
|
||||
Ok(delete_backup_keys::v3::Response {
|
||||
count: (services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)? as u32)
|
||||
.into(),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Delete the keys from the backup for a given room.
|
||||
pub async fn delete_backup_keys_for_room_route(
|
||||
body: Ruma<delete_backup_keys_for_room::v3::Request>,
|
||||
) -> Result<delete_backup_keys_for_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.key_backups
|
||||
.delete_room_keys(sender_user, &body.version, &body.room_id)?;
|
||||
|
||||
Ok(delete_backup_keys_for_room::v3::Response {
|
||||
count: (services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)? as u32)
|
||||
.into(),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Delete a key from the backup.
|
||||
pub async fn delete_backup_keys_for_session_route(
|
||||
body: Ruma<delete_backup_keys_for_session::v3::Request>,
|
||||
) -> Result<delete_backup_keys_for_session::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services().key_backups.delete_room_key(
|
||||
sender_user,
|
||||
&body.version,
|
||||
&body.room_id,
|
||||
&body.session_id,
|
||||
)?;
|
||||
|
||||
Ok(delete_backup_keys_for_session::v3::Response {
|
||||
count: (services()
|
||||
.key_backups
|
||||
.count_keys(sender_user, &body.version)? as u32)
|
||||
.into(),
|
||||
etag: services()
|
||||
.key_backups
|
||||
.get_etag(sender_user, &body.version)?,
|
||||
})
|
||||
}
|
28
src/api/client_server/capabilities.rs
Normal file
28
src/api/client_server/capabilities.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use crate::{services, Result, Ruma};
|
||||
use ruma::api::client::discovery::get_capabilities::{
|
||||
self, Capabilities, RoomVersionStability, RoomVersionsCapability,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// # `GET /_matrix/client/r0/capabilities`
|
||||
///
|
||||
/// Get information on the supported feature set and other relevent capabilities of this server.
|
||||
pub async fn get_capabilities_route(
|
||||
_body: Ruma<get_capabilities::v3::Request>,
|
||||
) -> Result<get_capabilities::v3::Response> {
|
||||
let mut available = BTreeMap::new();
|
||||
for room_version in &services().globals.unstable_room_versions {
|
||||
available.insert(room_version.clone(), RoomVersionStability::Unstable);
|
||||
}
|
||||
for room_version in &services().globals.stable_room_versions {
|
||||
available.insert(room_version.clone(), RoomVersionStability::Stable);
|
||||
}
|
||||
|
||||
let mut capabilities = Capabilities::new();
|
||||
capabilities.room_versions = RoomVersionsCapability {
|
||||
default: services().globals.default_room_version(),
|
||||
available,
|
||||
};
|
||||
|
||||
Ok(get_capabilities::v3::Response { capabilities })
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::config::{
|
||||
config::{
|
||||
get_global_account_data, get_room_account_data, set_global_account_data,
|
||||
set_room_account_data,
|
||||
},
|
||||
error::ErrorKind,
|
||||
},
|
||||
events::{AnyGlobalAccountDataEventContent, AnyRoomAccountDataEventContent},
|
||||
serde::Raw,
|
||||
|
@ -13,29 +13,20 @@ use ruma::{
|
|||
use serde::Deserialize;
|
||||
use serde_json::{json, value::RawValue as RawJsonValue};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, put};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/account_data/{type}`
|
||||
///
|
||||
/// Sets some account data for the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/user/<_>/account_data/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_global_account_data_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_global_account_data::Request<'_>>,
|
||||
) -> ConduitResult<set_global_account_data::Response> {
|
||||
body: Ruma<set_global_account_data::v3::Request>,
|
||||
) -> Result<set_global_account_data::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let data: serde_json::Value = serde_json::from_str(body.data.get())
|
||||
let data: serde_json::Value = serde_json::from_str(body.data.json().get())
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))?;
|
||||
|
||||
let event_type = body.event_type.to_string();
|
||||
|
||||
db.account_data.update(
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
event_type.clone().into(),
|
||||
|
@ -43,37 +34,25 @@ pub async fn set_global_account_data_route(
|
|||
"type": event_type,
|
||||
"content": data,
|
||||
}),
|
||||
&db.globals,
|
||||
)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_global_account_data::Response {}.into())
|
||||
Ok(set_global_account_data::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
|
||||
///
|
||||
/// Sets some room account data for the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put(
|
||||
"/_matrix/client/r0/user/<_>/rooms/<_>/account_data/<_>",
|
||||
data = "<body>"
|
||||
)
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_room_account_data_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_room_account_data::Request<'_>>,
|
||||
) -> ConduitResult<set_room_account_data::Response> {
|
||||
body: Ruma<set_room_account_data::v3::Request>,
|
||||
) -> Result<set_room_account_data::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let data: serde_json::Value = serde_json::from_str(body.data.get())
|
||||
let data: serde_json::Value = serde_json::from_str(body.data.json().get())
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Data is invalid."))?;
|
||||
|
||||
let event_type = body.event_type.to_string();
|
||||
|
||||
db.account_data.update(
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
event_type.clone().into(),
|
||||
|
@ -81,71 +60,49 @@ pub async fn set_room_account_data_route(
|
|||
"type": event_type,
|
||||
"content": data,
|
||||
}),
|
||||
&db.globals,
|
||||
)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_room_account_data::Response {}.into())
|
||||
Ok(set_room_account_data::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/account_data/{type}`
|
||||
///
|
||||
/// Gets some account data for the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/user/<_>/account_data/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_global_account_data_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_global_account_data::Request<'_>>,
|
||||
) -> ConduitResult<get_global_account_data::Response> {
|
||||
body: Ruma<get_global_account_data::v3::Request>,
|
||||
) -> Result<get_global_account_data::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event: Box<RawJsonValue> = db
|
||||
let event: Box<RawJsonValue> = services()
|
||||
.account_data
|
||||
.get(None, sender_user, body.event_type.clone().into())?
|
||||
.get(None, sender_user, body.event_type.to_string().into())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<ExtractGlobalEventContent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
Ok(get_global_account_data::Response { account_data }.into())
|
||||
Ok(get_global_account_data::v3::Response { account_data })
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/account_data/{type}`
|
||||
///
|
||||
/// Gets some room account data for the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get(
|
||||
"/_matrix/client/r0/user/<_>/rooms/<_>/account_data/<_>",
|
||||
data = "<body>"
|
||||
)
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_room_account_data_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_room_account_data::Request<'_>>,
|
||||
) -> ConduitResult<get_room_account_data::Response> {
|
||||
body: Ruma<get_room_account_data::v3::Request>,
|
||||
) -> Result<get_room_account_data::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event: Box<RawJsonValue> = db
|
||||
let event: Box<RawJsonValue> = services()
|
||||
.account_data
|
||||
.get(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
body.event_type.clone().into(),
|
||||
)?
|
||||
.get(Some(&body.room_id), sender_user, body.event_type.clone())?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
|
||||
|
||||
let account_data = serde_json::from_str::<ExtractRoomEventContent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
Ok(get_room_account_data::Response { account_data }.into())
|
||||
Ok(get_room_account_data::v3::Response { account_data })
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
|
@ -1,75 +1,65 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::{context::get_context, filter::LazyLoadOptions},
|
||||
},
|
||||
events::EventType,
|
||||
api::client::{context::get_context, error::ErrorKind, filter::LazyLoadOptions},
|
||||
events::StateEventType,
|
||||
};
|
||||
use std::{collections::HashSet, convert::TryFrom};
|
||||
use std::collections::HashSet;
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::get;
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/context`
|
||||
///
|
||||
/// Allows loading room history around an event.
|
||||
///
|
||||
/// - Only works if the user is joined (TODO: always allow, but only show events if the user was
|
||||
/// joined, depending on history_visibility)
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/rooms/<_>/context/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_context_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_context::Request<'_>>,
|
||||
) -> ConduitResult<get_context::Response> {
|
||||
body: Ruma<get_context::v3::Request>,
|
||||
) -> Result<get_context::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
// Load filter
|
||||
let filter = body.filter.clone().unwrap_or_default();
|
||||
|
||||
let (lazy_load_enabled, lazy_load_send_redundant) = match filter.lazy_load_options {
|
||||
let (lazy_load_enabled, lazy_load_send_redundant) = match &body.filter.lazy_load_options {
|
||||
LazyLoadOptions::Enabled {
|
||||
include_redundant_members: redundant,
|
||||
} => (true, redundant),
|
||||
include_redundant_members,
|
||||
} => (true, *include_redundant_members),
|
||||
_ => (false, false),
|
||||
};
|
||||
|
||||
let mut lazy_loaded = HashSet::new();
|
||||
|
||||
let base_pdu_id = db
|
||||
let base_token = services()
|
||||
.rooms
|
||||
.get_pdu_id(&body.event_id)?
|
||||
.timeline
|
||||
.get_pdu_count(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Base event id not found.",
|
||||
))?;
|
||||
|
||||
let base_token = db.rooms.pdu_count(&base_pdu_id)?;
|
||||
|
||||
let base_event = db
|
||||
.rooms
|
||||
.get_pdu_from_id(&base_pdu_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Base event not found.",
|
||||
))?;
|
||||
let base_event =
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Base event not found.",
|
||||
))?;
|
||||
|
||||
let room_id = base_event.room_id.clone();
|
||||
|
||||
if !db.rooms.is_joined(sender_user, &room_id)? {
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &body.event_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view this room.",
|
||||
"You don't have permission to view this event.",
|
||||
));
|
||||
}
|
||||
|
||||
if !db.rooms.lazy_load_was_sent_before(
|
||||
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
|
@ -79,22 +69,28 @@ pub async fn get_context_route(
|
|||
lazy_loaded.insert(base_event.sender.as_str().to_owned());
|
||||
}
|
||||
|
||||
// Use limit with maximum 100
|
||||
let limit = u64::from(body.limit).min(100) as usize;
|
||||
|
||||
let base_event = base_event.to_room_event();
|
||||
|
||||
let events_before: Vec<_> = db
|
||||
let events_before: Vec<_> = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_until(sender_user, &room_id, base_token)?
|
||||
.take(
|
||||
u32::try_from(body.limit).map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Limit value is invalid.")
|
||||
})? as usize
|
||||
/ 2,
|
||||
)
|
||||
.take(limit / 2)
|
||||
.filter_map(|r| r.ok()) // Remove buggy events
|
||||
.filter(|(_, pdu)| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_before {
|
||||
if !db.rooms.lazy_load_was_sent_before(
|
||||
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
|
@ -107,28 +103,31 @@ pub async fn get_context_route(
|
|||
|
||||
let start_token = events_before
|
||||
.last()
|
||||
.and_then(|(pdu_id, _)| db.rooms.pdu_count(pdu_id).ok())
|
||||
.map(|count| count.to_string());
|
||||
.map(|(count, _)| count.stringify())
|
||||
.unwrap_or_else(|| base_token.stringify());
|
||||
|
||||
let events_before: Vec<_> = events_before
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
let events_after: Vec<_> = db
|
||||
let events_after: Vec<_> = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_after(sender_user, &room_id, base_token)?
|
||||
.take(
|
||||
u32::try_from(body.limit).map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Limit value is invalid.")
|
||||
})? as usize
|
||||
/ 2,
|
||||
)
|
||||
.take(limit / 2)
|
||||
.filter_map(|r| r.ok()) // Remove buggy events
|
||||
.filter(|(_, pdu)| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_after {
|
||||
if !db.rooms.lazy_load_was_sent_before(
|
||||
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&room_id,
|
||||
|
@ -139,24 +138,29 @@ pub async fn get_context_route(
|
|||
}
|
||||
}
|
||||
|
||||
let shortstatehash = match db.rooms.pdu_shortstatehash(
|
||||
let shortstatehash = match services().rooms.state_accessor.pdu_shortstatehash(
|
||||
events_after
|
||||
.last()
|
||||
.map_or(&*body.event_id, |(_, e)| &*e.event_id),
|
||||
)? {
|
||||
Some(s) => s,
|
||||
None => db
|
||||
None => services()
|
||||
.rooms
|
||||
.current_shortstatehash(&room_id)?
|
||||
.state
|
||||
.get_room_shortstatehash(&room_id)?
|
||||
.expect("All rooms have state"),
|
||||
};
|
||||
|
||||
let state_ids = db.rooms.state_full_ids(shortstatehash)?;
|
||||
let state_ids = services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.state_full_ids(shortstatehash)
|
||||
.await?;
|
||||
|
||||
let end_token = events_after
|
||||
.last()
|
||||
.and_then(|(pdu_id, _)| db.rooms.pdu_count(pdu_id).ok())
|
||||
.map(|count| count.to_string());
|
||||
.map(|(count, _)| count.stringify())
|
||||
.unwrap_or_else(|| base_token.stringify());
|
||||
|
||||
let events_after: Vec<_> = events_after
|
||||
.into_iter()
|
||||
|
@ -166,10 +170,13 @@ pub async fn get_context_route(
|
|||
let mut state = Vec::new();
|
||||
|
||||
for (shortstatekey, id) in state_ids {
|
||||
let (event_type, state_key) = db.rooms.get_statekey_from_short(shortstatekey)?;
|
||||
let (event_type, state_key) = services()
|
||||
.rooms
|
||||
.short
|
||||
.get_statekey_from_short(shortstatekey)?;
|
||||
|
||||
if event_type != EventType::RoomMember {
|
||||
let pdu = match db.rooms.get_pdu(&id)? {
|
||||
if event_type != StateEventType::RoomMember {
|
||||
let pdu = match services().rooms.timeline.get_pdu(&id)? {
|
||||
Some(pdu) => pdu,
|
||||
None => {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
|
@ -178,7 +185,7 @@ pub async fn get_context_route(
|
|||
};
|
||||
state.push(pdu.to_state_event());
|
||||
} else if !lazy_load_enabled || lazy_loaded.contains(&state_key) {
|
||||
let pdu = match db.rooms.get_pdu(&id)? {
|
||||
let pdu = match services().rooms.timeline.get_pdu(&id)? {
|
||||
Some(pdu) => pdu,
|
||||
None => {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
|
@ -189,14 +196,14 @@ pub async fn get_context_route(
|
|||
}
|
||||
}
|
||||
|
||||
let resp = get_context::Response {
|
||||
start: start_token,
|
||||
end: end_token,
|
||||
let resp = get_context::v3::Response {
|
||||
start: Some(start_token),
|
||||
end: Some(end_token),
|
||||
events_before,
|
||||
event: Some(base_event),
|
||||
events_after,
|
||||
state,
|
||||
};
|
||||
|
||||
Ok(resp.into())
|
||||
Ok(resp)
|
||||
}
|
|
@ -1,88 +1,65 @@
|
|||
use crate::{database::DatabaseGuard, utils, ConduitResult, Error, Ruma};
|
||||
use crate::{services, utils, Error, Result, Ruma};
|
||||
use ruma::api::client::{
|
||||
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
|
||||
error::ErrorKind,
|
||||
r0::{
|
||||
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
};
|
||||
|
||||
use super::SESSION_ID_LENGTH;
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{delete, get, post, put};
|
||||
|
||||
/// # `GET /_matrix/client/r0/devices`
|
||||
///
|
||||
/// Get metadata on all devices of the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/devices", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_devices_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_devices::Request>,
|
||||
) -> ConduitResult<get_devices::Response> {
|
||||
body: Ruma<get_devices::v3::Request>,
|
||||
) -> Result<get_devices::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let devices: Vec<device::Device> = db
|
||||
let devices: Vec<device::Device> = services()
|
||||
.users
|
||||
.all_devices_metadata(sender_user)
|
||||
.filter_map(|r| r.ok()) // Filter out buggy devices
|
||||
.collect();
|
||||
|
||||
Ok(get_devices::Response { devices }.into())
|
||||
Ok(get_devices::v3::Response { devices })
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/devices/{deviceId}`
|
||||
///
|
||||
/// Get metadata on a single device of the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/devices/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_device_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_device::Request<'_>>,
|
||||
) -> ConduitResult<get_device::Response> {
|
||||
body: Ruma<get_device::v3::Request>,
|
||||
) -> Result<get_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let device = db
|
||||
let device = services()
|
||||
.users
|
||||
.get_device_metadata(sender_user, &body.body.device_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
|
||||
|
||||
Ok(get_device::Response { device }.into())
|
||||
Ok(get_device::v3::Response { device })
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
|
||||
///
|
||||
/// Updates the metadata on a given device of the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/devices/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn update_device_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<update_device::Request<'_>>,
|
||||
) -> ConduitResult<update_device::Response> {
|
||||
body: Ruma<update_device::v3::Request>,
|
||||
) -> Result<update_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut device = db
|
||||
let mut device = services()
|
||||
.users
|
||||
.get_device_metadata(sender_user, &body.device_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Device not found."))?;
|
||||
|
||||
device.display_name = body.display_name.clone();
|
||||
|
||||
db.users
|
||||
services()
|
||||
.users
|
||||
.update_device_metadata(sender_user, &body.device_id, &device)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(update_device::Response {}.into())
|
||||
Ok(update_device::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/devices/{deviceId}`
|
||||
|
@ -94,15 +71,9 @@ pub async fn update_device_route(
|
|||
/// - Deletes device metadata (device id, device display name, last seen ip, last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
delete("/_matrix/client/r0/devices/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_device_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_device::Request<'_>>,
|
||||
) -> ConduitResult<delete_device::Response> {
|
||||
body: Ruma<delete_device::v3::Request>,
|
||||
) -> Result<delete_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
|
@ -118,32 +89,29 @@ pub async fn delete_device_route(
|
|||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = db.uiaa.try_auth(
|
||||
sender_user,
|
||||
sender_device,
|
||||
auth,
|
||||
&uiaainfo,
|
||||
&db.users,
|
||||
&db.globals,
|
||||
)?;
|
||||
let (worked, uiaainfo) =
|
||||
services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
db.uiaa
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
||||
}
|
||||
|
||||
db.users.remove_device(sender_user, &body.device_id)?;
|
||||
services()
|
||||
.users
|
||||
.remove_device(sender_user, &body.device_id)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_device::Response {}.into())
|
||||
Ok(delete_device::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
|
||||
|
@ -157,15 +125,9 @@ pub async fn delete_device_route(
|
|||
/// - Deletes device metadata (device id, device display name, last seen ip, last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/delete_devices", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_devices_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_devices::Request<'_>>,
|
||||
) -> ConduitResult<delete_devices::Response> {
|
||||
body: Ruma<delete_devices::v3::Request>,
|
||||
) -> Result<delete_devices::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
|
@ -181,21 +143,18 @@ pub async fn delete_devices_route(
|
|||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = db.uiaa.try_auth(
|
||||
sender_user,
|
||||
sender_device,
|
||||
auth,
|
||||
&uiaainfo,
|
||||
&db.users,
|
||||
&db.globals,
|
||||
)?;
|
||||
let (worked, uiaainfo) =
|
||||
services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
db.uiaa
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
|
@ -203,10 +162,8 @@ pub async fn delete_devices_route(
|
|||
}
|
||||
|
||||
for device_id in &body.devices {
|
||||
db.users.remove_device(sender_user, device_id)?
|
||||
services().users.remove_device(sender_user, device_id)?
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_devices::Response {}.into())
|
||||
Ok(delete_devices::v3::Response {})
|
||||
}
|
|
@ -1,53 +1,42 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Database, Error, Result, Ruma};
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
r0::{
|
||||
directory::{
|
||||
get_public_rooms, get_public_rooms_filtered, get_room_visibility,
|
||||
set_room_visibility,
|
||||
},
|
||||
room,
|
||||
directory::{
|
||||
get_public_rooms, get_public_rooms_filtered, get_room_visibility,
|
||||
set_room_visibility,
|
||||
},
|
||||
error::ErrorKind,
|
||||
room,
|
||||
},
|
||||
federation,
|
||||
},
|
||||
directory::{Filter, IncomingFilter, IncomingRoomNetwork, PublicRoomsChunk, RoomNetwork},
|
||||
directory::{Filter, PublicRoomJoinRule, PublicRoomsChunk, RoomNetwork},
|
||||
events::{
|
||||
room::{
|
||||
avatar::RoomAvatarEventContent,
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
create::RoomCreateEventContent,
|
||||
guest_access::{GuestAccess, RoomGuestAccessEventContent},
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
name::RoomNameEventContent,
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
EventType,
|
||||
StateEventType,
|
||||
},
|
||||
ServerName, UInt,
|
||||
};
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, post, put};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
/// # `POST /_matrix/client/r0/publicRooms`
|
||||
///
|
||||
/// Lists the public rooms on this server.
|
||||
///
|
||||
/// - Rooms are ordered by the number of joined members
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/publicRooms", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_public_rooms_filtered_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_public_rooms_filtered::Request<'_>>,
|
||||
) -> ConduitResult<get_public_rooms_filtered::Response> {
|
||||
body: Ruma<get_public_rooms_filtered::v3::Request>,
|
||||
) -> Result<get_public_rooms_filtered::v3::Response> {
|
||||
get_public_rooms_filtered_helper(
|
||||
&db,
|
||||
body.server.as_deref(),
|
||||
body.limit,
|
||||
body.since.as_deref(),
|
||||
|
@ -62,33 +51,24 @@ pub async fn get_public_rooms_filtered_route(
|
|||
/// Lists the public rooms on this server.
|
||||
///
|
||||
/// - Rooms are ordered by the number of joined members
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/publicRooms", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_public_rooms_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_public_rooms::Request<'_>>,
|
||||
) -> ConduitResult<get_public_rooms::Response> {
|
||||
body: Ruma<get_public_rooms::v3::Request>,
|
||||
) -> Result<get_public_rooms::v3::Response> {
|
||||
let response = get_public_rooms_filtered_helper(
|
||||
&db,
|
||||
body.server.as_deref(),
|
||||
body.limit,
|
||||
body.since.as_deref(),
|
||||
&IncomingFilter::default(),
|
||||
&IncomingRoomNetwork::Matrix,
|
||||
&Filter::default(),
|
||||
&RoomNetwork::Matrix,
|
||||
)
|
||||
.await?
|
||||
.0;
|
||||
.await?;
|
||||
|
||||
Ok(get_public_rooms::Response {
|
||||
Ok(get_public_rooms::v3::Response {
|
||||
chunk: response.chunk,
|
||||
prev_batch: response.prev_batch,
|
||||
next_batch: response.next_batch,
|
||||
total_room_count_estimate: response.total_room_count_estimate,
|
||||
}
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
|
||||
|
@ -96,23 +76,22 @@ pub async fn get_public_rooms_route(
|
|||
/// Sets the visibility of a given room in the room directory.
|
||||
///
|
||||
/// - TODO: Access control checks
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/directory/list/room/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_room_visibility_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_room_visibility::Request<'_>>,
|
||||
) -> ConduitResult<set_room_visibility::Response> {
|
||||
body: Ruma<set_room_visibility::v3::Request>,
|
||||
) -> Result<set_room_visibility::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services().rooms.metadata.exists(&body.room_id)? {
|
||||
// Return 404 if the room doesn't exist
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
|
||||
}
|
||||
|
||||
match &body.visibility {
|
||||
room::Visibility::Public => {
|
||||
db.rooms.set_public(&body.room_id, true)?;
|
||||
services().rooms.directory.set_public(&body.room_id)?;
|
||||
info!("{} made {} public", sender_user, body.room_id);
|
||||
}
|
||||
room::Visibility::Private => db.rooms.set_public(&body.room_id, false)?,
|
||||
room::Visibility::Private => services().rooms.directory.set_not_public(&body.room_id)?,
|
||||
_ => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
|
@ -121,78 +100,61 @@ pub async fn set_room_visibility_route(
|
|||
}
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_room_visibility::Response {}.into())
|
||||
Ok(set_room_visibility::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/directory/list/room/{roomId}`
|
||||
///
|
||||
/// Gets the visibility of a given room in the room directory.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/directory/list/room/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_room_visibility_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_room_visibility::Request<'_>>,
|
||||
) -> ConduitResult<get_room_visibility::Response> {
|
||||
Ok(get_room_visibility::Response {
|
||||
visibility: if db.rooms.is_public_room(&body.room_id)? {
|
||||
body: Ruma<get_room_visibility::v3::Request>,
|
||||
) -> Result<get_room_visibility::v3::Response> {
|
||||
if !services().rooms.metadata.exists(&body.room_id)? {
|
||||
// Return 404 if the room doesn't exist
|
||||
return Err(Error::BadRequest(ErrorKind::NotFound, "Room not found"));
|
||||
}
|
||||
|
||||
Ok(get_room_visibility::v3::Response {
|
||||
visibility: if services().rooms.directory.is_public_room(&body.room_id)? {
|
||||
room::Visibility::Public
|
||||
} else {
|
||||
room::Visibility::Private
|
||||
},
|
||||
}
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn get_public_rooms_filtered_helper(
|
||||
db: &Database,
|
||||
server: Option<&ServerName>,
|
||||
limit: Option<UInt>,
|
||||
since: Option<&str>,
|
||||
filter: &IncomingFilter,
|
||||
_network: &IncomingRoomNetwork,
|
||||
) -> ConduitResult<get_public_rooms_filtered::Response> {
|
||||
if let Some(other_server) = server.filter(|server| *server != db.globals.server_name().as_str())
|
||||
filter: &Filter,
|
||||
_network: &RoomNetwork,
|
||||
) -> Result<get_public_rooms_filtered::v3::Response> {
|
||||
if let Some(other_server) =
|
||||
server.filter(|server| *server != services().globals.server_name().as_str())
|
||||
{
|
||||
let response = db
|
||||
let response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
other_server,
|
||||
federation::directory::get_public_rooms_filtered::v1::Request {
|
||||
limit,
|
||||
since,
|
||||
since: since.map(ToOwned::to_owned),
|
||||
filter: Filter {
|
||||
generic_search_term: filter.generic_search_term.as_deref(),
|
||||
generic_search_term: filter.generic_search_term.clone(),
|
||||
room_types: filter.room_types.clone(),
|
||||
},
|
||||
room_network: RoomNetwork::Matrix,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(get_public_rooms_filtered::Response {
|
||||
chunk: response
|
||||
.chunk
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
// Convert ruma::api::federation::directory::get_public_rooms::v1::PublicRoomsChunk
|
||||
// to ruma::api::client::r0::directory::PublicRoomsChunk
|
||||
serde_json::from_str(
|
||||
&serde_json::to_string(&c)
|
||||
.expect("PublicRoomsChunk::to_string always works"),
|
||||
)
|
||||
.expect("federation and client-server PublicRoomsChunk are the same type")
|
||||
})
|
||||
.collect(),
|
||||
return Ok(get_public_rooms_filtered::v3::Response {
|
||||
chunk: response.chunk,
|
||||
prev_batch: response.prev_batch,
|
||||
next_batch: response.next_batch,
|
||||
total_room_count_estimate: response.total_room_count_estimate,
|
||||
}
|
||||
.into());
|
||||
});
|
||||
}
|
||||
|
||||
let limit = limit.map_or(10, u64::from);
|
||||
|
@ -221,17 +183,18 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
|||
}
|
||||
}
|
||||
|
||||
let mut all_rooms: Vec<_> = db
|
||||
let mut all_rooms: Vec<_> = services()
|
||||
.rooms
|
||||
.directory
|
||||
.public_rooms()
|
||||
.map(|room_id| {
|
||||
let room_id = room_id?;
|
||||
|
||||
let chunk = PublicRoomsChunk {
|
||||
aliases: Vec::new(),
|
||||
canonical_alias: db
|
||||
canonical_alias: services()
|
||||
.rooms
|
||||
.room_state_get(&room_id, &EventType::RoomCanonicalAlias, "")?
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomCanonicalAlias, "")?
|
||||
.map_or(Ok(None), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomCanonicalAliasEventContent| c.alias)
|
||||
|
@ -239,18 +202,10 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
|||
Error::bad_database("Invalid canonical alias event in database.")
|
||||
})
|
||||
})?,
|
||||
name: db
|
||||
.rooms
|
||||
.room_state_get(&room_id, &EventType::RoomName, "")?
|
||||
.map_or(Ok(None), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomNameEventContent| c.name)
|
||||
.map_err(|_| {
|
||||
Error::bad_database("Invalid room name event in database.")
|
||||
})
|
||||
})?,
|
||||
num_joined_members: db
|
||||
name: services().rooms.state_accessor.get_name(&room_id)?,
|
||||
num_joined_members: services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_joined_count(&room_id)?
|
||||
.unwrap_or_else(|| {
|
||||
warn!("Room {} has no member count", room_id);
|
||||
|
@ -258,19 +213,22 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
|||
})
|
||||
.try_into()
|
||||
.expect("user count should not be that big"),
|
||||
topic: db
|
||||
topic: services()
|
||||
.rooms
|
||||
.room_state_get(&room_id, &EventType::RoomTopic, "")?
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomTopic, "")?
|
||||
.map_or(Ok(None), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomTopicEventContent| Some(c.topic))
|
||||
.map_err(|_| {
|
||||
error!("Invalid room topic event in database for room {}", room_id);
|
||||
Error::bad_database("Invalid room topic event in database.")
|
||||
})
|
||||
})?,
|
||||
world_readable: db
|
||||
world_readable: services()
|
||||
.rooms
|
||||
.room_state_get(&room_id, &EventType::RoomHistoryVisibility, "")?
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomHistoryVisibility, "")?
|
||||
.map_or(Ok(false), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomHistoryVisibilityEventContent| {
|
||||
|
@ -282,9 +240,10 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
|||
)
|
||||
})
|
||||
})?,
|
||||
guest_can_join: db
|
||||
guest_can_join: services()
|
||||
.rooms
|
||||
.room_state_get(&room_id, &EventType::RoomGuestAccess, "")?
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomGuestAccess, "")?
|
||||
.map_or(Ok(false), |s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomGuestAccessEventContent| {
|
||||
|
@ -294,9 +253,10 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
|||
Error::bad_database("Invalid room guest access event in database.")
|
||||
})
|
||||
})?,
|
||||
avatar_url: db
|
||||
avatar_url: services()
|
||||
.rooms
|
||||
.room_state_get(&room_id, &EventType::RoomAvatar, "")?
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomAvatar, "")?
|
||||
.map(|s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomAvatarEventContent| c.url)
|
||||
|
@ -307,6 +267,39 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
|||
.transpose()?
|
||||
// url is now an Option<String> so we must flatten
|
||||
.flatten(),
|
||||
join_rule: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomJoinRules, "")?
|
||||
.map(|s| {
|
||||
serde_json::from_str(s.content.get())
|
||||
.map(|c: RoomJoinRulesEventContent| match c.join_rule {
|
||||
JoinRule::Public => Some(PublicRoomJoinRule::Public),
|
||||
JoinRule::Knock => Some(PublicRoomJoinRule::Knock),
|
||||
_ => None,
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!("Invalid room join rule event in database: {}", e);
|
||||
Error::BadDatabase("Invalid room join rule event in database.")
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
.flatten()
|
||||
.ok_or_else(|| Error::bad_database("Missing room join rule event for room."))?,
|
||||
room_type: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room_id, &StateEventType::RoomCreate, "")?
|
||||
.map(|s| {
|
||||
serde_json::from_str::<RoomCreateEventContent>(s.content.get()).map_err(
|
||||
|e| {
|
||||
error!("Invalid room create event in database: {}", e);
|
||||
Error::BadDatabase("Invalid room create event in database.")
|
||||
},
|
||||
)
|
||||
})
|
||||
.transpose()?
|
||||
.and_then(|e| e.room_type),
|
||||
room_id,
|
||||
};
|
||||
Ok(chunk)
|
||||
|
@ -358,7 +351,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
|||
let prev_batch = if num_since == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(format!("p{}", num_since))
|
||||
Some(format!("p{num_since}"))
|
||||
};
|
||||
|
||||
let next_batch = if chunk.len() < limit as usize {
|
||||
|
@ -367,11 +360,10 @@ pub(crate) async fn get_public_rooms_filtered_helper(
|
|||
Some(format!("n{}", num_since + limit))
|
||||
};
|
||||
|
||||
Ok(get_public_rooms_filtered::Response {
|
||||
Ok(get_public_rooms_filtered::v3::Response {
|
||||
chunk,
|
||||
prev_batch,
|
||||
next_batch,
|
||||
total_room_count_estimate: Some(total_room_count_estimate),
|
||||
}
|
||||
.into())
|
||||
})
|
||||
}
|
34
src/api/client_server/filter.rs
Normal file
34
src/api/client_server/filter.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
filter::{create_filter, get_filter},
|
||||
};
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/filter/{filterId}`
|
||||
///
|
||||
/// Loads a filter that was previously created.
|
||||
///
|
||||
/// - A user can only access their own filters
|
||||
pub async fn get_filter_route(
|
||||
body: Ruma<get_filter::v3::Request>,
|
||||
) -> Result<get_filter::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let filter = match services().users.get_filter(sender_user, &body.filter_id)? {
|
||||
Some(filter) => filter,
|
||||
None => return Err(Error::BadRequest(ErrorKind::NotFound, "Filter not found.")),
|
||||
};
|
||||
|
||||
Ok(get_filter::v3::Response::new(filter))
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/filter`
|
||||
///
|
||||
/// Creates a new filter to be used by other endpoints.
|
||||
pub async fn create_filter_route(
|
||||
body: Ruma<create_filter::v3::Request>,
|
||||
) -> Result<create_filter::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
Ok(create_filter::v3::Response::new(
|
||||
services().users.create_filter(sender_user, &body.filter)?,
|
||||
))
|
||||
}
|
|
@ -1,28 +1,27 @@
|
|||
use super::SESSION_ID_LENGTH;
|
||||
use crate::{database::DatabaseGuard, utils, ConduitResult, Database, Error, Result, Ruma};
|
||||
use rocket::futures::{prelude::*, stream::FuturesUnordered};
|
||||
use crate::{services, utils, Error, Result, Ruma};
|
||||
use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
r0::{
|
||||
keys::{
|
||||
claim_keys, get_key_changes, get_keys, upload_keys, upload_signatures,
|
||||
upload_signing_keys,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
keys::{
|
||||
claim_keys, get_key_changes, get_keys, upload_keys, upload_signatures,
|
||||
upload_signing_keys,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
||||
},
|
||||
federation,
|
||||
},
|
||||
serde::Raw,
|
||||
DeviceId, DeviceKeyAlgorithm, UserId,
|
||||
DeviceKeyAlgorithm, OwnedDeviceId, OwnedUserId, UserId,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, post};
|
||||
use std::{
|
||||
collections::{hash_map, BTreeMap, HashMap, HashSet},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/upload`
|
||||
///
|
||||
|
@ -30,47 +29,37 @@ use rocket::{get, post};
|
|||
///
|
||||
/// - Adds one time keys
|
||||
/// - If there are no device keys yet: Adds device keys (TODO: merge with existing keys?)
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/keys/upload", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn upload_keys_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<upload_keys::Request>,
|
||||
) -> ConduitResult<upload_keys::Response> {
|
||||
body: Ruma<upload_keys::v3::Request>,
|
||||
) -> Result<upload_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
for (key_key, key_value) in &body.one_time_keys {
|
||||
db.users
|
||||
.add_one_time_key(sender_user, sender_device, key_key, key_value, &db.globals)?;
|
||||
services()
|
||||
.users
|
||||
.add_one_time_key(sender_user, sender_device, key_key, key_value)?;
|
||||
}
|
||||
|
||||
if let Some(device_keys) = &body.device_keys {
|
||||
// TODO: merge this and the existing event?
|
||||
// This check is needed to assure that signatures are kept
|
||||
if db
|
||||
if services()
|
||||
.users
|
||||
.get_device_keys(sender_user, sender_device)?
|
||||
.is_none()
|
||||
{
|
||||
db.users.add_device_keys(
|
||||
sender_user,
|
||||
sender_device,
|
||||
device_keys,
|
||||
&db.rooms,
|
||||
&db.globals,
|
||||
)?;
|
||||
services()
|
||||
.users
|
||||
.add_device_keys(sender_user, sender_device, device_keys)?;
|
||||
}
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(upload_keys::Response {
|
||||
one_time_key_counts: db.users.count_one_time_keys(sender_user, sender_device)?,
|
||||
}
|
||||
.into())
|
||||
Ok(upload_keys::v3::Response {
|
||||
one_time_key_counts: services()
|
||||
.users
|
||||
.count_one_time_keys(sender_user, sender_device)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/query`
|
||||
|
@ -80,45 +69,24 @@ pub async fn upload_keys_route(
|
|||
/// - Always fetches users from other servers over federation
|
||||
/// - Gets master keys, self-signing keys, user signing keys and device keys.
|
||||
/// - The master and self-signing keys contain signatures that the user is allowed to see
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/keys/query", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_keys_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_keys::Request<'_>>,
|
||||
) -> ConduitResult<get_keys::Response> {
|
||||
pub async fn get_keys_route(body: Ruma<get_keys::v3::Request>) -> Result<get_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let response = get_keys_helper(
|
||||
Some(sender_user),
|
||||
&body.device_keys,
|
||||
|u| u == sender_user,
|
||||
&db,
|
||||
)
|
||||
.await?;
|
||||
let response =
|
||||
get_keys_helper(Some(sender_user), &body.device_keys, |u| u == sender_user).await?;
|
||||
|
||||
Ok(response.into())
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/claim`
|
||||
///
|
||||
/// Claims one-time keys
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/keys/claim", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn claim_keys_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<claim_keys::Request>,
|
||||
) -> ConduitResult<claim_keys::Response> {
|
||||
let response = claim_keys_helper(&body.one_time_keys, &db).await?;
|
||||
body: Ruma<claim_keys::v3::Request>,
|
||||
) -> Result<claim_keys::v3::Response> {
|
||||
let response = claim_keys_helper(&body.one_time_keys).await?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(response.into())
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/device_signing/upload`
|
||||
|
@ -126,15 +94,9 @@ pub async fn claim_keys_route(
|
|||
/// Uploads end-to-end key information for the sender user.
|
||||
///
|
||||
/// - Requires UIAA to verify password
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/unstable/keys/device_signing/upload", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn upload_signing_keys_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<upload_signing_keys::Request<'_>>,
|
||||
) -> ConduitResult<upload_signing_keys::Response> {
|
||||
body: Ruma<upload_signing_keys::v3::Request>,
|
||||
) -> Result<upload_signing_keys::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
|
@ -150,21 +112,18 @@ pub async fn upload_signing_keys_route(
|
|||
};
|
||||
|
||||
if let Some(auth) = &body.auth {
|
||||
let (worked, uiaainfo) = db.uiaa.try_auth(
|
||||
sender_user,
|
||||
sender_device,
|
||||
auth,
|
||||
&uiaainfo,
|
||||
&db.users,
|
||||
&db.globals,
|
||||
)?;
|
||||
let (worked, uiaainfo) =
|
||||
services()
|
||||
.uiaa
|
||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)?;
|
||||
if !worked {
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
}
|
||||
// Success!
|
||||
} else if let Some(json) = body.json_body {
|
||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
||||
db.uiaa
|
||||
services()
|
||||
.uiaa
|
||||
.create(sender_user, sender_device, &uiaainfo, &json)?;
|
||||
return Err(Error::Uiaa(uiaainfo));
|
||||
} else {
|
||||
|
@ -172,38 +131,32 @@ pub async fn upload_signing_keys_route(
|
|||
}
|
||||
|
||||
if let Some(master_key) = &body.master_key {
|
||||
db.users.add_cross_signing_keys(
|
||||
services().users.add_cross_signing_keys(
|
||||
sender_user,
|
||||
master_key,
|
||||
&body.self_signing_key,
|
||||
&body.user_signing_key,
|
||||
&db.rooms,
|
||||
&db.globals,
|
||||
true, // notify so that other users see the new keys
|
||||
)?;
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(upload_signing_keys::Response {}.into())
|
||||
Ok(upload_signing_keys::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/signatures/upload`
|
||||
///
|
||||
/// Uploads end-to-end key signatures from the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/unstable/keys/signatures/upload", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn upload_signatures_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<upload_signatures::Request>,
|
||||
) -> ConduitResult<upload_signatures::Response> {
|
||||
body: Ruma<upload_signatures::v3::Request>,
|
||||
) -> Result<upload_signatures::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
for (user_id, signed_keys) in &body.signed_keys {
|
||||
for (key_id, signed_key) in signed_keys {
|
||||
for signature in signed_key
|
||||
for (user_id, keys) in &body.signed_keys {
|
||||
for (key_id, key) in keys {
|
||||
let key = serde_json::to_value(key)
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid key JSON"))?;
|
||||
|
||||
for signature in key
|
||||
.get("signatures")
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
|
@ -234,21 +187,16 @@ pub async fn upload_signatures_route(
|
|||
))?
|
||||
.to_owned(),
|
||||
);
|
||||
db.users.sign_key(
|
||||
user_id,
|
||||
key_id,
|
||||
signature,
|
||||
sender_user,
|
||||
&db.rooms,
|
||||
&db.globals,
|
||||
)?;
|
||||
services()
|
||||
.users
|
||||
.sign_key(user_id, key_id, signature, sender_user)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(upload_signatures::Response {}.into())
|
||||
Ok(upload_signatures::v3::Response {
|
||||
failures: BTreeMap::new(), // TODO: integrate
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/keys/changes`
|
||||
|
@ -256,21 +204,16 @@ pub async fn upload_signatures_route(
|
|||
/// Gets a list of users who have updated their device identity keys since the previous sync token.
|
||||
///
|
||||
/// - TODO: left users
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/keys/changes", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_key_changes_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_key_changes::Request<'_>>,
|
||||
) -> ConduitResult<get_key_changes::Response> {
|
||||
body: Ruma<get_key_changes::v3::Request>,
|
||||
) -> Result<get_key_changes::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut device_list_updates = HashSet::new();
|
||||
|
||||
device_list_updates.extend(
|
||||
db.users
|
||||
services()
|
||||
.users
|
||||
.keys_changed(
|
||||
sender_user.as_str(),
|
||||
body.from
|
||||
|
@ -285,11 +228,17 @@ pub async fn get_key_changes_route(
|
|||
.filter_map(|r| r.ok()),
|
||||
);
|
||||
|
||||
for room_id in db.rooms.rooms_joined(sender_user).filter_map(|r| r.ok()) {
|
||||
for room_id in services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(|r| r.ok())
|
||||
{
|
||||
device_list_updates.extend(
|
||||
db.users
|
||||
services()
|
||||
.users
|
||||
.keys_changed(
|
||||
&room_id.to_string(),
|
||||
room_id.as_ref(),
|
||||
body.from.parse().map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from`.")
|
||||
})?,
|
||||
|
@ -300,19 +249,17 @@ pub async fn get_key_changes_route(
|
|||
.filter_map(|r| r.ok()),
|
||||
);
|
||||
}
|
||||
Ok(get_key_changes::Response {
|
||||
Ok(get_key_changes::v3::Response {
|
||||
changed: device_list_updates.into_iter().collect(),
|
||||
left: Vec::new(), // TODO
|
||||
}
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
|
||||
sender_user: Option<&UserId>,
|
||||
device_keys_input: &BTreeMap<Box<UserId>, Vec<Box<DeviceId>>>,
|
||||
device_keys_input: &BTreeMap<OwnedUserId, Vec<OwnedDeviceId>>,
|
||||
allowed_signatures: F,
|
||||
db: &Database,
|
||||
) -> Result<get_keys::Response> {
|
||||
) -> Result<get_keys::v3::Response> {
|
||||
let mut master_keys = BTreeMap::new();
|
||||
let mut self_signing_keys = BTreeMap::new();
|
||||
let mut user_signing_keys = BTreeMap::new();
|
||||
|
@ -321,9 +268,9 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
|
|||
let mut get_over_federation = HashMap::new();
|
||||
|
||||
for (user_id, device_ids) in device_keys_input {
|
||||
let user_id: &UserId = &**user_id;
|
||||
let user_id: &UserId = user_id;
|
||||
|
||||
if user_id.server_name() != db.globals.server_name() {
|
||||
if user_id.server_name() != services().globals.server_name() {
|
||||
get_over_federation
|
||||
.entry(user_id.server_name())
|
||||
.or_insert_with(Vec::new)
|
||||
|
@ -333,10 +280,10 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
|
|||
|
||||
if device_ids.is_empty() {
|
||||
let mut container = BTreeMap::new();
|
||||
for device_id in db.users.all_device_ids(user_id) {
|
||||
for device_id in services().users.all_device_ids(user_id) {
|
||||
let device_id = device_id?;
|
||||
if let Some(mut keys) = db.users.get_device_keys(user_id, &device_id)? {
|
||||
let metadata = db
|
||||
if let Some(mut keys) = services().users.get_device_keys(user_id, &device_id)? {
|
||||
let metadata = services()
|
||||
.users
|
||||
.get_device_metadata(user_id, &device_id)?
|
||||
.ok_or_else(|| {
|
||||
|
@ -352,13 +299,14 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
|
|||
} else {
|
||||
for device_id in device_ids {
|
||||
let mut container = BTreeMap::new();
|
||||
if let Some(mut keys) = db.users.get_device_keys(user_id, device_id)? {
|
||||
let metadata = db.users.get_device_metadata(user_id, device_id)?.ok_or(
|
||||
Error::BadRequest(
|
||||
if let Some(mut keys) = services().users.get_device_keys(user_id, device_id)? {
|
||||
let metadata = services()
|
||||
.users
|
||||
.get_device_metadata(user_id, device_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Tried to get keys for nonexistent device.",
|
||||
),
|
||||
)?;
|
||||
))?;
|
||||
|
||||
add_unsigned_device_display_name(&mut keys, metadata)
|
||||
.map_err(|_| Error::bad_database("invalid device keys in database"))?;
|
||||
|
@ -368,17 +316,22 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(master_key) = db.users.get_master_key(user_id, &allowed_signatures)? {
|
||||
if let Some(master_key) =
|
||||
services()
|
||||
.users
|
||||
.get_master_key(sender_user, user_id, &allowed_signatures)?
|
||||
{
|
||||
master_keys.insert(user_id.to_owned(), master_key);
|
||||
}
|
||||
if let Some(self_signing_key) = db
|
||||
.users
|
||||
.get_self_signing_key(user_id, &allowed_signatures)?
|
||||
if let Some(self_signing_key) =
|
||||
services()
|
||||
.users
|
||||
.get_self_signing_key(sender_user, user_id, &allowed_signatures)?
|
||||
{
|
||||
self_signing_keys.insert(user_id.to_owned(), self_signing_key);
|
||||
}
|
||||
if Some(user_id) == sender_user {
|
||||
if let Some(user_signing_key) = db.users.get_user_signing_key(user_id)? {
|
||||
if let Some(user_signing_key) = services().users.get_user_signing_key(user_id)? {
|
||||
user_signing_keys.insert(user_id.to_owned(), user_signing_key);
|
||||
}
|
||||
}
|
||||
|
@ -386,42 +339,102 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
|
|||
|
||||
let mut failures = BTreeMap::new();
|
||||
|
||||
let back_off = |id| match services()
|
||||
.globals
|
||||
.bad_query_ratelimiter
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(id)
|
||||
{
|
||||
hash_map::Entry::Vacant(e) => {
|
||||
e.insert((Instant::now(), 1));
|
||||
}
|
||||
hash_map::Entry::Occupied(mut e) => *e.get_mut() = (Instant::now(), e.get().1 + 1),
|
||||
};
|
||||
|
||||
let mut futures: FuturesUnordered<_> = get_over_federation
|
||||
.into_iter()
|
||||
.map(|(server, vec)| async move {
|
||||
if let Some((time, tries)) = services()
|
||||
.globals
|
||||
.bad_query_ratelimiter
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(server)
|
||||
{
|
||||
// Exponential backoff
|
||||
let mut min_elapsed_duration = Duration::from_secs(30) * (*tries) * (*tries);
|
||||
if min_elapsed_duration > Duration::from_secs(60 * 60 * 24) {
|
||||
min_elapsed_duration = Duration::from_secs(60 * 60 * 24);
|
||||
}
|
||||
|
||||
if time.elapsed() < min_elapsed_duration {
|
||||
debug!("Backing off query from {:?}", server);
|
||||
return (
|
||||
server,
|
||||
Err(Error::BadServerResponse("bad query, still backing off")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut device_keys_input_fed = BTreeMap::new();
|
||||
for (user_id, keys) in vec {
|
||||
device_keys_input_fed.insert(user_id.to_owned(), keys.clone());
|
||||
}
|
||||
(
|
||||
server,
|
||||
db.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(25),
|
||||
services().sending.send_federation_request(
|
||||
server,
|
||||
federation::keys::get_keys::v1::Request {
|
||||
device_keys: device_keys_input_fed,
|
||||
},
|
||||
)
|
||||
.await,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map_err(|_e| Error::BadServerResponse("Query took too long")),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
while let Some((server, response)) = futures.next().await {
|
||||
match response {
|
||||
Ok(response) => {
|
||||
master_keys.extend(response.master_keys);
|
||||
Ok(Ok(response)) => {
|
||||
for (user, masterkey) in response.master_keys {
|
||||
let (master_key_id, mut master_key) =
|
||||
services().users.parse_master_key(&user, &masterkey)?;
|
||||
|
||||
if let Some(our_master_key) = services().users.get_key(
|
||||
&master_key_id,
|
||||
sender_user,
|
||||
&user,
|
||||
&allowed_signatures,
|
||||
)? {
|
||||
let (_, our_master_key) =
|
||||
services().users.parse_master_key(&user, &our_master_key)?;
|
||||
master_key.signatures.extend(our_master_key.signatures);
|
||||
}
|
||||
let json = serde_json::to_value(master_key).expect("to_value always works");
|
||||
let raw = serde_json::from_value(json).expect("Raw::from_value always works");
|
||||
services().users.add_cross_signing_keys(
|
||||
&user, &raw, &None, &None,
|
||||
false, // Dont notify. A notification would trigger another key request resulting in an endless loop
|
||||
)?;
|
||||
master_keys.insert(user, raw);
|
||||
}
|
||||
|
||||
self_signing_keys.extend(response.self_signing_keys);
|
||||
device_keys.extend(response.device_keys);
|
||||
}
|
||||
Err(_e) => {
|
||||
_ => {
|
||||
back_off(server.to_owned());
|
||||
failures.insert(server.to_string(), json!({}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(get_keys::Response {
|
||||
Ok(get_keys::v3::Response {
|
||||
master_keys,
|
||||
self_signing_keys,
|
||||
user_signing_keys,
|
||||
|
@ -432,7 +445,7 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
|
|||
|
||||
fn add_unsigned_device_display_name(
|
||||
keys: &mut Raw<ruma::encryption::DeviceKeys>,
|
||||
metadata: ruma::api::client::r0::device::Device,
|
||||
metadata: ruma::api::client::device::Device,
|
||||
) -> serde_json::Result<()> {
|
||||
if let Some(display_name) = metadata.display_name {
|
||||
let mut object = keys.deserialize_as::<serde_json::Map<String, serde_json::Value>>()?;
|
||||
|
@ -449,15 +462,14 @@ fn add_unsigned_device_display_name(
|
|||
}
|
||||
|
||||
pub(crate) async fn claim_keys_helper(
|
||||
one_time_keys_input: &BTreeMap<Box<UserId>, BTreeMap<Box<DeviceId>, DeviceKeyAlgorithm>>,
|
||||
db: &Database,
|
||||
) -> Result<claim_keys::Response> {
|
||||
one_time_keys_input: &BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceId, DeviceKeyAlgorithm>>,
|
||||
) -> Result<claim_keys::v3::Response> {
|
||||
let mut one_time_keys = BTreeMap::new();
|
||||
|
||||
let mut get_over_federation = BTreeMap::new();
|
||||
|
||||
for (user_id, map) in one_time_keys_input {
|
||||
if user_id.server_name() != db.globals.server_name() {
|
||||
if user_id.server_name() != services().globals.server_name() {
|
||||
get_over_federation
|
||||
.entry(user_id.server_name())
|
||||
.or_insert_with(Vec::new)
|
||||
|
@ -467,8 +479,9 @@ pub(crate) async fn claim_keys_helper(
|
|||
let mut container = BTreeMap::new();
|
||||
for (device_id, key_algorithm) in map {
|
||||
if let Some(one_time_keys) =
|
||||
db.users
|
||||
.take_one_time_key(user_id, device_id, key_algorithm, &db.globals)?
|
||||
services()
|
||||
.users
|
||||
.take_one_time_key(user_id, device_id, key_algorithm)?
|
||||
{
|
||||
let mut c = BTreeMap::new();
|
||||
c.insert(one_time_keys.0, one_time_keys.1);
|
||||
|
@ -480,30 +493,40 @@ pub(crate) async fn claim_keys_helper(
|
|||
|
||||
let mut failures = BTreeMap::new();
|
||||
|
||||
for (server, vec) in get_over_federation {
|
||||
let mut one_time_keys_input_fed = BTreeMap::new();
|
||||
for (user_id, keys) in vec {
|
||||
one_time_keys_input_fed.insert(user_id.clone(), keys.clone());
|
||||
}
|
||||
// Ignore failures
|
||||
if let Ok(keys) = db
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
let mut futures: FuturesUnordered<_> = get_over_federation
|
||||
.into_iter()
|
||||
.map(|(server, vec)| async move {
|
||||
let mut one_time_keys_input_fed = BTreeMap::new();
|
||||
for (user_id, keys) in vec {
|
||||
one_time_keys_input_fed.insert(user_id.clone(), keys.clone());
|
||||
}
|
||||
(
|
||||
server,
|
||||
federation::keys::claim_keys::v1::Request {
|
||||
one_time_keys: one_time_keys_input_fed,
|
||||
},
|
||||
services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
server,
|
||||
federation::keys::claim_keys::v1::Request {
|
||||
one_time_keys: one_time_keys_input_fed,
|
||||
},
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await
|
||||
{
|
||||
one_time_keys.extend(keys.one_time_keys);
|
||||
} else {
|
||||
failures.insert(server.to_string(), json!({}));
|
||||
})
|
||||
.collect();
|
||||
|
||||
while let Some((server, response)) = futures.next().await {
|
||||
match response {
|
||||
Ok(keys) => {
|
||||
one_time_keys.extend(keys.one_time_keys);
|
||||
}
|
||||
Err(_e) => {
|
||||
failures.insert(server.to_string(), json!({}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(claim_keys::Response {
|
||||
Ok(claim_keys::v3::Response {
|
||||
failures,
|
||||
one_time_keys,
|
||||
})
|
|
@ -1,32 +1,25 @@
|
|||
use crate::{
|
||||
database::{media::FileMeta, DatabaseGuard},
|
||||
utils, ConduitResult, Error, Ruma,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma};
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
r0::media::{
|
||||
media::{
|
||||
create_content, get_content, get_content_as_filename, get_content_thumbnail,
|
||||
get_media_config,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, post};
|
||||
|
||||
const MXC_LENGTH: usize = 32;
|
||||
|
||||
/// # `GET /_matrix/media/r0/config`
|
||||
///
|
||||
/// Returns max upload size.
|
||||
#[cfg_attr(feature = "conduit_bin", get("/_matrix/media/r0/config"))]
|
||||
#[tracing::instrument(skip(db))]
|
||||
pub async fn get_media_config_route(
|
||||
db: DatabaseGuard,
|
||||
) -> ConduitResult<get_media_config::Response> {
|
||||
Ok(get_media_config::Response {
|
||||
upload_size: db.globals.max_request_size().into(),
|
||||
}
|
||||
.into())
|
||||
_body: Ruma<get_media_config::v3::Request>,
|
||||
) -> Result<get_media_config::v3::Response> {
|
||||
Ok(get_media_config::v3::Response {
|
||||
upload_size: services().globals.max_request_size().into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/media/r0/upload`
|
||||
|
@ -35,69 +28,59 @@ pub async fn get_media_config_route(
|
|||
///
|
||||
/// - Some metadata will be saved in the database
|
||||
/// - Media will be saved in the media/ directory
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/media/r0/upload", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn create_content_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<create_content::Request<'_>>,
|
||||
) -> ConduitResult<create_content::Response> {
|
||||
body: Ruma<create_content::v3::Request>,
|
||||
) -> Result<create_content::v3::Response> {
|
||||
let mxc = format!(
|
||||
"mxc://{}/{}",
|
||||
db.globals.server_name(),
|
||||
services().globals.server_name(),
|
||||
utils::random_string(MXC_LENGTH)
|
||||
);
|
||||
|
||||
db.media
|
||||
services()
|
||||
.media
|
||||
.create(
|
||||
mxc.clone(),
|
||||
&db.globals,
|
||||
&body
|
||||
.filename
|
||||
body.filename
|
||||
.as_ref()
|
||||
.map(|filename| "inline; filename=".to_owned() + filename)
|
||||
.as_deref(),
|
||||
&body.content_type.as_deref(),
|
||||
body.content_type.as_deref(),
|
||||
&body.file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(create_content::Response {
|
||||
content_uri: mxc.try_into().expect("Invalid mxc:// URI"),
|
||||
Ok(create_content::v3::Response {
|
||||
content_uri: mxc.into(),
|
||||
blurhash: None,
|
||||
}
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_remote_content(
|
||||
db: &DatabaseGuard,
|
||||
mxc: &str,
|
||||
server_name: &ruma::ServerName,
|
||||
media_id: &str,
|
||||
) -> Result<get_content::Response, Error> {
|
||||
let content_response = db
|
||||
media_id: String,
|
||||
) -> Result<get_content::v3::Response, Error> {
|
||||
let content_response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
server_name,
|
||||
get_content::Request {
|
||||
get_content::v3::Request {
|
||||
allow_remote: false,
|
||||
server_name,
|
||||
server_name: server_name.to_owned(),
|
||||
media_id,
|
||||
timeout_ms: Duration::from_secs(20),
|
||||
allow_redirect: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
db.media
|
||||
services()
|
||||
.media
|
||||
.create(
|
||||
mxc.to_string(),
|
||||
&db.globals,
|
||||
&content_response.content_disposition.as_deref(),
|
||||
&content_response.content_type.as_deref(),
|
||||
mxc.to_owned(),
|
||||
content_response.content_disposition.as_deref(),
|
||||
content_response.content_type.as_deref(),
|
||||
&content_response.file,
|
||||
)
|
||||
.await?;
|
||||
|
@ -110,33 +93,27 @@ pub async fn get_remote_content(
|
|||
/// Load media from our server or over federation.
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/media/r0/download/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_content_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_content::Request<'_>>,
|
||||
) -> ConduitResult<get_content::Response> {
|
||||
body: Ruma<get_content::v3::Request>,
|
||||
) -> Result<get_content::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
|
||||
if let Some(FileMeta {
|
||||
content_disposition,
|
||||
content_type,
|
||||
file,
|
||||
}) = db.media.get(&db.globals, &mxc).await?
|
||||
}) = services().media.get(mxc.clone()).await?
|
||||
{
|
||||
Ok(get_content::Response {
|
||||
Ok(get_content::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition,
|
||||
}
|
||||
.into())
|
||||
} else if &*body.server_name != db.globals.server_name() && body.allow_remote {
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
} else if &*body.server_name != services().globals.server_name() && body.allow_remote {
|
||||
let remote_content_response =
|
||||
get_remote_content(&db, &mxc, &body.server_name, &body.media_id).await?;
|
||||
Ok(remote_content_response.into())
|
||||
get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?;
|
||||
Ok(remote_content_response)
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
}
|
||||
|
@ -147,39 +124,33 @@ pub async fn get_content_route(
|
|||
/// Load media from our server or over federation, permitting desired filename.
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/media/r0/download/<_>/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_content_as_filename_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_content_as_filename::Request<'_>>,
|
||||
) -> ConduitResult<get_content_as_filename::Response> {
|
||||
body: Ruma<get_content_as_filename::v3::Request>,
|
||||
) -> Result<get_content_as_filename::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
|
||||
if let Some(FileMeta {
|
||||
content_disposition: _,
|
||||
content_type,
|
||||
file,
|
||||
}) = db.media.get(&db.globals, &mxc).await?
|
||||
}) = services().media.get(mxc.clone()).await?
|
||||
{
|
||||
Ok(get_content_as_filename::Response {
|
||||
Ok(get_content_as_filename::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
content_disposition: Some(format!("inline; filename={}", body.filename)),
|
||||
}
|
||||
.into())
|
||||
} else if &*body.server_name != db.globals.server_name() && body.allow_remote {
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
} else if &*body.server_name != services().globals.server_name() && body.allow_remote {
|
||||
let remote_content_response =
|
||||
get_remote_content(&db, &mxc, &body.server_name, &body.media_id).await?;
|
||||
get_remote_content(&mxc, &body.server_name, body.media_id.clone()).await?;
|
||||
|
||||
Ok(get_content_as_filename::Response {
|
||||
Ok(get_content_as_filename::v3::Response {
|
||||
content_disposition: Some(format!("inline: filename={}", body.filename)),
|
||||
content_type: remote_content_response.content_type,
|
||||
file: remote_content_response.file,
|
||||
}
|
||||
.into())
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
}
|
||||
|
@ -190,24 +161,17 @@ pub async fn get_content_as_filename_route(
|
|||
/// Load media thumbnail from our server or over federation.
|
||||
///
|
||||
/// - Only allows federation if `allow_remote` is true
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/media/r0/thumbnail/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_content_thumbnail_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_content_thumbnail::Request<'_>>,
|
||||
) -> ConduitResult<get_content_thumbnail::Response> {
|
||||
body: Ruma<get_content_thumbnail::v3::Request>,
|
||||
) -> Result<get_content_thumbnail::v3::Response> {
|
||||
let mxc = format!("mxc://{}/{}", body.server_name, body.media_id);
|
||||
|
||||
if let Some(FileMeta {
|
||||
content_type, file, ..
|
||||
}) = db
|
||||
}) = services()
|
||||
.media
|
||||
.get_thumbnail(
|
||||
&mxc,
|
||||
&db.globals,
|
||||
mxc.clone(),
|
||||
body.width
|
||||
.try_into()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Width is invalid."))?,
|
||||
|
@ -217,37 +181,42 @@ pub async fn get_content_thumbnail_route(
|
|||
)
|
||||
.await?
|
||||
{
|
||||
Ok(get_content_thumbnail::Response { file, content_type }.into())
|
||||
} else if &*body.server_name != db.globals.server_name() && body.allow_remote {
|
||||
let get_thumbnail_response = db
|
||||
Ok(get_content_thumbnail::v3::Response {
|
||||
file,
|
||||
content_type,
|
||||
cross_origin_resource_policy: Some("cross-origin".to_owned()),
|
||||
})
|
||||
} else if &*body.server_name != services().globals.server_name() && body.allow_remote {
|
||||
let get_thumbnail_response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
&body.server_name,
|
||||
get_content_thumbnail::Request {
|
||||
get_content_thumbnail::v3::Request {
|
||||
allow_remote: false,
|
||||
height: body.height,
|
||||
width: body.width,
|
||||
method: body.method.clone(),
|
||||
server_name: &body.server_name,
|
||||
media_id: &body.media_id,
|
||||
server_name: body.server_name.clone(),
|
||||
media_id: body.media_id.clone(),
|
||||
timeout_ms: Duration::from_secs(20),
|
||||
allow_redirect: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
db.media
|
||||
services()
|
||||
.media
|
||||
.upload_thumbnail(
|
||||
mxc,
|
||||
&db.globals,
|
||||
&None,
|
||||
&get_thumbnail_response.content_type,
|
||||
None,
|
||||
get_thumbnail_response.content_type.as_deref(),
|
||||
body.width.try_into().expect("all UInts are valid u32s"),
|
||||
body.height.try_into().expect("all UInts are valid u32s"),
|
||||
&get_thumbnail_response.file,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(get_thumbnail_response.into())
|
||||
Ok(get_thumbnail_response)
|
||||
} else {
|
||||
Err(Error::BadRequest(ErrorKind::NotFound, "Media not found."))
|
||||
}
|
1579
src/api/client_server/membership.rs
Normal file
1579
src/api/client_server/membership.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,19 +1,19 @@
|
|||
use crate::{database::DatabaseGuard, pdu::PduBuilder, utils, ConduitResult, Error, Ruma};
|
||||
use crate::{
|
||||
service::{pdu::PduBuilder, rooms::timeline::PduCount},
|
||||
services, utils, Error, Result, Ruma,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::message::{get_message_events, send_message_event},
|
||||
message::{get_message_events, send_message_event},
|
||||
},
|
||||
events::EventType,
|
||||
events::{StateEventType, TimelineEventType},
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, put};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}`
|
||||
///
|
||||
/// Send a message event into the room.
|
||||
|
@ -21,20 +21,15 @@ use rocket::{get, put};
|
|||
/// - Is a NOOP if the txn id was already used before and returns the same event id again
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is allowed
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/rooms/<_>/send/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn send_message_event_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<send_message_event::Request<'_>>,
|
||||
) -> ConduitResult<send_message_event::Response> {
|
||||
body: Ruma<send_message_event::v3::Request>,
|
||||
) -> Result<send_message_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_deref();
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -44,7 +39,9 @@ pub async fn send_message_event_route(
|
|||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
// Forbid m.room.encrypted if encryption is disabled
|
||||
if &body.event_type == "m.room.encrypted" && !db.globals.allow_encryption() {
|
||||
if TimelineEventType::RoomEncrypted == body.event_type.to_string().into()
|
||||
&& !services().globals.allow_encryption()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Encryption has been disabled",
|
||||
|
@ -53,7 +50,8 @@ pub async fn send_message_event_route(
|
|||
|
||||
// Check if this is a new transaction id
|
||||
if let Some(response) =
|
||||
db.transaction_ids
|
||||
services()
|
||||
.transaction_ids
|
||||
.existing_txnid(sender_user, sender_device, &body.txn_id)?
|
||||
{
|
||||
// The client might have sent a txnid of the /sendToDevice endpoint
|
||||
|
@ -69,15 +67,15 @@ pub async fn send_message_event_route(
|
|||
.map_err(|_| Error::bad_database("Invalid txnid bytes in database."))?
|
||||
.try_into()
|
||||
.map_err(|_| Error::bad_database("Invalid event id in txnid data."))?;
|
||||
return Ok(send_message_event::Response { event_id }.into());
|
||||
return Ok(send_message_event::v3::Response { event_id });
|
||||
}
|
||||
|
||||
let mut unsigned = BTreeMap::new();
|
||||
unsigned.insert("transaction_id".to_owned(), body.txn_id.to_string().into());
|
||||
|
||||
let event_id = db.rooms.build_and_append_pdu(
|
||||
let event_id = services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::from(&*body.event_type),
|
||||
event_type: body.event_type.to_string().into(),
|
||||
content: serde_json::from_str(body.body.body.json().get())
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid JSON body."))?,
|
||||
unsigned: Some(unsigned),
|
||||
|
@ -86,11 +84,10 @@ pub async fn send_message_event_route(
|
|||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
db.transaction_ids.add_txnid(
|
||||
services().transaction_ids.add_txnid(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.txn_id,
|
||||
|
@ -99,9 +96,9 @@ pub async fn send_message_event_route(
|
|||
|
||||
drop(state_lock);
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(send_message_event::Response::new((*event_id).to_owned()).into())
|
||||
Ok(send_message_event::v3::Response::new(
|
||||
(*event_id).to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/messages`
|
||||
|
@ -110,63 +107,63 @@ pub async fn send_message_event_route(
|
|||
///
|
||||
/// - Only works if the user is joined (TODO: always allow, but only show events where the user was
|
||||
/// joined, depending on history_visibility)
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/rooms/<_>/messages", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_message_events_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_message_events::Request<'_>>,
|
||||
) -> ConduitResult<get_message_events::Response> {
|
||||
body: Ruma<get_message_events::v3::Request>,
|
||||
) -> Result<get_message_events::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
if !db.rooms.is_joined(sender_user, &body.room_id)? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
let from = match body.from.clone() {
|
||||
Some(from) => PduCount::try_from_string(&from)?,
|
||||
None => match body.dir {
|
||||
ruma::api::Direction::Forward => PduCount::min(),
|
||||
ruma::api::Direction::Backward => PduCount::max(),
|
||||
},
|
||||
};
|
||||
|
||||
let from = body
|
||||
.from
|
||||
.clone()
|
||||
.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from` value."))?;
|
||||
let to = body
|
||||
.to
|
||||
.as_ref()
|
||||
.and_then(|t| PduCount::try_from_string(t).ok());
|
||||
|
||||
let to = body.to.as_ref().map(|t| t.parse());
|
||||
services().rooms.lazy_loading.lazy_load_confirm_delivery(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.room_id,
|
||||
from,
|
||||
)?;
|
||||
|
||||
db.rooms
|
||||
.lazy_load_confirm_delivery(sender_user, sender_device, &body.room_id, from)?;
|
||||
|
||||
// Use limit or else 10
|
||||
let limit = body.limit.try_into().map_or(10_usize, |l: u32| l as usize);
|
||||
let limit = u64::from(body.limit).min(100) as usize;
|
||||
|
||||
let next_token;
|
||||
|
||||
let mut resp = get_message_events::Response::new();
|
||||
let mut resp = get_message_events::v3::Response::new();
|
||||
|
||||
let mut lazy_loaded = HashSet::new();
|
||||
|
||||
match body.dir {
|
||||
get_message_events::Direction::Forward => {
|
||||
let events_after: Vec<_> = db
|
||||
ruma::api::Direction::Forward => {
|
||||
let events_after: Vec<_> = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_after(sender_user, &body.room_id, from)?
|
||||
.take(limit)
|
||||
.filter_map(|r| r.ok()) // Filter out buggy events
|
||||
.filter_map(|(pdu_id, pdu)| {
|
||||
db.rooms
|
||||
.pdu_count(&pdu_id)
|
||||
.map(|pdu_count| (pdu_count, pdu))
|
||||
.ok()
|
||||
.filter(|(_, pdu)| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &body.room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.take_while(|&(k, _)| Some(Ok(k)) != to) // Stop at `to`
|
||||
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_after {
|
||||
if !db.rooms.lazy_load_was_sent_before(
|
||||
/* TODO: Remove this when these are resolved:
|
||||
* https://github.com/vector-im/element-android/issues/3417
|
||||
* https://github.com/vector-im/element-web/issues/21034
|
||||
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.room_id,
|
||||
|
@ -174,6 +171,8 @@ pub async fn get_message_events_route(
|
|||
)? {
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
*/
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
next_token = events_after.last().map(|(count, _)| count).copied();
|
||||
|
@ -183,27 +182,37 @@ pub async fn get_message_events_route(
|
|||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
resp.start = body.from.to_owned();
|
||||
resp.end = next_token.map(|count| count.to_string());
|
||||
resp.start = from.stringify();
|
||||
resp.end = next_token.map(|count| count.stringify());
|
||||
resp.chunk = events_after;
|
||||
}
|
||||
get_message_events::Direction::Backward => {
|
||||
let events_before: Vec<_> = db
|
||||
ruma::api::Direction::Backward => {
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.backfill_if_required(&body.room_id, from)
|
||||
.await?;
|
||||
let events_before: Vec<_> = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.pdus_until(sender_user, &body.room_id, from)?
|
||||
.take(limit)
|
||||
.filter_map(|r| r.ok()) // Filter out buggy events
|
||||
.filter_map(|(pdu_id, pdu)| {
|
||||
db.rooms
|
||||
.pdu_count(&pdu_id)
|
||||
.map(|pdu_count| (pdu_count, pdu))
|
||||
.ok()
|
||||
.filter(|(_, pdu)| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &body.room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.take_while(|&(k, _)| Some(Ok(k)) != to) // Stop at `to`
|
||||
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
|
||||
.collect();
|
||||
|
||||
for (_, event) in &events_before {
|
||||
if !db.rooms.lazy_load_was_sent_before(
|
||||
/* TODO: Remove this when these are resolved:
|
||||
* https://github.com/vector-im/element-android/issues/3417
|
||||
* https://github.com/vector-im/element-web/issues/21034
|
||||
if !services().rooms.lazy_loading.lazy_load_was_sent_before(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.room_id,
|
||||
|
@ -211,6 +220,8 @@ pub async fn get_message_events_route(
|
|||
)? {
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
*/
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
}
|
||||
|
||||
next_token = events_before.last().map(|(count, _)| count).copied();
|
||||
|
@ -220,24 +231,27 @@ pub async fn get_message_events_route(
|
|||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect();
|
||||
|
||||
resp.start = body.from.to_owned();
|
||||
resp.end = next_token.map(|count| count.to_string());
|
||||
resp.start = from.stringify();
|
||||
resp.end = next_token.map(|count| count.stringify());
|
||||
resp.chunk = events_before;
|
||||
}
|
||||
}
|
||||
|
||||
resp.state = Vec::new();
|
||||
for ll_id in &lazy_loaded {
|
||||
if let Some(member_event) =
|
||||
db.rooms
|
||||
.room_state_get(&body.room_id, &EventType::RoomMember, ll_id.as_str())?
|
||||
{
|
||||
if let Some(member_event) = services().rooms.state_accessor.room_state_get(
|
||||
&body.room_id,
|
||||
&StateEventType::RoomMember,
|
||||
ll_id.as_str(),
|
||||
)? {
|
||||
resp.state.push(member_event.to_state_event());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: enable again when we are sure clients can handle it
|
||||
/*
|
||||
if let Some(next_token) = next_token {
|
||||
db.rooms.lazy_load_mark_sent(
|
||||
services().rooms.lazy_loading.lazy_load_mark_sent(
|
||||
sender_user,
|
||||
sender_device,
|
||||
&body.room_id,
|
||||
|
@ -245,6 +259,7 @@ pub async fn get_message_events_route(
|
|||
next_token,
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
Ok(resp.into())
|
||||
Ok(resp)
|
||||
}
|
|
@ -16,14 +16,17 @@ mod profile;
|
|||
mod push;
|
||||
mod read_marker;
|
||||
mod redact;
|
||||
mod relations;
|
||||
mod report;
|
||||
mod room;
|
||||
mod search;
|
||||
mod session;
|
||||
mod space;
|
||||
mod state;
|
||||
mod sync;
|
||||
mod tag;
|
||||
mod thirdparty;
|
||||
mod threads;
|
||||
mod to_device;
|
||||
mod typing;
|
||||
mod unversioned;
|
||||
|
@ -48,37 +51,24 @@ pub use profile::*;
|
|||
pub use push::*;
|
||||
pub use read_marker::*;
|
||||
pub use redact::*;
|
||||
pub use relations::*;
|
||||
pub use report::*;
|
||||
pub use room::*;
|
||||
pub use search::*;
|
||||
pub use session::*;
|
||||
pub use space::*;
|
||||
pub use state::*;
|
||||
pub use sync::*;
|
||||
pub use tag::*;
|
||||
pub use thirdparty::*;
|
||||
pub use threads::*;
|
||||
pub use to_device::*;
|
||||
pub use typing::*;
|
||||
pub use unversioned::*;
|
||||
pub use user_directory::*;
|
||||
pub use voip::*;
|
||||
|
||||
#[cfg(not(feature = "conduit_bin"))]
|
||||
use super::State;
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use {
|
||||
crate::ConduitResult, rocket::options, ruma::api::client::r0::to_device::send_event_to_device,
|
||||
};
|
||||
|
||||
pub const DEVICE_ID_LENGTH: usize = 10;
|
||||
pub const TOKEN_LENGTH: usize = 256;
|
||||
pub const SESSION_ID_LENGTH: usize = 256;
|
||||
|
||||
/// # `OPTIONS`
|
||||
///
|
||||
/// Web clients use this to get CORS headers.
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
#[options("/<_..>")]
|
||||
#[tracing::instrument]
|
||||
pub async fn options_route() -> ConduitResult<send_event_to_device::Response> {
|
||||
Ok(send_event_to_device::Response {}.into())
|
||||
}
|
||||
pub const TOKEN_LENGTH: usize = 32;
|
||||
pub const SESSION_ID_LENGTH: usize = 32;
|
||||
pub const AUTO_GEN_PASSWORD_LENGTH: usize = 15;
|
|
@ -1,35 +1,29 @@
|
|||
use crate::{database::DatabaseGuard, utils, ConduitResult, Ruma};
|
||||
use ruma::api::client::r0::presence::{get_presence, set_presence};
|
||||
use crate::{services, utils, Error, Result, Ruma};
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
presence::{get_presence, set_presence},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, put};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/presence/{userId}/status`
|
||||
///
|
||||
/// Sets the presence state of the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/presence/<_>/status", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_presence_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_presence::Request<'_>>,
|
||||
) -> ConduitResult<set_presence::Response> {
|
||||
body: Ruma<set_presence::v3::Request>,
|
||||
) -> Result<set_presence::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
for room_id in db.rooms.rooms_joined(sender_user) {
|
||||
for room_id in services().rooms.state_cache.rooms_joined(sender_user) {
|
||||
let room_id = room_id?;
|
||||
|
||||
db.rooms.edus.update_presence(
|
||||
services().rooms.edus.presence.update_presence(
|
||||
sender_user,
|
||||
&room_id,
|
||||
ruma::events::presence::PresenceEvent {
|
||||
content: ruma::events::presence::PresenceEventContent {
|
||||
avatar_url: db.users.avatar_url(sender_user)?,
|
||||
avatar_url: services().users.avatar_url(sender_user)?,
|
||||
currently_active: None,
|
||||
displayname: db.users.displayname(sender_user)?,
|
||||
displayname: services().users.displayname(sender_user)?,
|
||||
last_active_ago: Some(
|
||||
utils::millis_since_unix_epoch()
|
||||
.try_into()
|
||||
|
@ -40,13 +34,10 @@ pub async fn set_presence_route(
|
|||
},
|
||||
sender: sender_user.clone(),
|
||||
},
|
||||
&db.globals,
|
||||
)?;
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_presence::Response {}.into())
|
||||
Ok(set_presence::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/presence/{userId}/status`
|
||||
|
@ -54,28 +45,24 @@ pub async fn set_presence_route(
|
|||
/// Gets the presence state of the given user.
|
||||
///
|
||||
/// - Only works if you share a room with the user
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/presence/<_>/status", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_presence_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_presence::Request<'_>>,
|
||||
) -> ConduitResult<get_presence::Response> {
|
||||
body: Ruma<get_presence::v3::Request>,
|
||||
) -> Result<get_presence::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut presence_event = None;
|
||||
|
||||
for room_id in db
|
||||
for room_id in services()
|
||||
.rooms
|
||||
.user
|
||||
.get_shared_rooms(vec![sender_user.clone(), body.user_id.clone()])?
|
||||
{
|
||||
let room_id = room_id?;
|
||||
|
||||
if let Some(presence) = db
|
||||
if let Some(presence) = services()
|
||||
.rooms
|
||||
.edus
|
||||
.presence
|
||||
.get_last_presence_event(sender_user, &room_id)?
|
||||
{
|
||||
presence_event = Some(presence);
|
||||
|
@ -84,7 +71,7 @@ pub async fn get_presence_route(
|
|||
}
|
||||
|
||||
if let Some(presence) = presence_event {
|
||||
Ok(get_presence::Response {
|
||||
Ok(get_presence::v3::Response {
|
||||
// TODO: Should ruma just use the presenceeventcontent type here?
|
||||
status_msg: presence.content.status_msg,
|
||||
currently_active: presence.content.currently_active,
|
||||
|
@ -93,9 +80,11 @@ pub async fn get_presence_route(
|
|||
.last_active_ago
|
||||
.map(|millis| Duration::from_millis(millis.into())),
|
||||
presence: presence.content.presence,
|
||||
}
|
||||
.into())
|
||||
})
|
||||
} else {
|
||||
todo!();
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Presence state for this user was not found",
|
||||
))
|
||||
}
|
||||
}
|
|
@ -1,57 +1,52 @@
|
|||
use crate::{database::DatabaseGuard, pdu::PduBuilder, utils, ConduitResult, Error, Ruma};
|
||||
use crate::{service::pdu::PduBuilder, services, utils, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{
|
||||
error::ErrorKind,
|
||||
r0::profile::{
|
||||
profile::{
|
||||
get_avatar_url, get_display_name, get_profile, set_avatar_url, set_display_name,
|
||||
},
|
||||
},
|
||||
federation::{self, query::get_profile_information::v1::ProfileField},
|
||||
},
|
||||
events::{room::member::RoomMemberEventContent, EventType},
|
||||
events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
|
||||
};
|
||||
use serde_json::value::to_raw_value;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, put};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/profile/{userId}/displayname`
|
||||
///
|
||||
/// Updates the displayname.
|
||||
///
|
||||
/// - Also makes sure other users receive the update using presence EDUs
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/profile/<_>/displayname", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_displayname_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_display_name::Request<'_>>,
|
||||
) -> ConduitResult<set_display_name::Response> {
|
||||
body: Ruma<set_display_name::v3::Request>,
|
||||
) -> Result<set_display_name::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
db.users
|
||||
services()
|
||||
.users
|
||||
.set_displayname(sender_user, body.displayname.clone())?;
|
||||
|
||||
// Send a new membership event and presence update into all joined rooms
|
||||
let all_rooms_joined: Vec<_> = db
|
||||
let all_rooms_joined: Vec<_> = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(|r| r.ok())
|
||||
.map(|room_id| {
|
||||
Ok::<_, Error>((
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomMember,
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
displayname: body.displayname.clone(),
|
||||
..serde_json::from_str(
|
||||
db.rooms
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(
|
||||
&room_id,
|
||||
&EventType::RoomMember,
|
||||
&StateEventType::RoomMember,
|
||||
sender_user.as_str(),
|
||||
)?
|
||||
.ok_or_else(|| {
|
||||
|
@ -78,7 +73,8 @@ pub async fn set_displayname_route(
|
|||
|
||||
for (pdu_builder, room_id) in all_rooms_joined {
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -87,19 +83,22 @@ pub async fn set_displayname_route(
|
|||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
let _ = db
|
||||
.rooms
|
||||
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &db, &state_lock);
|
||||
let _ = services().rooms.timeline.build_and_append_pdu(
|
||||
pdu_builder,
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
);
|
||||
|
||||
// Presence update
|
||||
db.rooms.edus.update_presence(
|
||||
services().rooms.edus.presence.update_presence(
|
||||
sender_user,
|
||||
&room_id,
|
||||
ruma::events::presence::PresenceEvent {
|
||||
content: ruma::events::presence::PresenceEventContent {
|
||||
avatar_url: db.users.avatar_url(sender_user)?,
|
||||
avatar_url: services().users.avatar_url(sender_user)?,
|
||||
currently_active: None,
|
||||
displayname: db.users.displayname(sender_user)?,
|
||||
displayname: services().users.displayname(sender_user)?,
|
||||
last_active_ago: Some(
|
||||
utils::millis_since_unix_epoch()
|
||||
.try_into()
|
||||
|
@ -110,13 +109,10 @@ pub async fn set_displayname_route(
|
|||
},
|
||||
sender: sender_user.clone(),
|
||||
},
|
||||
&db.globals,
|
||||
)?;
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_display_name::Response {}.into())
|
||||
Ok(set_display_name::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/profile/{userId}/displayname`
|
||||
|
@ -124,38 +120,29 @@ pub async fn set_displayname_route(
|
|||
/// Returns the displayname of the user.
|
||||
///
|
||||
/// - If user is on another server: Fetches displayname over federation
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/profile/<_>/displayname", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_displayname_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_display_name::Request<'_>>,
|
||||
) -> ConduitResult<get_display_name::Response> {
|
||||
if body.user_id.server_name() != db.globals.server_name() {
|
||||
let response = db
|
||||
body: Ruma<get_display_name::v3::Request>,
|
||||
) -> Result<get_display_name::v3::Response> {
|
||||
if body.user_id.server_name() != services().globals.server_name() {
|
||||
let response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
body.user_id.server_name(),
|
||||
federation::query::get_profile_information::v1::Request {
|
||||
user_id: &body.user_id,
|
||||
field: Some(&ProfileField::DisplayName),
|
||||
user_id: body.user_id.clone(),
|
||||
field: Some(ProfileField::DisplayName),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(get_display_name::Response {
|
||||
return Ok(get_display_name::v3::Response {
|
||||
displayname: response.displayname,
|
||||
}
|
||||
.into());
|
||||
});
|
||||
}
|
||||
|
||||
Ok(get_display_name::Response {
|
||||
displayname: db.users.displayname(&body.user_id)?,
|
||||
}
|
||||
.into())
|
||||
Ok(get_display_name::v3::Response {
|
||||
displayname: services().users.displayname(&body.user_id)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/profile/{userId}/avatar_url`
|
||||
|
@ -163,38 +150,38 @@ pub async fn get_displayname_route(
|
|||
/// Updates the avatar_url and blurhash.
|
||||
///
|
||||
/// - Also makes sure other users receive the update using presence EDUs
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/profile/<_>/avatar_url", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_avatar_url_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_avatar_url::Request<'_>>,
|
||||
) -> ConduitResult<set_avatar_url::Response> {
|
||||
body: Ruma<set_avatar_url::v3::Request>,
|
||||
) -> Result<set_avatar_url::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
db.users
|
||||
services()
|
||||
.users
|
||||
.set_avatar_url(sender_user, body.avatar_url.clone())?;
|
||||
|
||||
db.users.set_blurhash(sender_user, body.blurhash.clone())?;
|
||||
services()
|
||||
.users
|
||||
.set_blurhash(sender_user, body.blurhash.clone())?;
|
||||
|
||||
// Send a new membership event and presence update into all joined rooms
|
||||
let all_joined_rooms: Vec<_> = db
|
||||
let all_joined_rooms: Vec<_> = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(|r| r.ok())
|
||||
.map(|room_id| {
|
||||
Ok::<_, Error>((
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomMember,
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
avatar_url: body.avatar_url.clone(),
|
||||
..serde_json::from_str(
|
||||
db.rooms
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(
|
||||
&room_id,
|
||||
&EventType::RoomMember,
|
||||
&StateEventType::RoomMember,
|
||||
sender_user.as_str(),
|
||||
)?
|
||||
.ok_or_else(|| {
|
||||
|
@ -221,7 +208,8 @@ pub async fn set_avatar_url_route(
|
|||
|
||||
for (pdu_builder, room_id) in all_joined_rooms {
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -230,19 +218,22 @@ pub async fn set_avatar_url_route(
|
|||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
let _ = db
|
||||
.rooms
|
||||
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &db, &state_lock);
|
||||
let _ = services().rooms.timeline.build_and_append_pdu(
|
||||
pdu_builder,
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
);
|
||||
|
||||
// Presence update
|
||||
db.rooms.edus.update_presence(
|
||||
services().rooms.edus.presence.update_presence(
|
||||
sender_user,
|
||||
&room_id,
|
||||
ruma::events::presence::PresenceEvent {
|
||||
content: ruma::events::presence::PresenceEventContent {
|
||||
avatar_url: db.users.avatar_url(sender_user)?,
|
||||
avatar_url: services().users.avatar_url(sender_user)?,
|
||||
currently_active: None,
|
||||
displayname: db.users.displayname(sender_user)?,
|
||||
displayname: services().users.displayname(sender_user)?,
|
||||
last_active_ago: Some(
|
||||
utils::millis_since_unix_epoch()
|
||||
.try_into()
|
||||
|
@ -253,13 +244,10 @@ pub async fn set_avatar_url_route(
|
|||
},
|
||||
sender: sender_user.clone(),
|
||||
},
|
||||
&db.globals,
|
||||
)?;
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_avatar_url::Response {}.into())
|
||||
Ok(set_avatar_url::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/profile/{userId}/avatar_url`
|
||||
|
@ -267,40 +255,31 @@ pub async fn set_avatar_url_route(
|
|||
/// Returns the avatar_url and blurhash of the user.
|
||||
///
|
||||
/// - If user is on another server: Fetches avatar_url and blurhash over federation
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/profile/<_>/avatar_url", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_avatar_url_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_avatar_url::Request<'_>>,
|
||||
) -> ConduitResult<get_avatar_url::Response> {
|
||||
if body.user_id.server_name() != db.globals.server_name() {
|
||||
let response = db
|
||||
body: Ruma<get_avatar_url::v3::Request>,
|
||||
) -> Result<get_avatar_url::v3::Response> {
|
||||
if body.user_id.server_name() != services().globals.server_name() {
|
||||
let response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
body.user_id.server_name(),
|
||||
federation::query::get_profile_information::v1::Request {
|
||||
user_id: &body.user_id,
|
||||
field: Some(&ProfileField::AvatarUrl),
|
||||
user_id: body.user_id.clone(),
|
||||
field: Some(ProfileField::AvatarUrl),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(get_avatar_url::Response {
|
||||
return Ok(get_avatar_url::v3::Response {
|
||||
avatar_url: response.avatar_url,
|
||||
blurhash: response.blurhash,
|
||||
}
|
||||
.into());
|
||||
});
|
||||
}
|
||||
|
||||
Ok(get_avatar_url::Response {
|
||||
avatar_url: db.users.avatar_url(&body.user_id)?,
|
||||
blurhash: db.users.blurhash(&body.user_id)?,
|
||||
}
|
||||
.into())
|
||||
Ok(get_avatar_url::v3::Response {
|
||||
avatar_url: services().users.avatar_url(&body.user_id)?,
|
||||
blurhash: services().users.blurhash(&body.user_id)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/profile/{userId}`
|
||||
|
@ -308,37 +287,29 @@ pub async fn get_avatar_url_route(
|
|||
/// Returns the displayname, avatar_url and blurhash of the user.
|
||||
///
|
||||
/// - If user is on another server: Fetches profile over federation
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/profile/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_profile_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_profile::Request<'_>>,
|
||||
) -> ConduitResult<get_profile::Response> {
|
||||
if body.user_id.server_name() != db.globals.server_name() {
|
||||
let response = db
|
||||
body: Ruma<get_profile::v3::Request>,
|
||||
) -> Result<get_profile::v3::Response> {
|
||||
if body.user_id.server_name() != services().globals.server_name() {
|
||||
let response = services()
|
||||
.sending
|
||||
.send_federation_request(
|
||||
&db.globals,
|
||||
body.user_id.server_name(),
|
||||
federation::query::get_profile_information::v1::Request {
|
||||
user_id: &body.user_id,
|
||||
user_id: body.user_id.clone(),
|
||||
field: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(get_profile::Response {
|
||||
return Ok(get_profile::v3::Response {
|
||||
displayname: response.displayname,
|
||||
avatar_url: response.avatar_url,
|
||||
blurhash: response.blurhash,
|
||||
}
|
||||
.into());
|
||||
});
|
||||
}
|
||||
|
||||
if !db.users.exists(&body.user_id)? {
|
||||
if !services().users.exists(&body.user_id)? {
|
||||
// Return 404 if this user doesn't exist
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
|
@ -346,10 +317,9 @@ pub async fn get_profile_route(
|
|||
));
|
||||
}
|
||||
|
||||
Ok(get_profile::Response {
|
||||
avatar_url: db.users.avatar_url(&body.user_id)?,
|
||||
blurhash: db.users.blurhash(&body.user_id)?,
|
||||
displayname: db.users.displayname(&body.user_id)?,
|
||||
}
|
||||
.into())
|
||||
Ok(get_profile::v3::Response {
|
||||
avatar_url: services().users.avatar_url(&body.user_id)?,
|
||||
blurhash: services().users.blurhash(&body.user_id)?,
|
||||
displayname: services().users.displayname(&body.user_id)?,
|
||||
})
|
||||
}
|
432
src/api/client_server/push.rs
Normal file
432
src/api/client_server/push.rs
Normal file
|
@ -0,0 +1,432 @@
|
|||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
push::{
|
||||
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions, get_pushrule_enabled,
|
||||
get_pushrules_all, set_pusher, set_pushrule, set_pushrule_actions,
|
||||
set_pushrule_enabled, RuleScope,
|
||||
},
|
||||
},
|
||||
events::{push_rules::PushRulesEvent, GlobalAccountDataEventType},
|
||||
push::{InsertPushRuleError, RemovePushRuleError},
|
||||
};
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules`
|
||||
///
|
||||
/// Retrieves the push rules event for this user.
|
||||
pub async fn get_pushrules_all_route(
|
||||
body: Ruma<get_pushrules_all::v3::Request>,
|
||||
) -> Result<get_pushrules_all::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
Ok(get_pushrules_all::v3::Response {
|
||||
global: account_data.global,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Retrieves a single specified push rule for this user.
|
||||
pub async fn get_pushrule_route(
|
||||
body: Ruma<get_pushrule::v3::Request>,
|
||||
) -> Result<get_pushrule::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
let rule = account_data
|
||||
.global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(Into::into);
|
||||
|
||||
if let Some(rule) = rule {
|
||||
Ok(get_pushrule::v3::Response { rule })
|
||||
} else {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Push rule not found.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Creates a single specified push rule for this user.
|
||||
pub async fn set_pushrule_route(
|
||||
body: Ruma<set_pushrule::v3::Request>,
|
||||
) -> Result<set_pushrule::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let body = body.body;
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
if let Err(error) = account_data.content.global.insert(
|
||||
body.rule.clone(),
|
||||
body.after.as_deref(),
|
||||
body.before.as_deref(),
|
||||
) {
|
||||
let err = match error {
|
||||
InsertPushRuleError::ServerDefaultRuleId => Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Rule IDs starting with a dot are reserved for server-default rules.",
|
||||
),
|
||||
InsertPushRuleError::InvalidRuleId => Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Rule ID containing invalid characters.",
|
||||
),
|
||||
InsertPushRuleError::RelativeToServerDefaultRule => Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Can't place a push rule relatively to a server-default rule.",
|
||||
),
|
||||
InsertPushRuleError::UnknownRuleId => Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"The before or after rule could not be found.",
|
||||
),
|
||||
InsertPushRuleError::BeforeHigherThanAfter => Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"The before rule has a higher priority than the after rule.",
|
||||
),
|
||||
_ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(set_pushrule::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
|
||||
///
|
||||
/// Gets the actions of a single specified push rule for this user.
|
||||
pub async fn get_pushrule_actions_route(
|
||||
body: Ruma<get_pushrule_actions::v3::Request>,
|
||||
) -> Result<get_pushrule_actions::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
|
||||
.content;
|
||||
|
||||
let global = account_data.global;
|
||||
let actions = global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(|rule| rule.actions().to_owned())
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Push rule not found.",
|
||||
))?;
|
||||
|
||||
Ok(get_pushrule_actions::v3::Response { actions })
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
|
||||
///
|
||||
/// Sets the actions of a single specified push rule for this user.
|
||||
pub async fn set_pushrule_actions_route(
|
||||
body: Ruma<set_pushrule_actions::v3::Request>,
|
||||
) -> Result<set_pushrule_actions::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
if account_data
|
||||
.content
|
||||
.global
|
||||
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone())
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Push rule not found.",
|
||||
));
|
||||
}
|
||||
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(set_pushrule_actions::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
|
||||
///
|
||||
/// Gets the enabled status of a single specified push rule for this user.
|
||||
pub async fn get_pushrule_enabled_route(
|
||||
body: Ruma<get_pushrule_enabled::v3::Request>,
|
||||
) -> Result<get_pushrule_enabled::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
let global = account_data.content.global;
|
||||
let enabled = global
|
||||
.get(body.kind.clone(), &body.rule_id)
|
||||
.map(|r| r.enabled())
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Push rule not found.",
|
||||
))?;
|
||||
|
||||
Ok(get_pushrule_enabled::v3::Response { enabled })
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
|
||||
///
|
||||
/// Sets the enabled status of a single specified push rule for this user.
|
||||
pub async fn set_pushrule_enabled_route(
|
||||
body: Ruma<set_pushrule_enabled::v3::Request>,
|
||||
) -> Result<set_pushrule_enabled::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
if account_data
|
||||
.content
|
||||
.global
|
||||
.set_enabled(body.kind.clone(), &body.rule_id, body.enabled)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Push rule not found.",
|
||||
));
|
||||
}
|
||||
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(set_pushrule_enabled::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Deletes a single specified push rule for this user.
|
||||
pub async fn delete_pushrule_route(
|
||||
body: Ruma<delete_pushrule::v3::Request>,
|
||||
) -> Result<delete_pushrule::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != RuleScope::Global {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.account_data
|
||||
.get(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
|
||||
|
||||
if let Err(error) = account_data
|
||||
.content
|
||||
.global
|
||||
.remove(body.kind.clone(), &body.rule_id)
|
||||
{
|
||||
let err = match error {
|
||||
RemovePushRuleError::ServerDefault => Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Cannot delete a server-default pushrule.",
|
||||
),
|
||||
RemovePushRuleError::NotFound => {
|
||||
Error::BadRequest(ErrorKind::NotFound, "Push rule not found.")
|
||||
}
|
||||
_ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."),
|
||||
};
|
||||
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
services().account_data.update(
|
||||
None,
|
||||
sender_user,
|
||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||
&serde_json::to_value(account_data).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(delete_pushrule::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushers`
|
||||
///
|
||||
/// Gets all currently active pushers for the sender user.
|
||||
pub async fn get_pushers_route(
|
||||
body: Ruma<get_pushers::v3::Request>,
|
||||
) -> Result<get_pushers::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_pushers::v3::Response {
|
||||
pushers: services().pusher.get_pushers(sender_user)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/pushers/set`
|
||||
///
|
||||
/// Adds a pusher for the sender user.
|
||||
///
|
||||
/// - TODO: Handle `append`
|
||||
pub async fn set_pushers_route(
|
||||
body: Ruma<set_pusher::v3::Request>,
|
||||
) -> Result<set_pusher::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
services()
|
||||
.pusher
|
||||
.set_pusher(sender_user, body.action.clone())?;
|
||||
|
||||
Ok(set_pusher::v3::Response::default())
|
||||
}
|
182
src/api/client_server/read_marker.rs
Normal file
182
src/api/client_server/read_marker.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use crate::{service::rooms::timeline::PduCount, services, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt},
|
||||
events::{
|
||||
receipt::{ReceiptThread, ReceiptType},
|
||||
RoomAccountDataEventType,
|
||||
},
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
|
||||
///
|
||||
/// Sets different types of read markers.
|
||||
///
|
||||
/// - Updates fully-read account data event to `fully_read`
|
||||
/// - If `read_receipt` is set: Update private marker and public read receipt EDU
|
||||
pub async fn set_read_marker_route(
|
||||
body: Ruma<set_read_marker::v3::Request>,
|
||||
) -> Result<set_read_marker::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if let Some(fully_read) = &body.fully_read {
|
||||
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
|
||||
content: ruma::events::fully_read::FullyReadEventContent {
|
||||
event_id: fully_read.clone(),
|
||||
},
|
||||
};
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event).expect("to json value always works"),
|
||||
)?;
|
||||
}
|
||||
|
||||
if body.private_read_receipt.is_some() || body.read_receipt.is_some() {
|
||||
services()
|
||||
.rooms
|
||||
.user
|
||||
.reset_notification_counts(sender_user, &body.room_id)?;
|
||||
}
|
||||
|
||||
if let Some(event) = &body.private_read_receipt {
|
||||
let count = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(event)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Event does not exist.",
|
||||
))?;
|
||||
let count = match count {
|
||||
PduCount::Backfilled(_) => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Read receipt is in backfilled timeline",
|
||||
))
|
||||
}
|
||||
PduCount::Normal(c) => c,
|
||||
};
|
||||
services()
|
||||
.rooms
|
||||
.edus
|
||||
.read_receipt
|
||||
.private_read_set(&body.room_id, sender_user, count)?;
|
||||
}
|
||||
|
||||
if let Some(event) = &body.read_receipt {
|
||||
let mut user_receipts = BTreeMap::new();
|
||||
user_receipts.insert(
|
||||
sender_user.clone(),
|
||||
ruma::events::receipt::Receipt {
|
||||
ts: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
thread: ReceiptThread::Unthreaded,
|
||||
},
|
||||
);
|
||||
|
||||
let mut receipts = BTreeMap::new();
|
||||
receipts.insert(ReceiptType::Read, user_receipts);
|
||||
|
||||
let mut receipt_content = BTreeMap::new();
|
||||
receipt_content.insert(event.to_owned(), receipts);
|
||||
|
||||
services().rooms.edus.read_receipt.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(set_read_marker::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId}`
|
||||
///
|
||||
/// Sets private read marker and public read receipt EDU.
|
||||
pub async fn create_receipt_route(
|
||||
body: Ruma<create_receipt::v3::Request>,
|
||||
) -> Result<create_receipt::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if matches!(
|
||||
&body.receipt_type,
|
||||
create_receipt::v3::ReceiptType::Read | create_receipt::v3::ReceiptType::ReadPrivate
|
||||
) {
|
||||
services()
|
||||
.rooms
|
||||
.user
|
||||
.reset_notification_counts(sender_user, &body.room_id)?;
|
||||
}
|
||||
|
||||
match body.receipt_type {
|
||||
create_receipt::v3::ReceiptType::FullyRead => {
|
||||
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
|
||||
content: ruma::events::fully_read::FullyReadEventContent {
|
||||
event_id: body.event_id.clone(),
|
||||
},
|
||||
};
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::FullyRead,
|
||||
&serde_json::to_value(fully_read_event).expect("to json value always works"),
|
||||
)?;
|
||||
}
|
||||
create_receipt::v3::ReceiptType::Read => {
|
||||
let mut user_receipts = BTreeMap::new();
|
||||
user_receipts.insert(
|
||||
sender_user.clone(),
|
||||
ruma::events::receipt::Receipt {
|
||||
ts: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
thread: ReceiptThread::Unthreaded,
|
||||
},
|
||||
);
|
||||
let mut receipts = BTreeMap::new();
|
||||
receipts.insert(ReceiptType::Read, user_receipts);
|
||||
|
||||
let mut receipt_content = BTreeMap::new();
|
||||
receipt_content.insert(body.event_id.to_owned(), receipts);
|
||||
|
||||
services().rooms.edus.read_receipt.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
create_receipt::v3::ReceiptType::ReadPrivate => {
|
||||
let count = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_count(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Event does not exist.",
|
||||
))?;
|
||||
let count = match count {
|
||||
PduCount::Backfilled(_) => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Read receipt is in backfilled timeline",
|
||||
))
|
||||
}
|
||||
PduCount::Normal(c) => c,
|
||||
};
|
||||
services().rooms.edus.read_receipt.private_read_set(
|
||||
&body.room_id,
|
||||
sender_user,
|
||||
count,
|
||||
)?;
|
||||
}
|
||||
_ => return Err(Error::bad_database("Unsupported receipt type")),
|
||||
}
|
||||
|
||||
Ok(create_receipt::v3::Response {})
|
||||
}
|
|
@ -1,13 +1,11 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{database::DatabaseGuard, pdu::PduBuilder, ConduitResult, Ruma};
|
||||
use crate::{service::pdu::PduBuilder, services, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::r0::redact::redact_event,
|
||||
events::{room::redaction::RoomRedactionEventContent, EventType},
|
||||
api::client::redact::redact_event,
|
||||
events::{room::redaction::RoomRedactionEventContent, TimelineEventType},
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::put;
|
||||
use serde_json::value::to_raw_value;
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId}`
|
||||
|
@ -15,20 +13,15 @@ use serde_json::value::to_raw_value;
|
|||
/// Tries to send a redaction event into the room.
|
||||
///
|
||||
/// - TODO: Handle txn id
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/rooms/<_>/redact/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn redact_event_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<redact_event::Request<'_>>,
|
||||
) -> ConduitResult<redact_event::Response> {
|
||||
body: Ruma<redact_event::v3::Request>,
|
||||
) -> Result<redact_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let body = body.body;
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -37,10 +30,11 @@ pub async fn redact_event_route(
|
|||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
let event_id = db.rooms.build_and_append_pdu(
|
||||
let event_id = services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomRedaction,
|
||||
event_type: TimelineEventType::RoomRedaction,
|
||||
content: to_raw_value(&RoomRedactionEventContent {
|
||||
redacts: Some(body.event_id.clone()),
|
||||
reason: body.reason.clone(),
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
|
@ -50,14 +44,11 @@ pub async fn redact_event_route(
|
|||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
db.flush()?;
|
||||
|
||||
let event_id = (*event_id).to_owned();
|
||||
Ok(redact_event::Response { event_id }.into())
|
||||
Ok(redact_event::v3::Response { event_id })
|
||||
}
|
146
src/api/client_server/relations.rs
Normal file
146
src/api/client_server/relations.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
use ruma::api::client::relations::{
|
||||
get_relating_events, get_relating_events_with_rel_type,
|
||||
get_relating_events_with_rel_type_and_event_type,
|
||||
};
|
||||
|
||||
use crate::{service::rooms::timeline::PduCount, services, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
|
||||
pub async fn get_relating_events_with_rel_type_and_event_type_route(
|
||||
body: Ruma<get_relating_events_with_rel_type_and_event_type::v1::Request>,
|
||||
) -> Result<get_relating_events_with_rel_type_and_event_type::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let from = match body.from.clone() {
|
||||
Some(from) => PduCount::try_from_string(&from)?,
|
||||
None => match ruma::api::Direction::Backward {
|
||||
// TODO: fix ruma so `body.dir` exists
|
||||
ruma::api::Direction::Forward => PduCount::min(),
|
||||
ruma::api::Direction::Backward => PduCount::max(),
|
||||
},
|
||||
};
|
||||
|
||||
let to = body
|
||||
.to
|
||||
.as_ref()
|
||||
.and_then(|t| PduCount::try_from_string(t).ok());
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = body
|
||||
.limit
|
||||
.and_then(|u| u32::try_from(u).ok())
|
||||
.map_or(10_usize, |u| u as usize)
|
||||
.min(100);
|
||||
|
||||
let res = services()
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
Some(body.event_type.clone()),
|
||||
Some(body.rel_type.clone()),
|
||||
from,
|
||||
to,
|
||||
limit,
|
||||
)?;
|
||||
|
||||
Ok(
|
||||
get_relating_events_with_rel_type_and_event_type::v1::Response {
|
||||
chunk: res.chunk,
|
||||
next_batch: res.next_batch,
|
||||
prev_batch: res.prev_batch,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}`
|
||||
pub async fn get_relating_events_with_rel_type_route(
|
||||
body: Ruma<get_relating_events_with_rel_type::v1::Request>,
|
||||
) -> Result<get_relating_events_with_rel_type::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let from = match body.from.clone() {
|
||||
Some(from) => PduCount::try_from_string(&from)?,
|
||||
None => match ruma::api::Direction::Backward {
|
||||
// TODO: fix ruma so `body.dir` exists
|
||||
ruma::api::Direction::Forward => PduCount::min(),
|
||||
ruma::api::Direction::Backward => PduCount::max(),
|
||||
},
|
||||
};
|
||||
|
||||
let to = body
|
||||
.to
|
||||
.as_ref()
|
||||
.and_then(|t| PduCount::try_from_string(t).ok());
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = body
|
||||
.limit
|
||||
.and_then(|u| u32::try_from(u).ok())
|
||||
.map_or(10_usize, |u| u as usize)
|
||||
.min(100);
|
||||
|
||||
let res = services()
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
None,
|
||||
Some(body.rel_type.clone()),
|
||||
from,
|
||||
to,
|
||||
limit,
|
||||
)?;
|
||||
|
||||
Ok(get_relating_events_with_rel_type::v1::Response {
|
||||
chunk: res.chunk,
|
||||
next_batch: res.next_batch,
|
||||
prev_batch: res.prev_batch,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}`
|
||||
pub async fn get_relating_events_route(
|
||||
body: Ruma<get_relating_events::v1::Request>,
|
||||
) -> Result<get_relating_events::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let from = match body.from.clone() {
|
||||
Some(from) => PduCount::try_from_string(&from)?,
|
||||
None => match ruma::api::Direction::Backward {
|
||||
// TODO: fix ruma so `body.dir` exists
|
||||
ruma::api::Direction::Forward => PduCount::min(),
|
||||
ruma::api::Direction::Backward => PduCount::max(),
|
||||
},
|
||||
};
|
||||
|
||||
let to = body
|
||||
.to
|
||||
.as_ref()
|
||||
.and_then(|t| PduCount::try_from_string(t).ok());
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = body
|
||||
.limit
|
||||
.and_then(|u| u32::try_from(u).ok())
|
||||
.map_or(10_usize, |u| u as usize)
|
||||
.min(100);
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.pdu_metadata
|
||||
.paginate_relations_with_filter(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_id,
|
||||
None,
|
||||
None,
|
||||
from,
|
||||
to,
|
||||
limit,
|
||||
)
|
||||
}
|
|
@ -1,29 +1,20 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use crate::{services, utils::HtmlEscape, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::{error::ErrorKind, r0::room::report_content},
|
||||
api::client::{error::ErrorKind, room::report_content},
|
||||
events::room::message,
|
||||
int,
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{http::RawStr, post};
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/report/{eventId}`
|
||||
///
|
||||
/// Reports an inappropriate event to homeserver admins
|
||||
///
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/rooms/<_>/report/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn report_event_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<report_content::Request<'_>>,
|
||||
) -> ConduitResult<report_content::Response> {
|
||||
body: Ruma<report_content::v3::Request>,
|
||||
) -> Result<report_content::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let pdu = match db.rooms.get_pdu(&body.event_id)? {
|
||||
let pdu = match services().rooms.timeline.get_pdu(&body.event_id)? {
|
||||
Some(pdu) => pdu,
|
||||
_ => {
|
||||
return Err(Error::BadRequest(
|
||||
|
@ -33,48 +24,46 @@ pub async fn report_event_route(
|
|||
}
|
||||
};
|
||||
|
||||
if body.score > int!(0) || body.score < int!(-100) {
|
||||
if let Some(true) = body.score.map(|s| s > int!(0) || s < int!(-100)) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Invalid score, must be within 0 to -100",
|
||||
));
|
||||
};
|
||||
|
||||
if body.reason.chars().count() > 250 {
|
||||
if let Some(true) = body.reason.clone().map(|s| s.chars().count() > 250) {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Reason too long, should be 250 characters or fewer",
|
||||
));
|
||||
};
|
||||
|
||||
db.admin
|
||||
services().admin
|
||||
.send_message(message::RoomMessageEventContent::text_html(
|
||||
format!(
|
||||
"Report received from: {}\n\n\
|
||||
Event ID: {}\n\
|
||||
Room ID: {}\n\
|
||||
Sent By: {}\n\n\
|
||||
Report Score: {}\n\
|
||||
Report Reason: {}",
|
||||
Event ID: {:?}\n\
|
||||
Room ID: {:?}\n\
|
||||
Sent By: {:?}\n\n\
|
||||
Report Score: {:?}\n\
|
||||
Report Reason: {:?}",
|
||||
sender_user, pdu.event_id, pdu.room_id, pdu.sender, body.score, body.reason
|
||||
),
|
||||
format!(
|
||||
"<details><summary>Report received from: <a href=\"https://matrix.to/#/{0}\">{0}\
|
||||
</a></summary><ul><li>Event Info<ul><li>Event ID: <code>{1}</code>\
|
||||
<a href=\"https://matrix.to/#/{2}/{1}\">🔗</a></li><li>Room ID: <code>{2}</code>\
|
||||
</li><li>Sent By: <a href=\"https://matrix.to/#/{3}\">{3}</a></li></ul></li><li>\
|
||||
Report Info<ul><li>Report Score: {4}</li><li>Report Reason: {5}</li></ul></li>\
|
||||
"<details><summary>Report received from: <a href=\"https://matrix.to/#/{0:?}\">{0:?}\
|
||||
</a></summary><ul><li>Event Info<ul><li>Event ID: <code>{1:?}</code>\
|
||||
<a href=\"https://matrix.to/#/{2:?}/{1:?}\">🔗</a></li><li>Room ID: <code>{2:?}</code>\
|
||||
</li><li>Sent By: <a href=\"https://matrix.to/#/{3:?}\">{3:?}</a></li></ul></li><li>\
|
||||
Report Info<ul><li>Report Score: {4:?}</li><li>Report Reason: {5}</li></ul></li>\
|
||||
</ul></details>",
|
||||
sender_user,
|
||||
pdu.event_id,
|
||||
pdu.room_id,
|
||||
pdu.sender,
|
||||
body.score,
|
||||
RawStr::new(&body.reason).html_escape()
|
||||
HtmlEscape(body.reason.as_deref().unwrap_or(""))
|
||||
),
|
||||
));
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(report_content::Response {}.into())
|
||||
Ok(report_content::v3::Response {})
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
use crate::{
|
||||
client_server::invite_helper, database::DatabaseGuard, pdu::PduBuilder, ConduitResult, Error,
|
||||
Ruma,
|
||||
api::client_server::invite_helper, service::pdu::PduBuilder, services, Error, Result, Ruma,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::room::{self, aliases, create_room, get_room_event, upgrade_room},
|
||||
room::{self, aliases, create_room, get_room_event, upgrade_room},
|
||||
},
|
||||
events::{
|
||||
room::{
|
||||
|
@ -20,19 +19,16 @@ use ruma::{
|
|||
tombstone::RoomTombstoneEventContent,
|
||||
topic::RoomTopicEventContent,
|
||||
},
|
||||
EventType,
|
||||
StateEventType, TimelineEventType,
|
||||
},
|
||||
int,
|
||||
serde::{CanonicalJsonObject, JsonObject},
|
||||
RoomAliasId, RoomId, RoomVersionId,
|
||||
serde::JsonObject,
|
||||
CanonicalJsonObject, OwnedRoomAliasId, RoomAliasId, RoomId,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
use std::{cmp::max, collections::BTreeMap, sync::Arc};
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, post};
|
||||
|
||||
/// # `POST /_matrix/client/r0/createRoom`
|
||||
///
|
||||
/// Creates a new room.
|
||||
|
@ -49,23 +45,20 @@ use rocket::{get, post};
|
|||
/// - Send events listed in initial state
|
||||
/// - Send events implied by `name` and `topic`
|
||||
/// - Send invite events
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/createRoom", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn create_room_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<create_room::Request<'_>>,
|
||||
) -> ConduitResult<create_room::Response> {
|
||||
body: Ruma<create_room::v3::Request>,
|
||||
) -> Result<create_room::v3::Response> {
|
||||
use create_room::v3::RoomPreset;
|
||||
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let room_id = RoomId::new(db.globals.server_name());
|
||||
let room_id = RoomId::new(services().globals.server_name());
|
||||
|
||||
db.rooms.get_or_create_shortroomid(&room_id, &db.globals)?;
|
||||
services().rooms.short.get_or_create_shortroomid(&room_id)?;
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -74,9 +67,9 @@ pub async fn create_room_route(
|
|||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
if !db.globals.allow_room_creation()
|
||||
if !services().globals.allow_room_creation()
|
||||
&& !body.from_appservice
|
||||
&& !db.users.is_admin(sender_user, &db.rooms, &db.globals)?
|
||||
&& !services().users.is_admin(sender_user)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
|
@ -84,18 +77,24 @@ pub async fn create_room_route(
|
|||
));
|
||||
}
|
||||
|
||||
let alias: Option<Box<RoomAliasId>> =
|
||||
let alias: Option<OwnedRoomAliasId> =
|
||||
body.room_alias_name
|
||||
.as_ref()
|
||||
.map_or(Ok(None), |localpart| {
|
||||
// TODO: Check for invalid characters and maximum length
|
||||
let alias =
|
||||
RoomAliasId::parse(format!("#{}:{}", localpart, db.globals.server_name()))
|
||||
.map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Invalid alias.")
|
||||
})?;
|
||||
let alias = RoomAliasId::parse(format!(
|
||||
"#{}:{}",
|
||||
localpart,
|
||||
services().globals.server_name()
|
||||
))
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid alias."))?;
|
||||
|
||||
if db.rooms.id_from_alias(&alias)?.is_some() {
|
||||
if services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&alias)?
|
||||
.is_some()
|
||||
{
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::RoomInUse,
|
||||
"Room alias already exists.",
|
||||
|
@ -107,7 +106,11 @@ pub async fn create_room_route(
|
|||
|
||||
let room_version = match body.room_version.clone() {
|
||||
Some(room_version) => {
|
||||
if room_version == RoomVersionId::V5 || room_version == RoomVersionId::V6 {
|
||||
if services()
|
||||
.globals
|
||||
.supported_room_versions()
|
||||
.contains(&room_version)
|
||||
{
|
||||
room_version
|
||||
} else {
|
||||
return Err(Error::BadRequest(
|
||||
|
@ -116,7 +119,7 @@ pub async fn create_room_route(
|
|||
));
|
||||
}
|
||||
}
|
||||
None => RoomVersionId::V6,
|
||||
None => services().globals.default_room_version(),
|
||||
};
|
||||
|
||||
let content = match &body.creation_content {
|
||||
|
@ -139,8 +142,9 @@ pub async fn create_room_route(
|
|||
content
|
||||
}
|
||||
None => {
|
||||
// TODO: Add correct value for v11
|
||||
let mut content = serde_json::from_str::<CanonicalJsonObject>(
|
||||
to_raw_value(&RoomCreateEventContent::new(sender_user.clone()))
|
||||
to_raw_value(&RoomCreateEventContent::new_v1(sender_user.clone()))
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::BadJson, "Invalid creation content"))?
|
||||
.get(),
|
||||
)
|
||||
|
@ -170,9 +174,9 @@ pub async fn create_room_route(
|
|||
}
|
||||
|
||||
// 1. The room create event
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomCreate,
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&content).expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
|
@ -180,21 +184,20 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
// 2. Let the room creator join
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomMember,
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
membership: MembershipState::Join,
|
||||
displayname: db.users.displayname(sender_user)?,
|
||||
avatar_url: db.users.avatar_url(sender_user)?,
|
||||
displayname: services().users.displayname(sender_user)?,
|
||||
avatar_url: services().users.avatar_url(sender_user)?,
|
||||
is_direct: Some(body.is_direct),
|
||||
third_party_invite: None,
|
||||
blurhash: db.users.blurhash(sender_user)?,
|
||||
blurhash: services().users.blurhash(sender_user)?,
|
||||
reason: None,
|
||||
join_authorized_via_users_server: None,
|
||||
})
|
||||
|
@ -205,26 +208,22 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
// 3. Power levels
|
||||
|
||||
// Figure out preset. We need it for preset specific events
|
||||
let preset = body
|
||||
.preset
|
||||
.clone()
|
||||
.unwrap_or_else(|| match &body.visibility {
|
||||
room::Visibility::Private => create_room::RoomPreset::PrivateChat,
|
||||
room::Visibility::Public => create_room::RoomPreset::PublicChat,
|
||||
_ => create_room::RoomPreset::PrivateChat, // Room visibility should not be custom
|
||||
});
|
||||
let preset = body.preset.clone().unwrap_or(match &body.visibility {
|
||||
room::Visibility::Private => RoomPreset::PrivateChat,
|
||||
room::Visibility::Public => RoomPreset::PublicChat,
|
||||
_ => RoomPreset::PrivateChat, // Room visibility should not be custom
|
||||
});
|
||||
|
||||
let mut users = BTreeMap::new();
|
||||
users.insert(sender_user.clone(), int!(100));
|
||||
|
||||
if preset == create_room::RoomPreset::TrustedPrivateChat {
|
||||
if preset == RoomPreset::TrustedPrivateChat {
|
||||
for invite_ in &body.invite {
|
||||
users.insert(invite_.clone(), int!(100));
|
||||
}
|
||||
|
@ -247,9 +246,9 @@ pub async fn create_room_route(
|
|||
}
|
||||
}
|
||||
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomPowerLevels,
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_content)
|
||||
.expect("to_raw_value always works on serde_json::Value"),
|
||||
unsigned: None,
|
||||
|
@ -258,15 +257,14 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
// 4. Canonical room alias
|
||||
if let Some(room_alias_id) = &alias {
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomCanonicalAlias,
|
||||
event_type: TimelineEventType::RoomCanonicalAlias,
|
||||
content: to_raw_value(&RoomCanonicalAliasEventContent {
|
||||
alias: Some(room_alias_id.to_owned()),
|
||||
alt_aliases: vec![],
|
||||
|
@ -278,7 +276,6 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
}
|
||||
|
@ -286,11 +283,11 @@ pub async fn create_room_route(
|
|||
// 5. Events set by preset
|
||||
|
||||
// 5.1 Join Rules
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomJoinRules,
|
||||
event_type: TimelineEventType::RoomJoinRules,
|
||||
content: to_raw_value(&RoomJoinRulesEventContent::new(match preset {
|
||||
create_room::RoomPreset::PublicChat => JoinRule::Public,
|
||||
RoomPreset::PublicChat => JoinRule::Public,
|
||||
// according to spec "invite" is the default
|
||||
_ => JoinRule::Invite,
|
||||
}))
|
||||
|
@ -301,14 +298,13 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
// 5.2 History Visibility
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomHistoryVisibility,
|
||||
event_type: TimelineEventType::RoomHistoryVisibility,
|
||||
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(
|
||||
HistoryVisibility::Shared,
|
||||
))
|
||||
|
@ -319,16 +315,15 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
// 5.3 Guest Access
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomGuestAccess,
|
||||
event_type: TimelineEventType::RoomGuestAccess,
|
||||
content: to_raw_value(&RoomGuestAccessEventContent::new(match preset {
|
||||
create_room::RoomPreset::PublicChat => GuestAccess::Forbidden,
|
||||
RoomPreset::PublicChat => GuestAccess::Forbidden,
|
||||
_ => GuestAccess::CanJoin,
|
||||
}))
|
||||
.expect("event is valid, we just created it"),
|
||||
|
@ -338,7 +333,6 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
|
@ -353,20 +347,26 @@ pub async fn create_room_route(
|
|||
pdu_builder.state_key.get_or_insert_with(|| "".to_owned());
|
||||
|
||||
// Silently skip encryption events if they are not allowed
|
||||
if pdu_builder.event_type == EventType::RoomEncryption && !db.globals.allow_encryption() {
|
||||
if pdu_builder.event_type == TimelineEventType::RoomEncryption
|
||||
&& !services().globals.allow_encryption()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
db.rooms
|
||||
.build_and_append_pdu(pdu_builder, sender_user, &room_id, &db, &state_lock)?;
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
pdu_builder,
|
||||
sender_user,
|
||||
&room_id,
|
||||
&state_lock,
|
||||
)?;
|
||||
}
|
||||
|
||||
// 7. Events implied by name and topic
|
||||
if let Some(name) = &body.name {
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomName,
|
||||
content: to_raw_value(&RoomNameEventContent::new(Some(name.clone())))
|
||||
event_type: TimelineEventType::RoomName,
|
||||
content: to_raw_value(&RoomNameEventContent::new(name.clone()))
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
|
@ -374,15 +374,14 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(topic) = &body.topic {
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomTopic,
|
||||
event_type: TimelineEventType::RoomTopic,
|
||||
content: to_raw_value(&RoomTopicEventContent {
|
||||
topic: topic.clone(),
|
||||
})
|
||||
|
@ -393,7 +392,6 @@ pub async fn create_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
}
|
||||
|
@ -401,23 +399,21 @@ pub async fn create_room_route(
|
|||
// 8. Events implied by invite (and TODO: invite_3pid)
|
||||
drop(state_lock);
|
||||
for user_id in &body.invite {
|
||||
let _ = invite_helper(sender_user, user_id, &room_id, &db, body.is_direct).await;
|
||||
let _ = invite_helper(sender_user, user_id, &room_id, None, body.is_direct).await;
|
||||
}
|
||||
|
||||
// Homeserver specific stuff
|
||||
if let Some(alias) = alias {
|
||||
db.rooms.set_alias(&alias, Some(&room_id), &db.globals)?;
|
||||
services().rooms.alias.set_alias(&alias, &room_id)?;
|
||||
}
|
||||
|
||||
if body.visibility == room::Visibility::Public {
|
||||
db.rooms.set_public(&room_id, true)?;
|
||||
services().rooms.directory.set_public(&room_id)?;
|
||||
}
|
||||
|
||||
info!("{} created a room", sender_user);
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(create_room::Response::new(room_id).into())
|
||||
Ok(create_room::v3::Response::new(room_id))
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}`
|
||||
|
@ -425,32 +421,37 @@ pub async fn create_room_route(
|
|||
/// Gets a single event.
|
||||
///
|
||||
/// - You have to currently be joined to the room (TODO: Respect history visibility)
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/rooms/<_>/event/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_room_event_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_room_event::Request<'_>>,
|
||||
) -> ConduitResult<get_room_event::Response> {
|
||||
body: Ruma<get_room_event::v3::Request>,
|
||||
) -> Result<get_room_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !db.rooms.is_joined(sender_user, &body.room_id)? {
|
||||
let event = services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu(&body.event_id)?
|
||||
.ok_or_else(|| {
|
||||
warn!("Event not found, event ID: {:?}", &body.event_id);
|
||||
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
|
||||
})?;
|
||||
|
||||
if !services().rooms.state_accessor.user_can_see_event(
|
||||
sender_user,
|
||||
&event.room_id,
|
||||
&body.event_id,
|
||||
)? {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view this room.",
|
||||
"You don't have permission to view this event.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(get_room_event::Response {
|
||||
event: db
|
||||
.rooms
|
||||
.get_pdu(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Event not found."))?
|
||||
.to_room_event(),
|
||||
}
|
||||
.into())
|
||||
let mut event = (*event).clone();
|
||||
event.add_age()?;
|
||||
|
||||
Ok(get_room_event::v3::Response {
|
||||
event: event.to_room_event(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/aliases`
|
||||
|
@ -458,32 +459,30 @@ pub async fn get_room_event_route(
|
|||
/// Lists all aliases of the room.
|
||||
///
|
||||
/// - Only users joined to the room are allowed to call this TODO: Allow any user to call it if history_visibility is world readable
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/rooms/<_>/aliases", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_room_aliases_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<aliases::Request<'_>>,
|
||||
) -> ConduitResult<aliases::Response> {
|
||||
body: Ruma<aliases::v3::Request>,
|
||||
) -> Result<aliases::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !db.rooms.is_joined(sender_user, &body.room_id)? {
|
||||
if !services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(aliases::Response {
|
||||
aliases: db
|
||||
Ok(aliases::v3::Response {
|
||||
aliases: services()
|
||||
.rooms
|
||||
.room_aliases(&body.room_id)
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.filter_map(|a| a.ok())
|
||||
.collect(),
|
||||
}
|
||||
.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/upgrade`
|
||||
|
@ -496,18 +495,16 @@ pub async fn get_room_aliases_route(
|
|||
/// - Transfers some state events
|
||||
/// - Moves local aliases
|
||||
/// - Modifies old room power levels to prevent users from speaking
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/rooms/<_>/upgrade", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn upgrade_room_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<upgrade_room::Request<'_>>,
|
||||
) -> ConduitResult<upgrade_room::Response> {
|
||||
body: Ruma<upgrade_room::v3::Request>,
|
||||
) -> Result<upgrade_room::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !matches!(body.new_version, RoomVersionId::V5 | RoomVersionId::V6) {
|
||||
if !services()
|
||||
.globals
|
||||
.supported_room_versions()
|
||||
.contains(&body.new_version)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UnsupportedRoomVersion,
|
||||
"This server does not support that room version.",
|
||||
|
@ -515,12 +512,15 @@ pub async fn upgrade_room_route(
|
|||
}
|
||||
|
||||
// Create a replacement room
|
||||
let replacement_room = RoomId::new(db.globals.server_name());
|
||||
db.rooms
|
||||
.get_or_create_shortroomid(&replacement_room, &db.globals)?;
|
||||
let replacement_room = RoomId::new(services().globals.server_name());
|
||||
services()
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortroomid(&replacement_room)?;
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -531,9 +531,9 @@ pub async fn upgrade_room_route(
|
|||
|
||||
// Send a m.room.tombstone event to the old room to indicate that it is not intended to be used any further
|
||||
// Fail if the sender does not have the required permissions
|
||||
let tombstone_event_id = db.rooms.build_and_append_pdu(
|
||||
let tombstone_event_id = services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomTombstone,
|
||||
event_type: TimelineEventType::RoomTombstone,
|
||||
content: to_raw_value(&RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.clone(),
|
||||
|
@ -545,14 +545,14 @@ pub async fn upgrade_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
// Change lock to replacement room
|
||||
drop(state_lock);
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -563,8 +563,10 @@ pub async fn upgrade_room_route(
|
|||
|
||||
// Get the old room creation event
|
||||
let mut create_event_content = serde_json::from_str::<CanonicalJsonObject>(
|
||||
db.rooms
|
||||
.room_state_get(&body.room_id, &EventType::RoomCreate, "")?
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &StateEventType::RoomCreate, "")?
|
||||
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
|
||||
.content
|
||||
.get(),
|
||||
|
@ -611,9 +613,9 @@ pub async fn upgrade_room_route(
|
|||
));
|
||||
}
|
||||
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomCreate,
|
||||
event_type: TimelineEventType::RoomCreate,
|
||||
content: to_raw_value(&create_event_content)
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
|
@ -622,21 +624,20 @@ pub async fn upgrade_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
// Join the new room
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomMember,
|
||||
event_type: TimelineEventType::RoomMember,
|
||||
content: to_raw_value(&RoomMemberEventContent {
|
||||
membership: MembershipState::Join,
|
||||
displayname: db.users.displayname(sender_user)?,
|
||||
avatar_url: db.users.avatar_url(sender_user)?,
|
||||
displayname: services().users.displayname(sender_user)?,
|
||||
avatar_url: services().users.avatar_url(sender_user)?,
|
||||
is_direct: None,
|
||||
third_party_invite: None,
|
||||
blurhash: db.users.blurhash(sender_user)?,
|
||||
blurhash: services().users.blurhash(sender_user)?,
|
||||
reason: None,
|
||||
join_authorized_via_users_server: None,
|
||||
})
|
||||
|
@ -647,33 +648,37 @@ pub async fn upgrade_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
// Recommended transferable state events list from the specs
|
||||
let transferable_state_events = vec![
|
||||
EventType::RoomServerAcl,
|
||||
EventType::RoomEncryption,
|
||||
EventType::RoomName,
|
||||
EventType::RoomAvatar,
|
||||
EventType::RoomTopic,
|
||||
EventType::RoomGuestAccess,
|
||||
EventType::RoomHistoryVisibility,
|
||||
EventType::RoomJoinRules,
|
||||
EventType::RoomPowerLevels,
|
||||
StateEventType::RoomServerAcl,
|
||||
StateEventType::RoomEncryption,
|
||||
StateEventType::RoomName,
|
||||
StateEventType::RoomAvatar,
|
||||
StateEventType::RoomTopic,
|
||||
StateEventType::RoomGuestAccess,
|
||||
StateEventType::RoomHistoryVisibility,
|
||||
StateEventType::RoomJoinRules,
|
||||
StateEventType::RoomPowerLevels,
|
||||
];
|
||||
|
||||
// Replicate transferable state events to the new room
|
||||
for event_type in transferable_state_events {
|
||||
let event_content = match db.rooms.room_state_get(&body.room_id, &event_type, "")? {
|
||||
Some(v) => v.content.clone(),
|
||||
None => continue, // Skipping missing events.
|
||||
};
|
||||
let event_content =
|
||||
match services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &event_type, "")?
|
||||
{
|
||||
Some(v) => v.content.clone(),
|
||||
None => continue, // Skipping missing events.
|
||||
};
|
||||
|
||||
db.rooms.build_and_append_pdu(
|
||||
services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type,
|
||||
event_type: event_type.to_string().into(),
|
||||
content: event_content,
|
||||
unsigned: None,
|
||||
state_key: Some("".to_owned()),
|
||||
|
@ -681,21 +686,29 @@ pub async fn upgrade_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&replacement_room,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Moves any local aliases to the new room
|
||||
for alias in db.rooms.room_aliases(&body.room_id).filter_map(|r| r.ok()) {
|
||||
db.rooms
|
||||
.set_alias(&alias, Some(&replacement_room), &db.globals)?;
|
||||
for alias in services()
|
||||
.rooms
|
||||
.alias
|
||||
.local_aliases_for_room(&body.room_id)
|
||||
.filter_map(|r| r.ok())
|
||||
{
|
||||
services()
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(&alias, &replacement_room)?;
|
||||
}
|
||||
|
||||
// Get the old room power levels
|
||||
let mut power_levels_event_content: RoomPowerLevelsEventContent = serde_json::from_str(
|
||||
db.rooms
|
||||
.room_state_get(&body.room_id, &EventType::RoomPowerLevels, "")?
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &StateEventType::RoomPowerLevels, "")?
|
||||
.ok_or_else(|| Error::bad_database("Found room without m.room.create event."))?
|
||||
.content
|
||||
.get(),
|
||||
|
@ -708,9 +721,9 @@ pub async fn upgrade_room_route(
|
|||
power_levels_event_content.invite = new_level;
|
||||
|
||||
// Modify the power levels in the old room to prevent sending of events and inviting new users
|
||||
let _ = db.rooms.build_and_append_pdu(
|
||||
let _ = services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: EventType::RoomPowerLevels,
|
||||
event_type: TimelineEventType::RoomPowerLevels,
|
||||
content: to_raw_value(&power_levels_event_content)
|
||||
.expect("event is valid, we just created it"),
|
||||
unsigned: None,
|
||||
|
@ -719,14 +732,11 @@ pub async fn upgrade_room_route(
|
|||
},
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
drop(state_lock);
|
||||
|
||||
db.flush()?;
|
||||
|
||||
// Return the replacement room id
|
||||
Ok(upgrade_room::Response { replacement_room }.into())
|
||||
Ok(upgrade_room::v3::Response { replacement_room })
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use ruma::api::client::{error::ErrorKind, r0::search::search_events};
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
search::search_events::{
|
||||
self,
|
||||
v3::{EventContextResult, ResultCategories, ResultRoomEvents, SearchResult},
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::post;
|
||||
use search_events::{EventContextResult, ResultCategories, ResultRoomEvents, SearchResult};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// # `POST /_matrix/client/r0/search`
|
||||
|
@ -11,41 +14,43 @@ use std::collections::BTreeMap;
|
|||
/// Searches rooms for messages.
|
||||
///
|
||||
/// - Only works if the user is currently joined to the room (TODO: Respect history visibility)
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/search", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn search_events_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<search_events::Request<'_>>,
|
||||
) -> ConduitResult<search_events::Response> {
|
||||
body: Ruma<search_events::v3::Request>,
|
||||
) -> Result<search_events::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let search_criteria = body.search_categories.room_events.as_ref().unwrap();
|
||||
let filter = search_criteria.filter.clone().unwrap_or_default();
|
||||
let filter = &search_criteria.filter;
|
||||
|
||||
let room_ids = filter.rooms.clone().unwrap_or_else(|| {
|
||||
db.rooms
|
||||
services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(sender_user)
|
||||
.filter_map(|r| r.ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
let limit = filter.limit.map_or(10, |l| u64::from(l) as usize);
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = filter.limit.map_or(10, u64::from).min(100) as usize;
|
||||
|
||||
let mut searches = Vec::new();
|
||||
|
||||
for room_id in room_ids {
|
||||
if !db.rooms.is_joined(sender_user, &room_id)? {
|
||||
if !services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, &room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view this room.",
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(search) = db
|
||||
if let Some(search) = services()
|
||||
.rooms
|
||||
.search
|
||||
.search_pdus(&room_id, &search_criteria.search_term)?
|
||||
{
|
||||
searches.push(search.0.peekable());
|
||||
|
@ -77,6 +82,21 @@ pub async fn search_events_route(
|
|||
|
||||
let results: Vec<_> = results
|
||||
.iter()
|
||||
.filter_map(|result| {
|
||||
services()
|
||||
.rooms
|
||||
.timeline
|
||||
.get_pdu_from_id(result)
|
||||
.ok()?
|
||||
.filter(|pdu| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.map(|pdu| pdu.to_room_event())
|
||||
})
|
||||
.map(|result| {
|
||||
Ok::<_, Error>(SearchResult {
|
||||
context: EventContextResult {
|
||||
|
@ -87,10 +107,7 @@ pub async fn search_events_route(
|
|||
start: None,
|
||||
},
|
||||
rank: None,
|
||||
result: db
|
||||
.rooms
|
||||
.get_pdu_from_id(result)?
|
||||
.map(|pdu| pdu.to_room_event()),
|
||||
result: Some(result),
|
||||
})
|
||||
})
|
||||
.filter_map(|r| r.ok())
|
||||
|
@ -98,13 +115,13 @@ pub async fn search_events_route(
|
|||
.take(limit)
|
||||
.collect();
|
||||
|
||||
let next_batch = if results.len() < limit as usize {
|
||||
let next_batch = if results.len() < limit {
|
||||
None
|
||||
} else {
|
||||
Some((skip + limit).to_string())
|
||||
};
|
||||
|
||||
Ok(search_events::Response::new(ResultCategories {
|
||||
Ok(search_events::v3::Response::new(ResultCategories {
|
||||
room_events: ResultRoomEvents {
|
||||
count: Some((results.len() as u32).into()), // TODO: set this to none. Element shouldn't depend on it
|
||||
groups: BTreeMap::new(), // TODO
|
||||
|
@ -117,6 +134,5 @@ pub async fn search_events_route(
|
|||
.map(str::to_lowercase)
|
||||
.collect(),
|
||||
},
|
||||
})
|
||||
.into())
|
||||
}))
|
||||
}
|
|
@ -1,40 +1,33 @@
|
|||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use crate::{database::DatabaseGuard, utils, ConduitResult, Error, Ruma};
|
||||
use crate::{services, utils, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::{
|
||||
session::{get_login_types, login, logout, logout_all},
|
||||
uiaa::IncomingUserIdentifier,
|
||||
},
|
||||
session::{get_login_types, login, logout, logout_all},
|
||||
uiaa::UserIdentifier,
|
||||
},
|
||||
UserId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::info;
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
exp: usize,
|
||||
//exp: usize,
|
||||
}
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, post};
|
||||
|
||||
/// # `GET /_matrix/client/r0/login`
|
||||
///
|
||||
/// Get the supported login types of this server. One of these should be used as the `type` field
|
||||
/// when logging in.
|
||||
#[cfg_attr(feature = "conduit_bin", get("/_matrix/client/r0/login"))]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_login_types_route() -> ConduitResult<get_login_types::Response> {
|
||||
Ok(
|
||||
get_login_types::Response::new(vec![get_login_types::LoginType::Password(
|
||||
Default::default(),
|
||||
)])
|
||||
.into(),
|
||||
)
|
||||
pub async fn get_login_types_route(
|
||||
_body: Ruma<get_login_types::v3::Request>,
|
||||
) -> Result<get_login_types::v3::Response> {
|
||||
Ok(get_login_types::v3::Response::new(vec![
|
||||
get_login_types::v3::LoginType::Password(Default::default()),
|
||||
get_login_types::v3::LoginType::ApplicationService(Default::default()),
|
||||
]))
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/login`
|
||||
|
@ -48,36 +41,39 @@ pub async fn get_login_types_route() -> ConduitResult<get_login_types::Response>
|
|||
///
|
||||
/// Note: You can use [`GET /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
|
||||
/// supported login types.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/login", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn login_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<login::Request<'_>>,
|
||||
) -> ConduitResult<login::Response> {
|
||||
pub async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Response> {
|
||||
// To allow deprecated login methods
|
||||
#![allow(deprecated)]
|
||||
// Validate login method
|
||||
// TODO: Other login methods
|
||||
let user_id = match &body.login_info {
|
||||
login::IncomingLoginInfo::Password(login::IncomingPassword {
|
||||
login::v3::LoginInfo::Password(login::v3::Password {
|
||||
identifier,
|
||||
password,
|
||||
user,
|
||||
address: _,
|
||||
medium: _,
|
||||
}) => {
|
||||
let username = if let IncomingUserIdentifier::MatrixId(matrix_id) = identifier {
|
||||
matrix_id
|
||||
let user_id = if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(
|
||||
user_id.to_lowercase(),
|
||||
services().globals.server_name(),
|
||||
)
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse(user)
|
||||
} else {
|
||||
warn!("Bad login type: {:?}", &body.login_info);
|
||||
return Err(Error::BadRequest(ErrorKind::Forbidden, "Bad login type."));
|
||||
};
|
||||
let user_id =
|
||||
UserId::parse_with_server_name(username.to_owned(), db.globals.server_name())
|
||||
.map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
|
||||
})?;
|
||||
let hash = db.users.password_hash(&user_id)?.ok_or(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Wrong username or password.",
|
||||
))?;
|
||||
}
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
|
||||
|
||||
let hash = services()
|
||||
.users
|
||||
.password_hash(&user_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Wrong username or password.",
|
||||
))?;
|
||||
|
||||
if hash.is_empty() {
|
||||
return Err(Error::BadRequest(
|
||||
|
@ -97,16 +93,16 @@ pub async fn login_route(
|
|||
|
||||
user_id
|
||||
}
|
||||
login::IncomingLoginInfo::Token(login::IncomingToken { token }) => {
|
||||
if let Some(jwt_decoding_key) = db.globals.jwt_decoding_key() {
|
||||
login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
||||
if let Some(jwt_decoding_key) = services().globals.jwt_decoding_key() {
|
||||
let token = jsonwebtoken::decode::<Claims>(
|
||||
token,
|
||||
jwt_decoding_key,
|
||||
&jsonwebtoken::Validation::default(),
|
||||
)
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Token is invalid."))?;
|
||||
let username = token.claims.sub;
|
||||
UserId::parse_with_server_name(username, db.globals.server_name()).map_err(
|
||||
let username = token.claims.sub.to_lowercase();
|
||||
UserId::parse_with_server_name(username, services().globals.server_name()).map_err(
|
||||
|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."),
|
||||
)?
|
||||
} else {
|
||||
|
@ -116,7 +112,31 @@ pub async fn login_route(
|
|||
));
|
||||
}
|
||||
}
|
||||
login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService {
|
||||
identifier,
|
||||
user,
|
||||
}) => {
|
||||
if !body.from_appservice {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Forbidden login type.",
|
||||
));
|
||||
};
|
||||
if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||
UserId::parse_with_server_name(
|
||||
user_id.to_lowercase(),
|
||||
services().globals.server_name(),
|
||||
)
|
||||
} else if let Some(user) = user {
|
||||
UserId::parse(user)
|
||||
} else {
|
||||
warn!("Bad login type: {:?}", &body.login_info);
|
||||
return Err(Error::BadRequest(ErrorKind::Forbidden, "Bad login type."));
|
||||
}
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?
|
||||
}
|
||||
_ => {
|
||||
warn!("Unsupported or unknown login type: {:?}", &body.login_info);
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Unknown,
|
||||
"Unsupported login type.",
|
||||
|
@ -135,15 +155,16 @@ pub async fn login_route(
|
|||
|
||||
// Determine if device_id was provided and exists in the db for this user
|
||||
let device_exists = body.device_id.as_ref().map_or(false, |device_id| {
|
||||
db.users
|
||||
services()
|
||||
.users
|
||||
.all_device_ids(&user_id)
|
||||
.any(|x| x.as_ref().map_or(false, |v| v == device_id))
|
||||
});
|
||||
|
||||
if device_exists {
|
||||
db.users.set_token(&user_id, &device_id, &token)?;
|
||||
services().users.set_token(&user_id, &device_id, &token)?;
|
||||
} else {
|
||||
db.users.create_device(
|
||||
services().users.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&token,
|
||||
|
@ -153,16 +174,17 @@ pub async fn login_route(
|
|||
|
||||
info!("{} logged in", user_id);
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(login::Response {
|
||||
// Homeservers are still required to send the `home_server` field
|
||||
#[allow(deprecated)]
|
||||
Ok(login::v3::Response {
|
||||
user_id,
|
||||
access_token: token,
|
||||
home_server: Some(db.globals.server_name().to_owned()),
|
||||
home_server: Some(services().globals.server_name().to_owned()),
|
||||
device_id,
|
||||
well_known: None,
|
||||
}
|
||||
.into())
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/logout`
|
||||
|
@ -173,23 +195,13 @@ pub async fn login_route(
|
|||
/// - Deletes device metadata (device id, device display name, last seen ip, last seen ts)
|
||||
/// - Forgets to-device events
|
||||
/// - Triggers device list updates
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/logout", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn logout_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<logout::Request>,
|
||||
) -> ConduitResult<logout::Response> {
|
||||
pub async fn logout_route(body: Ruma<logout::v3::Request>) -> Result<logout::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
|
||||
|
||||
db.users.remove_device(sender_user, sender_device)?;
|
||||
services().users.remove_device(sender_user, sender_device)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(logout::Response::new().into())
|
||||
Ok(logout::v3::Response::new())
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/logout/all`
|
||||
|
@ -203,22 +215,14 @@ pub async fn logout_route(
|
|||
///
|
||||
/// Note: This is equivalent to calling [`GET /_matrix/client/r0/logout`](fn.logout_route.html)
|
||||
/// from each device of this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/logout/all", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn logout_all_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<logout_all::Request>,
|
||||
) -> ConduitResult<logout_all::Response> {
|
||||
body: Ruma<logout_all::v3::Request>,
|
||||
) -> Result<logout_all::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
for device_id in db.users.all_device_ids(sender_user).flatten() {
|
||||
db.users.remove_device(sender_user, &device_id)?;
|
||||
for device_id in services().users.all_device_ids(sender_user).flatten() {
|
||||
services().users.remove_device(sender_user, &device_id)?;
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(logout_all::Response::new().into())
|
||||
Ok(logout_all::v3::Response::new())
|
||||
}
|
34
src/api/client_server/space.rs
Normal file
34
src/api/client_server/space.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use crate::{services, Result, Ruma};
|
||||
use ruma::api::client::space::get_hierarchy;
|
||||
|
||||
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy``
|
||||
///
|
||||
/// Paginates over the space tree in a depth-first manner to locate child rooms of a given space.
|
||||
pub async fn get_hierarchy_route(
|
||||
body: Ruma<get_hierarchy::v1::Request>,
|
||||
) -> Result<get_hierarchy::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let skip = body
|
||||
.from
|
||||
.as_ref()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let limit = body.limit.map_or(10, u64::from).min(100) as usize;
|
||||
|
||||
let max_depth = body.max_depth.map_or(3, u64::from).min(10) as usize + 1; // +1 to skip the space room itself
|
||||
|
||||
services()
|
||||
.rooms
|
||||
.spaces
|
||||
.get_hierarchy(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
limit,
|
||||
skip,
|
||||
max_depth,
|
||||
body.suggested_only,
|
||||
)
|
||||
.await
|
||||
}
|
250
src/api/client_server/state.rs
Normal file
250
src/api/client_server/state.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{service::pdu::PduBuilder, services, Error, Result, Ruma, RumaResponse};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
state::{get_state_events, get_state_events_for_key, send_state_event},
|
||||
},
|
||||
events::{
|
||||
room::canonical_alias::RoomCanonicalAliasEventContent, AnyStateEventContent, StateEventType,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, RoomId, UserId,
|
||||
};
|
||||
use tracing::log::warn;
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}`
|
||||
///
|
||||
/// Sends a state event into the room.
|
||||
///
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is allowed
|
||||
/// - If event is new canonical_alias: Rejects if alias is incorrect
|
||||
pub async fn send_state_event_for_key_route(
|
||||
body: Ruma<send_state_event::v3::Request>,
|
||||
) -> Result<send_state_event::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event_id = send_state_event_for_key_helper(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_type,
|
||||
&body.body.body, // Yes, I hate it too
|
||||
body.state_key.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let event_id = (*event_id).to_owned();
|
||||
Ok(send_state_event::v3::Response { event_id })
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}`
|
||||
///
|
||||
/// Sends a state event into the room.
|
||||
///
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is allowed
|
||||
/// - If event is new canonical_alias: Rejects if alias is incorrect
|
||||
pub async fn send_state_event_for_empty_key_route(
|
||||
body: Ruma<send_state_event::v3::Request>,
|
||||
) -> Result<RumaResponse<send_state_event::v3::Response>> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
// Forbid m.room.encryption if encryption is disabled
|
||||
if body.event_type == StateEventType::RoomEncryption && !services().globals.allow_encryption() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Encryption has been disabled",
|
||||
));
|
||||
}
|
||||
|
||||
let event_id = send_state_event_for_key_helper(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
&body.event_type.to_string().into(),
|
||||
&body.body.body,
|
||||
body.state_key.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let event_id = (*event_id).to_owned();
|
||||
Ok(send_state_event::v3::Response { event_id }.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomid}/state`
|
||||
///
|
||||
/// Get all state events for a room.
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world readable
|
||||
pub async fn get_state_events_route(
|
||||
body: Ruma<get_state_events::v3::Request>,
|
||||
) -> Result<get_state_events::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(get_state_events::v3::Response {
|
||||
room_state: services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_full(&body.room_id)
|
||||
.await?
|
||||
.values()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomid}/state/{eventType}/{stateKey}`
|
||||
///
|
||||
/// Get single state event of a room.
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world readable
|
||||
pub async fn get_state_events_for_key_route(
|
||||
body: Ruma<get_state_events_for_key::v3::Request>,
|
||||
) -> Result<get_state_events_for_key::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &body.event_type, &body.state_key)?
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"State event {:?} not found in room {:?}",
|
||||
&body.event_type, &body.room_id
|
||||
);
|
||||
Error::BadRequest(ErrorKind::NotFound, "State event not found.")
|
||||
})?;
|
||||
|
||||
Ok(get_state_events_for_key::v3::Response {
|
||||
content: serde_json::from_str(event.content.get())
|
||||
.map_err(|_| Error::bad_database("Invalid event content in database"))?,
|
||||
})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomid}/state/{eventType}`
|
||||
///
|
||||
/// Get single state event of a room.
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world readable
|
||||
pub async fn get_state_events_for_empty_key_route(
|
||||
body: Ruma<get_state_events_for_key::v3::Request>,
|
||||
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_state_events(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&body.room_id, &body.event_type, "")?
|
||||
.ok_or_else(|| {
|
||||
warn!(
|
||||
"State event {:?} not found in room {:?}",
|
||||
&body.event_type, &body.room_id
|
||||
);
|
||||
Error::BadRequest(ErrorKind::NotFound, "State event not found.")
|
||||
})?;
|
||||
|
||||
Ok(get_state_events_for_key::v3::Response {
|
||||
content: serde_json::from_str(event.content.get())
|
||||
.map_err(|_| Error::bad_database("Invalid event content in database"))?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
async fn send_state_event_for_key_helper(
|
||||
sender: &UserId,
|
||||
room_id: &RoomId,
|
||||
event_type: &StateEventType,
|
||||
json: &Raw<AnyStateEventContent>,
|
||||
state_key: String,
|
||||
) -> Result<Arc<EventId>> {
|
||||
let sender_user = sender;
|
||||
|
||||
// TODO: Review this check, error if event is unparsable, use event type, allow alias if it
|
||||
// previously existed
|
||||
if let Ok(canonical_alias) =
|
||||
serde_json::from_str::<RoomCanonicalAliasEventContent>(json.json().get())
|
||||
{
|
||||
let mut aliases = canonical_alias.alt_aliases.clone();
|
||||
|
||||
if let Some(alias) = canonical_alias.alias {
|
||||
aliases.push(alias);
|
||||
}
|
||||
|
||||
for alias in aliases {
|
||||
if alias.server_name() != services().globals.server_name()
|
||||
|| services()
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_local_alias(&alias)?
|
||||
.filter(|room| room == room_id) // Make sure it's the right room
|
||||
.is_none()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You are only allowed to send canonical_alias \
|
||||
events when it's aliases already exists",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
services()
|
||||
.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.to_owned())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
let event_id = services().rooms.timeline.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type: event_type.to_string().into(),
|
||||
content: serde_json::from_str(json.json().get()).expect("content is valid json"),
|
||||
unsigned: None,
|
||||
state_key: Some(state_key),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
room_id,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
Ok(event_id)
|
||||
}
|
1746
src/api/client_server/sync.rs
Normal file
1746
src/api/client_server/sync.rs
Normal file
File diff suppressed because it is too large
Load diff
126
src/api/client_server/tag.rs
Normal file
126
src/api/client_server/tag.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::tag::{create_tag, delete_tag, get_tags},
|
||||
events::{
|
||||
tag::{TagEvent, TagEventContent},
|
||||
RoomAccountDataEventType,
|
||||
},
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
|
||||
///
|
||||
/// Adds a tag to the room.
|
||||
///
|
||||
/// - Inserts the tag into the tag event of the room account data.
|
||||
pub async fn update_tag_route(
|
||||
body: Ruma<create_tag::v3::Request>,
|
||||
) -> Result<create_tag::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services().account_data.get(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
)?;
|
||||
|
||||
let mut tags_event = event
|
||||
.map(|e| {
|
||||
serde_json::from_str(e.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
})?;
|
||||
|
||||
tags_event
|
||||
.content
|
||||
.tags
|
||||
.insert(body.tag.clone().into(), body.tag_info.clone());
|
||||
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(create_tag::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
|
||||
///
|
||||
/// Deletes a tag from the room.
|
||||
///
|
||||
/// - Removes the tag from the tag event of the room account data.
|
||||
pub async fn delete_tag_route(
|
||||
body: Ruma<delete_tag::v3::Request>,
|
||||
) -> Result<delete_tag::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services().account_data.get(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
)?;
|
||||
|
||||
let mut tags_event = event
|
||||
.map(|e| {
|
||||
serde_json::from_str(e.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
})?;
|
||||
|
||||
tags_event.content.tags.remove(&body.tag.clone().into());
|
||||
|
||||
services().account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
&serde_json::to_value(tags_event).expect("to json value always works"),
|
||||
)?;
|
||||
|
||||
Ok(delete_tag::v3::Response {})
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags`
|
||||
///
|
||||
/// Returns tags on the room.
|
||||
///
|
||||
/// - Gets the tag event of the room account data.
|
||||
pub async fn get_tags_route(body: Ruma<get_tags::v3::Request>) -> Result<get_tags::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event = services().account_data.get(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
RoomAccountDataEventType::Tag,
|
||||
)?;
|
||||
|
||||
let tags_event = event
|
||||
.map(|e| {
|
||||
serde_json::from_str(e.get())
|
||||
.map_err(|_| Error::bad_database("Invalid account data event in db."))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Ok(TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(get_tags::v3::Response {
|
||||
tags: tags_event.content.tags,
|
||||
})
|
||||
}
|
16
src/api/client_server/thirdparty.rs
Normal file
16
src/api/client_server/thirdparty.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use crate::{Result, Ruma};
|
||||
use ruma::api::client::thirdparty::get_protocols;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// # `GET /_matrix/client/r0/thirdparty/protocols`
|
||||
///
|
||||
/// TODO: Fetches all metadata about protocols supported by the homeserver.
|
||||
pub async fn get_protocols_route(
|
||||
_body: Ruma<get_protocols::v3::Request>,
|
||||
) -> Result<get_protocols::v3::Response> {
|
||||
// TODO
|
||||
Ok(get_protocols::v3::Response {
|
||||
protocols: BTreeMap::new(),
|
||||
})
|
||||
}
|
49
src/api/client_server/threads.rs
Normal file
49
src/api/client_server/threads.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use ruma::api::client::{error::ErrorKind, threads::get_threads};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomId}/threads`
|
||||
pub async fn get_threads_route(
|
||||
body: Ruma<get_threads::v1::Request>,
|
||||
) -> Result<get_threads::v1::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
// Use limit or else 10, with maximum 100
|
||||
let limit = body
|
||||
.limit
|
||||
.and_then(|l| l.try_into().ok())
|
||||
.unwrap_or(10)
|
||||
.min(100);
|
||||
|
||||
let from = if let Some(from) = &body.from {
|
||||
from.parse()
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, ""))?
|
||||
} else {
|
||||
u64::MAX
|
||||
};
|
||||
|
||||
let threads = services()
|
||||
.rooms
|
||||
.threads
|
||||
.threads_until(sender_user, &body.room_id, from, &body.include)?
|
||||
.take(limit)
|
||||
.filter_map(|r| r.ok())
|
||||
.filter(|(_, pdu)| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.user_can_see_event(sender_user, &body.room_id, &pdu.event_id)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let next_batch = threads.last().map(|(count, _)| count.to_string());
|
||||
|
||||
Ok(get_threads::v1::Response {
|
||||
chunk: threads
|
||||
.into_iter()
|
||||
.map(|(_, pdu)| pdu.to_room_event())
|
||||
.collect(),
|
||||
next_batch,
|
||||
})
|
||||
}
|
|
@ -1,93 +1,81 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
use ruma::{
|
||||
api::{
|
||||
client::{error::ErrorKind, r0::to_device::send_event_to_device},
|
||||
client::{error::ErrorKind, to_device::send_event_to_device},
|
||||
federation::{self, transactions::edu::DirectDeviceContent},
|
||||
},
|
||||
events::EventType,
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::put;
|
||||
|
||||
/// # `PUT /_matrix/client/r0/sendToDevice/{eventType}/{txnId}`
|
||||
///
|
||||
/// Send a to-device event to a set of client devices.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/sendToDevice/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn send_event_to_device_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<send_event_to_device::Request<'_>>,
|
||||
) -> ConduitResult<send_event_to_device::Response> {
|
||||
body: Ruma<send_event_to_device::v3::Request>,
|
||||
) -> Result<send_event_to_device::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let sender_device = body.sender_device.as_deref();
|
||||
|
||||
// TODO: uncomment when https://github.com/vector-im/element-android/issues/3589 is solved
|
||||
// Check if this is a new transaction id
|
||||
/*
|
||||
if db
|
||||
if services()
|
||||
.transaction_ids
|
||||
.existing_txnid(sender_user, sender_device, &body.txn_id)?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(send_event_to_device::Response.into());
|
||||
return Ok(send_event_to_device::v3::Response {});
|
||||
}
|
||||
*/
|
||||
|
||||
for (target_user_id, map) in &body.messages {
|
||||
for (target_device_id_maybe, event) in map {
|
||||
if target_user_id.server_name() != db.globals.server_name() {
|
||||
if target_user_id.server_name() != services().globals.server_name() {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(target_device_id_maybe.clone(), event.clone());
|
||||
let mut messages = BTreeMap::new();
|
||||
messages.insert(target_user_id.clone(), map);
|
||||
let count = services().globals.next_count()?;
|
||||
|
||||
db.sending.send_reliable_edu(
|
||||
services().sending.send_reliable_edu(
|
||||
target_user_id.server_name(),
|
||||
serde_json::to_vec(&federation::transactions::edu::Edu::DirectToDevice(
|
||||
DirectDeviceContent {
|
||||
sender: sender_user.clone(),
|
||||
ev_type: EventType::from(&*body.event_type),
|
||||
message_id: body.txn_id.clone(),
|
||||
ev_type: body.event_type.clone(),
|
||||
message_id: count.to_string().into(),
|
||||
messages,
|
||||
},
|
||||
))
|
||||
.expect("DirectToDevice EDU can be serialized"),
|
||||
db.globals.next_count()?,
|
||||
count,
|
||||
)?;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
match target_device_id_maybe {
|
||||
DeviceIdOrAllDevices::DeviceId(target_device_id) => db.users.add_to_device_event(
|
||||
sender_user,
|
||||
target_user_id,
|
||||
target_device_id,
|
||||
&body.event_type,
|
||||
event.deserialize_as().map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid")
|
||||
})?,
|
||||
&db.globals,
|
||||
)?,
|
||||
DeviceIdOrAllDevices::DeviceId(target_device_id) => {
|
||||
services().users.add_to_device_event(
|
||||
sender_user,
|
||||
target_user_id,
|
||||
target_device_id,
|
||||
&body.event_type.to_string(),
|
||||
event.deserialize_as().map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid")
|
||||
})?,
|
||||
)?
|
||||
}
|
||||
|
||||
DeviceIdOrAllDevices::AllDevices => {
|
||||
for target_device_id in db.users.all_device_ids(target_user_id) {
|
||||
db.users.add_to_device_event(
|
||||
for target_device_id in services().users.all_device_ids(target_user_id) {
|
||||
services().users.add_to_device_event(
|
||||
sender_user,
|
||||
target_user_id,
|
||||
&target_device_id?,
|
||||
&body.event_type,
|
||||
&body.event_type.to_string(),
|
||||
event.deserialize_as().map_err(|_| {
|
||||
Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid")
|
||||
})?,
|
||||
&db.globals,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
@ -96,10 +84,9 @@ pub async fn send_event_to_device_route(
|
|||
}
|
||||
|
||||
// Save transaction id with empty data
|
||||
db.transaction_ids
|
||||
services()
|
||||
.transaction_ids
|
||||
.add_txnid(sender_user, sender_device, &body.txn_id, &[])?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(send_event_to_device::Response {}.into())
|
||||
Ok(send_event_to_device::v3::Response {})
|
||||
}
|
40
src/api/client_server/typing.rs
Normal file
40
src/api/client_server/typing.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use crate::{services, utils, Error, Result, Ruma};
|
||||
use ruma::api::client::{error::ErrorKind, typing::create_typing_event};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId}`
|
||||
///
|
||||
/// Sets the typing state of the sender user.
|
||||
pub async fn create_typing_event_route(
|
||||
body: Ruma<create_typing_event::v3::Request>,
|
||||
) -> Result<create_typing_event::v3::Response> {
|
||||
use create_typing_event::v3::Typing;
|
||||
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if !services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_joined(sender_user, &body.room_id)?
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You are not in this room.",
|
||||
));
|
||||
}
|
||||
|
||||
if let Typing::Yes(duration) = body.state {
|
||||
services().rooms.edus.typing.typing_add(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
duration.as_millis() as u64 + utils::millis_since_unix_epoch(),
|
||||
)?;
|
||||
} else {
|
||||
services()
|
||||
.rooms
|
||||
.edus
|
||||
.typing
|
||||
.typing_remove(sender_user, &body.room_id)?;
|
||||
}
|
||||
|
||||
Ok(create_typing_event::v3::Response {})
|
||||
}
|
50
src/api/client_server/unversioned.rs
Normal file
50
src/api/client_server/unversioned.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use std::{collections::BTreeMap, iter::FromIterator};
|
||||
|
||||
use axum::{response::IntoResponse, Json};
|
||||
use ruma::api::client::{discovery::get_supported_versions, error::ErrorKind};
|
||||
|
||||
use crate::{services, Error, Result, Ruma};
|
||||
|
||||
/// # `GET /_matrix/client/versions`
|
||||
///
|
||||
/// Get the versions of the specification and unstable features supported by this server.
|
||||
///
|
||||
/// - Versions take the form MAJOR.MINOR.PATCH
|
||||
/// - Only the latest PATCH release will be reported for each MAJOR.MINOR value
|
||||
/// - Unstable features are namespaced and may include version information in their name
|
||||
///
|
||||
/// Note: Unstable features are used while developing new features. Clients should avoid using
|
||||
/// unstable features in their stable releases
|
||||
pub async fn get_supported_versions_route(
|
||||
_body: Ruma<get_supported_versions::Request>,
|
||||
) -> Result<get_supported_versions::Response> {
|
||||
let resp = get_supported_versions::Response {
|
||||
versions: vec![
|
||||
"r0.5.0".to_owned(),
|
||||
"r0.6.0".to_owned(),
|
||||
"v1.1".to_owned(),
|
||||
"v1.2".to_owned(),
|
||||
"v1.3".to_owned(),
|
||||
"v1.4".to_owned(),
|
||||
"v1.5".to_owned(),
|
||||
],
|
||||
unstable_features: BTreeMap::from_iter([("org.matrix.e2e_cross_signing".to_owned(), true)]),
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// # `GET /.well-known/matrix/client`
|
||||
pub async fn well_known_client_route(
|
||||
_body: Ruma<get_supported_versions::Request>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let client_url = match services().globals.well_known_client() {
|
||||
Some(url) => url.clone(),
|
||||
None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
|
||||
};
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"m.homeserver": {"base_url": client_url},
|
||||
"org.matrix.msc3575.proxy": {"url": client_url}
|
||||
})))
|
||||
}
|
94
src/api/client_server/user_directory.rs
Normal file
94
src/api/client_server/user_directory.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
use crate::{services, Result, Ruma};
|
||||
use ruma::{
|
||||
api::client::user_directory::search_users,
|
||||
events::{
|
||||
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
StateEventType,
|
||||
},
|
||||
};
|
||||
|
||||
/// # `POST /_matrix/client/r0/user_directory/search`
|
||||
///
|
||||
/// Searches all known users for a match.
|
||||
///
|
||||
/// - Hides any local users that aren't in any public rooms (i.e. those that have the join rule set to public)
|
||||
/// and don't share a room with the sender
|
||||
pub async fn search_users_route(
|
||||
body: Ruma<search_users::v3::Request>,
|
||||
) -> Result<search_users::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let limit = u64::from(body.limit) as usize;
|
||||
|
||||
let mut users = services().users.iter().filter_map(|user_id| {
|
||||
// Filter out buggy users (they should not exist, but you never know...)
|
||||
let user_id = user_id.ok()?;
|
||||
|
||||
let user = search_users::v3::User {
|
||||
user_id: user_id.clone(),
|
||||
display_name: services().users.displayname(&user_id).ok()?,
|
||||
avatar_url: services().users.avatar_url(&user_id).ok()?,
|
||||
};
|
||||
|
||||
let user_id_matches = user
|
||||
.user_id
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.contains(&body.search_term.to_lowercase());
|
||||
|
||||
let user_displayname_matches = user
|
||||
.display_name
|
||||
.as_ref()
|
||||
.filter(|name| {
|
||||
name.to_lowercase()
|
||||
.contains(&body.search_term.to_lowercase())
|
||||
})
|
||||
.is_some();
|
||||
|
||||
if !user_id_matches && !user_displayname_matches {
|
||||
return None;
|
||||
}
|
||||
|
||||
let user_is_in_public_rooms = services()
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
.filter_map(|r| r.ok())
|
||||
.any(|room| {
|
||||
services()
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get(&room, &StateEventType::RoomJoinRules, "")
|
||||
.map_or(false, |event| {
|
||||
event.map_or(false, |event| {
|
||||
serde_json::from_str(event.content.get())
|
||||
.map_or(false, |r: RoomJoinRulesEventContent| {
|
||||
r.join_rule == JoinRule::Public
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if user_is_in_public_rooms {
|
||||
return Some(user);
|
||||
}
|
||||
|
||||
let user_is_in_shared_rooms = services()
|
||||
.rooms
|
||||
.user
|
||||
.get_shared_rooms(vec![sender_user.clone(), user_id])
|
||||
.ok()?
|
||||
.next()
|
||||
.is_some();
|
||||
|
||||
if user_is_in_shared_rooms {
|
||||
return Some(user);
|
||||
}
|
||||
|
||||
None
|
||||
});
|
||||
|
||||
let results = users.by_ref().take(limit).collect();
|
||||
let limited = users.next().is_some();
|
||||
|
||||
Ok(search_users::v3::Response { results, limited })
|
||||
}
|
48
src/api/client_server/voip.rs
Normal file
48
src/api/client_server/voip.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use crate::{services, Result, Ruma};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use hmac::{Hmac, Mac};
|
||||
use ruma::{api::client::voip::get_turn_server_info, SecondsSinceUnixEpoch};
|
||||
use sha1::Sha1;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
|
||||
/// # `GET /_matrix/client/r0/voip/turnServer`
|
||||
///
|
||||
/// TODO: Returns information about the recommended turn server.
|
||||
pub async fn turn_server_route(
|
||||
body: Ruma<get_turn_server_info::v3::Request>,
|
||||
) -> Result<get_turn_server_info::v3::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let turn_secret = services().globals.turn_secret().clone();
|
||||
|
||||
let (username, password) = if !turn_secret.is_empty() {
|
||||
let expiry = SecondsSinceUnixEpoch::from_system_time(
|
||||
SystemTime::now() + Duration::from_secs(services().globals.turn_ttl()),
|
||||
)
|
||||
.expect("time is valid");
|
||||
|
||||
let username: String = format!("{}:{}", expiry.get(), sender_user);
|
||||
|
||||
let mut mac = HmacSha1::new_from_slice(turn_secret.as_bytes())
|
||||
.expect("HMAC can take key of any size");
|
||||
mac.update(username.as_bytes());
|
||||
|
||||
let password: String = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
|
||||
|
||||
(username, password)
|
||||
} else {
|
||||
(
|
||||
services().globals.turn_username().clone(),
|
||||
services().globals.turn_password().clone(),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(get_turn_server_info::v3::Response {
|
||||
username,
|
||||
password,
|
||||
uris: services().globals.turn_uris().to_vec(),
|
||||
ttl: Duration::from_secs(services().globals.turn_ttl()),
|
||||
})
|
||||
}
|
4
src/api/mod.rs
Normal file
4
src/api/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod appservice_server;
|
||||
pub mod client_server;
|
||||
pub mod ruma_wrapper;
|
||||
pub mod server_server;
|
427
src/api/ruma_wrapper/axum.rs
Normal file
427
src/api/ruma_wrapper/axum.rs
Normal file
|
@ -0,0 +1,427 @@
|
|||
use std::{collections::BTreeMap, iter::FromIterator, str};
|
||||
|
||||
use axum::{
|
||||
async_trait,
|
||||
body::{Full, HttpBody},
|
||||
extract::{rejection::TypedHeaderRejectionReason, FromRequest, Path, TypedHeader},
|
||||
headers::{
|
||||
authorization::{Bearer, Credentials},
|
||||
Authorization,
|
||||
},
|
||||
response::{IntoResponse, Response},
|
||||
BoxError, RequestExt, RequestPartsExt,
|
||||
};
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use http::{Request, StatusCode};
|
||||
use ruma::{
|
||||
api::{client::error::ErrorKind, AuthScheme, IncomingRequest, OutgoingResponse},
|
||||
CanonicalJsonValue, OwnedDeviceId, OwnedServerName, UserId,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
use super::{Ruma, RumaResponse};
|
||||
use crate::{services, Error, Result};
|
||||
|
||||
#[async_trait]
|
||||
impl<T, S, B> FromRequest<S, B> for Ruma<T>
|
||||
where
|
||||
T: IncomingRequest,
|
||||
B: HttpBody + Send + 'static,
|
||||
B::Data: Send,
|
||||
B::Error: Into<BoxError>,
|
||||
{
|
||||
type Rejection = Error;
|
||||
|
||||
async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
#[derive(Deserialize)]
|
||||
struct QueryParams {
|
||||
access_token: Option<String>,
|
||||
user_id: Option<String>,
|
||||
}
|
||||
|
||||
let (mut parts, mut body) = match req.with_limited_body() {
|
||||
Ok(limited_req) => {
|
||||
let (parts, body) = limited_req.into_parts();
|
||||
let body = to_bytes(body)
|
||||
.await
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::MissingToken, "Missing token."))?;
|
||||
(parts, body)
|
||||
}
|
||||
Err(original_req) => {
|
||||
let (parts, body) = original_req.into_parts();
|
||||
let body = to_bytes(body)
|
||||
.await
|
||||
.map_err(|_| Error::BadRequest(ErrorKind::MissingToken, "Missing token."))?;
|
||||
(parts, body)
|
||||
}
|
||||
};
|
||||
|
||||
let metadata = T::METADATA;
|
||||
let auth_header: Option<TypedHeader<Authorization<Bearer>>> = parts.extract().await?;
|
||||
let path_params: Path<Vec<String>> = parts.extract().await?;
|
||||
|
||||
let query = parts.uri.query().unwrap_or_default();
|
||||
let query_params: QueryParams = match serde_html_form::from_str(query) {
|
||||
Ok(params) => params,
|
||||
Err(e) => {
|
||||
error!(%query, "Failed to deserialize query parameters: {}", e);
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Unknown,
|
||||
"Failed to read query parameters",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let token = match &auth_header {
|
||||
Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
|
||||
None => query_params.access_token.as_deref(),
|
||||
};
|
||||
|
||||
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
|
||||
|
||||
let appservices = services().appservice.all().unwrap();
|
||||
let appservice_registration = appservices.iter().find(|(_id, registration)| {
|
||||
registration
|
||||
.get("as_token")
|
||||
.and_then(|as_token| as_token.as_str())
|
||||
.map_or(false, |as_token| token == Some(as_token))
|
||||
});
|
||||
|
||||
let (sender_user, sender_device, sender_servername, from_appservice) =
|
||||
if let Some((_id, registration)) = appservice_registration {
|
||||
match metadata.authentication {
|
||||
AuthScheme::AccessToken => {
|
||||
let user_id = query_params.user_id.map_or_else(
|
||||
|| {
|
||||
UserId::parse_with_server_name(
|
||||
registration
|
||||
.get("sender_localpart")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap(),
|
||||
services().globals.server_name(),
|
||||
)
|
||||
.unwrap()
|
||||
},
|
||||
|s| UserId::parse(s).unwrap(),
|
||||
);
|
||||
|
||||
if !services().users.exists(&user_id).unwrap() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"User does not exist.",
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: Check if appservice is allowed to be that user
|
||||
(Some(user_id), None, None, true)
|
||||
}
|
||||
AuthScheme::ServerSignatures => (None, None, None, true),
|
||||
AuthScheme::None => (None, None, None, true),
|
||||
}
|
||||
} else {
|
||||
match metadata.authentication {
|
||||
AuthScheme::AccessToken => {
|
||||
let token = match token {
|
||||
Some(token) => token,
|
||||
_ => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::MissingToken,
|
||||
"Missing access token.",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match services().users.find_from_token(token).unwrap() {
|
||||
None => {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::UnknownToken { soft_logout: false },
|
||||
"Unknown access token.",
|
||||
))
|
||||
}
|
||||
Some((user_id, device_id)) => (
|
||||
Some(user_id),
|
||||
Some(OwnedDeviceId::from(device_id)),
|
||||
None,
|
||||
false,
|
||||
),
|
||||
}
|
||||
}
|
||||
AuthScheme::ServerSignatures => {
|
||||
let TypedHeader(Authorization(x_matrix)) = parts
|
||||
.extract::<TypedHeader<Authorization<XMatrix>>>()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
warn!("Missing or invalid Authorization header: {}", e);
|
||||
|
||||
let msg = match e.reason() {
|
||||
TypedHeaderRejectionReason::Missing => {
|
||||
"Missing Authorization header."
|
||||
}
|
||||
TypedHeaderRejectionReason::Error(_) => {
|
||||
"Invalid X-Matrix signatures."
|
||||
}
|
||||
_ => "Unknown header-related error",
|
||||
};
|
||||
|
||||
Error::BadRequest(ErrorKind::Forbidden, msg)
|
||||
})?;
|
||||
|
||||
let origin_signatures = BTreeMap::from_iter([(
|
||||
x_matrix.key.clone(),
|
||||
CanonicalJsonValue::String(x_matrix.sig),
|
||||
)]);
|
||||
|
||||
let signatures = BTreeMap::from_iter([(
|
||||
x_matrix.origin.as_str().to_owned(),
|
||||
CanonicalJsonValue::Object(origin_signatures),
|
||||
)]);
|
||||
|
||||
let mut request_map = BTreeMap::from_iter([
|
||||
(
|
||||
"method".to_owned(),
|
||||
CanonicalJsonValue::String(parts.method.to_string()),
|
||||
),
|
||||
(
|
||||
"uri".to_owned(),
|
||||
CanonicalJsonValue::String(parts.uri.to_string()),
|
||||
),
|
||||
(
|
||||
"origin".to_owned(),
|
||||
CanonicalJsonValue::String(x_matrix.origin.as_str().to_owned()),
|
||||
),
|
||||
(
|
||||
"destination".to_owned(),
|
||||
CanonicalJsonValue::String(
|
||||
services().globals.server_name().as_str().to_owned(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"signatures".to_owned(),
|
||||
CanonicalJsonValue::Object(signatures),
|
||||
),
|
||||
]);
|
||||
|
||||
if let Some(json_body) = &json_body {
|
||||
request_map.insert("content".to_owned(), json_body.clone());
|
||||
};
|
||||
|
||||
let keys_result = services()
|
||||
.rooms
|
||||
.event_handler
|
||||
.fetch_signing_keys(&x_matrix.origin, vec![x_matrix.key.to_owned()])
|
||||
.await;
|
||||
|
||||
let keys = match keys_result {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch signing keys: {}", e);
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Failed to fetch signing keys.",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let pub_key_map =
|
||||
BTreeMap::from_iter([(x_matrix.origin.as_str().to_owned(), keys)]);
|
||||
|
||||
match ruma::signatures::verify_json(&pub_key_map, &request_map) {
|
||||
Ok(()) => (None, None, Some(x_matrix.origin), false),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to verify json request from {}: {}\n{:?}",
|
||||
x_matrix.origin, e, request_map
|
||||
);
|
||||
|
||||
if parts.uri.to_string().contains('@') {
|
||||
warn!(
|
||||
"Request uri contained '@' character. Make sure your \
|
||||
reverse proxy gives Conduit the raw uri (apache: use \
|
||||
nocanon)"
|
||||
);
|
||||
}
|
||||
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Failed to verify X-Matrix signatures.",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
AuthScheme::None => (None, None, None, false),
|
||||
}
|
||||
};
|
||||
|
||||
let mut http_request = http::Request::builder().uri(parts.uri).method(parts.method);
|
||||
*http_request.headers_mut().unwrap() = parts.headers;
|
||||
|
||||
if let Some(CanonicalJsonValue::Object(json_body)) = &mut json_body {
|
||||
let user_id = sender_user.clone().unwrap_or_else(|| {
|
||||
UserId::parse_with_server_name("", services().globals.server_name())
|
||||
.expect("we know this is valid")
|
||||
});
|
||||
|
||||
let uiaa_request = json_body
|
||||
.get("auth")
|
||||
.and_then(|auth| auth.as_object())
|
||||
.and_then(|auth| auth.get("session"))
|
||||
.and_then(|session| session.as_str())
|
||||
.and_then(|session| {
|
||||
services().uiaa.get_uiaa_request(
|
||||
&user_id,
|
||||
&sender_device.clone().unwrap_or_else(|| "".into()),
|
||||
session,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(CanonicalJsonValue::Object(initial_request)) = uiaa_request {
|
||||
for (key, value) in initial_request {
|
||||
json_body.entry(key).or_insert(value);
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = BytesMut::new().writer();
|
||||
serde_json::to_writer(&mut buf, json_body).expect("value serialization can't fail");
|
||||
body = buf.into_inner().freeze();
|
||||
}
|
||||
|
||||
let http_request = http_request.body(&*body).unwrap();
|
||||
|
||||
debug!("{:?}", http_request);
|
||||
|
||||
let body = T::try_from_http_request(http_request, &path_params).map_err(|e| {
|
||||
warn!("try_from_http_request failed: {:?}", e);
|
||||
debug!("JSON body: {:?}", json_body);
|
||||
Error::BadRequest(ErrorKind::BadJson, "Failed to deserialize request.")
|
||||
})?;
|
||||
|
||||
Ok(Ruma {
|
||||
body,
|
||||
sender_user,
|
||||
sender_device,
|
||||
sender_servername,
|
||||
from_appservice,
|
||||
json_body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct XMatrix {
|
||||
origin: OwnedServerName,
|
||||
key: String, // KeyName?
|
||||
sig: String,
|
||||
}
|
||||
|
||||
impl Credentials for XMatrix {
|
||||
const SCHEME: &'static str = "X-Matrix";
|
||||
|
||||
fn decode(value: &http::HeaderValue) -> Option<Self> {
|
||||
debug_assert!(
|
||||
value.as_bytes().starts_with(b"X-Matrix "),
|
||||
"HeaderValue to decode should start with \"X-Matrix ..\", received = {value:?}",
|
||||
);
|
||||
|
||||
let parameters = str::from_utf8(&value.as_bytes()["X-Matrix ".len()..])
|
||||
.ok()?
|
||||
.trim_start();
|
||||
|
||||
let mut origin = None;
|
||||
let mut key = None;
|
||||
let mut sig = None;
|
||||
|
||||
for entry in parameters.split_terminator(',') {
|
||||
let (name, value) = entry.split_once('=')?;
|
||||
|
||||
// It's not at all clear why some fields are quoted and others not in the spec,
|
||||
// let's simply accept either form for every field.
|
||||
let value = value
|
||||
.strip_prefix('"')
|
||||
.and_then(|rest| rest.strip_suffix('"'))
|
||||
.unwrap_or(value);
|
||||
|
||||
// FIXME: Catch multiple fields of the same name
|
||||
match name {
|
||||
"origin" => origin = Some(value.try_into().ok()?),
|
||||
"key" => key = Some(value.to_owned()),
|
||||
"sig" => sig = Some(value.to_owned()),
|
||||
_ => debug!(
|
||||
"Unexpected field `{}` in X-Matrix Authorization header",
|
||||
name
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
origin: origin?,
|
||||
key: key?,
|
||||
sig: sig?,
|
||||
})
|
||||
}
|
||||
|
||||
fn encode(&self) -> http::HeaderValue {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: OutgoingResponse> IntoResponse for RumaResponse<T> {
|
||||
fn into_response(self) -> Response {
|
||||
match self.0.try_into_http_response::<BytesMut>() {
|
||||
Ok(res) => res.map(BytesMut::freeze).map(Full::new).into_response(),
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copied from hyper under the following license:
|
||||
// Copyright (c) 2014-2021 Sean McArthur
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
pub(crate) async fn to_bytes<T>(body: T) -> Result<Bytes, T::Error>
|
||||
where
|
||||
T: HttpBody,
|
||||
{
|
||||
futures_util::pin_mut!(body);
|
||||
|
||||
// If there's only 1 chunk, we can just return Buf::to_bytes()
|
||||
let mut first = if let Some(buf) = body.data().await {
|
||||
buf?
|
||||
} else {
|
||||
return Ok(Bytes::new());
|
||||
};
|
||||
|
||||
let second = if let Some(buf) = body.data().await {
|
||||
buf?
|
||||
} else {
|
||||
return Ok(first.copy_to_bytes(first.remaining()));
|
||||
};
|
||||
|
||||
// With more than 1 buf, we gotta flatten into a Vec first.
|
||||
let cap = first.remaining() + second.remaining() + body.size_hint().lower() as usize;
|
||||
let mut vec = Vec::with_capacity(cap);
|
||||
vec.put(first);
|
||||
vec.put(second);
|
||||
|
||||
while let Some(buf) = body.data().await {
|
||||
vec.put(buf?);
|
||||
}
|
||||
|
||||
Ok(vec.into())
|
||||
}
|
43
src/api/ruma_wrapper/mod.rs
Normal file
43
src/api/ruma_wrapper/mod.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use crate::Error;
|
||||
use ruma::{
|
||||
api::client::uiaa::UiaaResponse, CanonicalJsonValue, OwnedDeviceId, OwnedServerName,
|
||||
OwnedUserId,
|
||||
};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
mod axum;
|
||||
|
||||
/// Extractor for Ruma request structs
|
||||
pub struct Ruma<T> {
|
||||
pub body: T,
|
||||
pub sender_user: Option<OwnedUserId>,
|
||||
pub sender_device: Option<OwnedDeviceId>,
|
||||
pub sender_servername: Option<OwnedServerName>,
|
||||
// This is None when body is not a valid string
|
||||
pub json_body: Option<CanonicalJsonValue>,
|
||||
pub from_appservice: bool,
|
||||
}
|
||||
|
||||
impl<T> Deref for Ruma<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.body
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RumaResponse<T>(pub T);
|
||||
|
||||
impl<T> From<T> for RumaResponse<T> {
|
||||
fn from(t: T) -> Self {
|
||||
Self(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for RumaResponse<UiaaResponse> {
|
||||
fn from(t: Error) -> Self {
|
||||
t.to_response()
|
||||
}
|
||||
}
|
2020
src/api/server_server.rs
Normal file
2020
src/api/server_server.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,432 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
r0::backup::{
|
||||
add_backup_key_session, add_backup_key_sessions, add_backup_keys, create_backup,
|
||||
delete_backup, delete_backup_key_session, delete_backup_key_sessions, delete_backup_keys,
|
||||
get_backup, get_backup_key_session, get_backup_key_sessions, get_backup_keys,
|
||||
get_latest_backup, update_backup,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{delete, get, post, put};
|
||||
|
||||
/// # `POST /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
/// Creates a new backup.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/unstable/room_keys/version", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn create_backup_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<create_backup::Request>,
|
||||
) -> ConduitResult<create_backup::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let version = db
|
||||
.key_backups
|
||||
.create_backup(sender_user, &body.algorithm, &db.globals)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(create_backup::Response { version }.into())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/version/{version}`
|
||||
///
|
||||
/// Update information about an existing backup. Only `auth_data` can be modified.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/unstable/room_keys/version/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn update_backup_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<update_backup::Request<'_>>,
|
||||
) -> ConduitResult<update_backup::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
db.key_backups
|
||||
.update_backup(sender_user, &body.version, &body.algorithm, &db.globals)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(update_backup::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
/// Get information about the latest backup version.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/unstable/room_keys/version", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_latest_backup_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_latest_backup::Request>,
|
||||
) -> ConduitResult<get_latest_backup::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let (version, algorithm) =
|
||||
db.key_backups
|
||||
.get_latest_backup(sender_user)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Key backup does not exist.",
|
||||
))?;
|
||||
|
||||
Ok(get_latest_backup::Response {
|
||||
algorithm,
|
||||
count: (db.key_backups.count_keys(sender_user, &version)? as u32).into(),
|
||||
etag: db.key_backups.get_etag(sender_user, &version)?,
|
||||
version,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/version`
|
||||
///
|
||||
/// Get information about an existing backup.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/unstable/room_keys/version/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_backup_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_backup::Request<'_>>,
|
||||
) -> ConduitResult<get_backup::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let algorithm = db
|
||||
.key_backups
|
||||
.get_backup(sender_user, &body.version)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Key backup does not exist.",
|
||||
))?;
|
||||
|
||||
Ok(get_backup::Response {
|
||||
algorithm,
|
||||
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
|
||||
etag: db.key_backups.get_etag(sender_user, &body.version)?,
|
||||
version: body.version.to_owned(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/version/{version}`
|
||||
///
|
||||
/// Delete an existing key backup.
|
||||
///
|
||||
/// - Deletes both information about the backup, as well as all key data related to the backup
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
delete("/_matrix/client/unstable/room_keys/version/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_backup_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_backup::Request<'_>>,
|
||||
) -> ConduitResult<delete_backup::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
db.key_backups.delete_backup(sender_user, &body.version)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_backup::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Add the received backup keys to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/unstable/room_keys/keys", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn add_backup_keys_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<add_backup_keys::Request<'_>>,
|
||||
) -> ConduitResult<add_backup_keys::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= db
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
for (room_id, room) in &body.rooms {
|
||||
for (session_id, key_data) in &room.sessions {
|
||||
db.key_backups.add_key(
|
||||
sender_user,
|
||||
&body.version,
|
||||
room_id,
|
||||
session_id,
|
||||
key_data,
|
||||
&db.globals,
|
||||
)?
|
||||
}
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(add_backup_keys::Response {
|
||||
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
|
||||
etag: db.key_backups.get_etag(sender_user, &body.version)?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Add the received backup keys to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn add_backup_key_sessions_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<add_backup_key_sessions::Request<'_>>,
|
||||
) -> ConduitResult<add_backup_key_sessions::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= db
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
for (session_id, key_data) in &body.sessions {
|
||||
db.key_backups.add_key(
|
||||
sender_user,
|
||||
&body.version,
|
||||
&body.room_id,
|
||||
session_id,
|
||||
key_data,
|
||||
&db.globals,
|
||||
)?
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(add_backup_key_sessions::Response {
|
||||
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
|
||||
etag: db.key_backups.get_etag(sender_user, &body.version)?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Add the received backup key to the database.
|
||||
///
|
||||
/// - Only manipulating the most recently created version of the backup is allowed
|
||||
/// - Adds the keys to the backup
|
||||
/// - Returns the new number of keys in this backup and the etag
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn add_backup_key_session_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<add_backup_key_session::Request<'_>>,
|
||||
) -> ConduitResult<add_backup_key_session::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if Some(&body.version)
|
||||
!= db
|
||||
.key_backups
|
||||
.get_latest_backup_version(sender_user)?
|
||||
.as_ref()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"You may only manipulate the most recently created version of the backup.",
|
||||
));
|
||||
}
|
||||
|
||||
db.key_backups.add_key(
|
||||
sender_user,
|
||||
&body.version,
|
||||
&body.room_id,
|
||||
&body.session_id,
|
||||
&body.session_data,
|
||||
&db.globals,
|
||||
)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(add_backup_key_session::Response {
|
||||
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
|
||||
etag: db.key_backups.get_etag(sender_user, &body.version)?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Retrieves all keys from the backup.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/unstable/room_keys/keys", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_backup_keys_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_backup_keys::Request<'_>>,
|
||||
) -> ConduitResult<get_backup_keys::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let rooms = db.key_backups.get_all(sender_user, &body.version)?;
|
||||
|
||||
Ok(get_backup_keys::Response { rooms }.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Retrieves all keys from the backup for a given room.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_backup_key_sessions_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_backup_key_sessions::Request<'_>>,
|
||||
) -> ConduitResult<get_backup_key_sessions::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let sessions = db
|
||||
.key_backups
|
||||
.get_room(sender_user, &body.version, &body.room_id)?;
|
||||
|
||||
Ok(get_backup_key_sessions::Response { sessions }.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Retrieves a key from the backup.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_backup_key_session_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_backup_key_session::Request<'_>>,
|
||||
) -> ConduitResult<get_backup_key_session::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let key_data = db
|
||||
.key_backups
|
||||
.get_session(sender_user, &body.version, &body.room_id, &body.session_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Backup key not found for this user's session.",
|
||||
))?;
|
||||
|
||||
Ok(get_backup_key_session::Response { key_data }.into())
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys`
|
||||
///
|
||||
/// Delete the keys from the backup.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
delete("/_matrix/client/unstable/room_keys/keys", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_backup_keys_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_backup_keys::Request<'_>>,
|
||||
) -> ConduitResult<delete_backup_keys::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
db.key_backups.delete_all_keys(sender_user, &body.version)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_backup_keys::Response {
|
||||
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
|
||||
etag: db.key_backups.get_etag(sender_user, &body.version)?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}`
|
||||
///
|
||||
/// Delete the keys from the backup for a given room.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
delete("/_matrix/client/unstable/room_keys/keys/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_backup_key_sessions_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_backup_key_sessions::Request<'_>>,
|
||||
) -> ConduitResult<delete_backup_key_sessions::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
db.key_backups
|
||||
.delete_room_keys(sender_user, &body.version, &body.room_id)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_backup_key_sessions::Response {
|
||||
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
|
||||
etag: db.key_backups.get_etag(sender_user, &body.version)?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/room_keys/keys/{roomId}/{sessionId}`
|
||||
///
|
||||
/// Delete a key from the backup.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
delete("/_matrix/client/unstable/room_keys/keys/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_backup_key_session_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_backup_key_session::Request<'_>>,
|
||||
) -> ConduitResult<delete_backup_key_session::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
db.key_backups
|
||||
.delete_room_key(sender_user, &body.version, &body.room_id, &body.session_id)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_backup_key_session::Response {
|
||||
count: (db.key_backups.count_keys(sender_user, &body.version)? as u32).into(),
|
||||
etag: db.key_backups.get_etag(sender_user, &body.version)?,
|
||||
}
|
||||
.into())
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
use crate::{ConduitResult, Ruma};
|
||||
use ruma::{
|
||||
api::client::r0::capabilities::{
|
||||
get_capabilities, Capabilities, RoomVersionStability, RoomVersionsCapability,
|
||||
},
|
||||
RoomVersionId,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::get;
|
||||
|
||||
/// # `GET /_matrix/client/r0/capabilities`
|
||||
///
|
||||
/// Get information on the supported feature set and other relevent capabilities of this server.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/capabilities", data = "<_body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(_body))]
|
||||
pub async fn get_capabilities_route(
|
||||
_body: Ruma<get_capabilities::Request>,
|
||||
) -> ConduitResult<get_capabilities::Response> {
|
||||
let mut available = BTreeMap::new();
|
||||
available.insert(RoomVersionId::V5, RoomVersionStability::Stable);
|
||||
available.insert(RoomVersionId::V6, RoomVersionStability::Stable);
|
||||
|
||||
let mut capabilities = Capabilities::new();
|
||||
capabilities.room_versions = RoomVersionsCapability {
|
||||
default: RoomVersionId::V6,
|
||||
available,
|
||||
};
|
||||
|
||||
Ok(get_capabilities::Response { capabilities }.into())
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use ruma::api::client::{
|
||||
error::ErrorKind,
|
||||
r0::filter::{create_filter, get_filter},
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, post};
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/filter/{filterId}`
|
||||
///
|
||||
/// Loads a filter that was previously created.
|
||||
///
|
||||
/// - A user can only access their own filters
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/user/<_>/filter/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_filter_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_filter::Request<'_>>,
|
||||
) -> ConduitResult<get_filter::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let filter = match db.users.get_filter(sender_user, &body.filter_id)? {
|
||||
Some(filter) => filter,
|
||||
None => return Err(Error::BadRequest(ErrorKind::NotFound, "Filter not found.")),
|
||||
};
|
||||
|
||||
Ok(get_filter::Response::new(filter).into())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/filter`
|
||||
///
|
||||
/// Creates a new filter to be used by other endpoints.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/user/<_>/filter", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn create_filter_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<create_filter::Request<'_>>,
|
||||
) -> ConduitResult<create_filter::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
Ok(create_filter::Response::new(db.users.create_filter(sender_user, &body.filter)?).into())
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,588 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::push::{
|
||||
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions, get_pushrule_enabled,
|
||||
get_pushrules_all, set_pusher, set_pushrule, set_pushrule_actions,
|
||||
set_pushrule_enabled, RuleKind,
|
||||
},
|
||||
},
|
||||
events::{push_rules::PushRulesEvent, EventType},
|
||||
push::{ConditionalPushRuleInit, PatternedPushRuleInit, SimplePushRuleInit},
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{delete, get, post, put};
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules`
|
||||
///
|
||||
/// Retrieves the push rules event for this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/pushrules", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_pushrules_all_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_pushrules_all::Request>,
|
||||
) -> ConduitResult<get_pushrules_all::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event: PushRulesEvent = db
|
||||
.account_data
|
||||
.get(None, sender_user, EventType::PushRules)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
Ok(get_pushrules_all::Response {
|
||||
global: event.content.global,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Retrieves a single specified push rule for this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/pushrules/<_>/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_pushrule_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_pushrule::Request<'_>>,
|
||||
) -> ConduitResult<get_pushrule::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event: PushRulesEvent = db
|
||||
.account_data
|
||||
.get(None, sender_user, EventType::PushRules)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let global = event.content.global;
|
||||
let rule = match body.kind {
|
||||
RuleKind::Override => global
|
||||
.override_
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.clone().into()),
|
||||
RuleKind::Underride => global
|
||||
.underride
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.clone().into()),
|
||||
RuleKind::Sender => global
|
||||
.sender
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.clone().into()),
|
||||
RuleKind::Room => global
|
||||
.room
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.clone().into()),
|
||||
RuleKind::Content => global
|
||||
.content
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.clone().into()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(rule) = rule {
|
||||
Ok(get_pushrule::Response { rule }.into())
|
||||
} else {
|
||||
Err(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"Push rule not found.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Creates a single specified push rule for this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/pushrules/<_>/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_pushrule_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_pushrule::Request<'_>>,
|
||||
) -> ConduitResult<set_pushrule::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let body = body.body;
|
||||
|
||||
if body.scope != "global" {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut event: PushRulesEvent = db
|
||||
.account_data
|
||||
.get(None, sender_user, EventType::PushRules)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let global = &mut event.content.global;
|
||||
match body.kind {
|
||||
RuleKind::Override => {
|
||||
global.override_.replace(
|
||||
ConditionalPushRuleInit {
|
||||
actions: body.actions,
|
||||
default: false,
|
||||
enabled: true,
|
||||
rule_id: body.rule_id,
|
||||
conditions: body.conditions,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
RuleKind::Underride => {
|
||||
global.underride.replace(
|
||||
ConditionalPushRuleInit {
|
||||
actions: body.actions,
|
||||
default: false,
|
||||
enabled: true,
|
||||
rule_id: body.rule_id,
|
||||
conditions: body.conditions,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
RuleKind::Sender => {
|
||||
global.sender.replace(
|
||||
SimplePushRuleInit {
|
||||
actions: body.actions,
|
||||
default: false,
|
||||
enabled: true,
|
||||
rule_id: body.rule_id,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
RuleKind::Room => {
|
||||
global.room.replace(
|
||||
SimplePushRuleInit {
|
||||
actions: body.actions,
|
||||
default: false,
|
||||
enabled: true,
|
||||
rule_id: body.rule_id,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
RuleKind::Content => {
|
||||
global.content.replace(
|
||||
PatternedPushRuleInit {
|
||||
actions: body.actions,
|
||||
default: false,
|
||||
enabled: true,
|
||||
rule_id: body.rule_id,
|
||||
pattern: body.pattern.unwrap_or_default(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
db.account_data
|
||||
.update(None, sender_user, EventType::PushRules, &event, &db.globals)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_pushrule::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
|
||||
///
|
||||
/// Gets the actions of a single specified push rule for this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/pushrules/<_>/<_>/<_>/actions", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_pushrule_actions_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_pushrule_actions::Request<'_>>,
|
||||
) -> ConduitResult<get_pushrule_actions::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != "global" {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut event: PushRulesEvent = db
|
||||
.account_data
|
||||
.get(None, sender_user, EventType::PushRules)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let global = &mut event.content.global;
|
||||
let actions = match body.kind {
|
||||
RuleKind::Override => global
|
||||
.override_
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.actions.clone()),
|
||||
RuleKind::Underride => global
|
||||
.underride
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.actions.clone()),
|
||||
RuleKind::Sender => global
|
||||
.sender
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.actions.clone()),
|
||||
RuleKind::Room => global
|
||||
.room
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.actions.clone()),
|
||||
RuleKind::Content => global
|
||||
.content
|
||||
.get(body.rule_id.as_str())
|
||||
.map(|rule| rule.actions.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(get_pushrule_actions::Response {
|
||||
actions: actions.unwrap_or_default(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
|
||||
///
|
||||
/// Sets the actions of a single specified push rule for this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/pushrules/<_>/<_>/<_>/actions", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_pushrule_actions_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_pushrule_actions::Request<'_>>,
|
||||
) -> ConduitResult<set_pushrule_actions::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != "global" {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut event: PushRulesEvent = db
|
||||
.account_data
|
||||
.get(None, sender_user, EventType::PushRules)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let global = &mut event.content.global;
|
||||
match body.kind {
|
||||
RuleKind::Override => {
|
||||
if let Some(mut rule) = global.override_.get(body.rule_id.as_str()).cloned() {
|
||||
rule.actions = body.actions.clone();
|
||||
global.override_.replace(rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Underride => {
|
||||
if let Some(mut rule) = global.underride.get(body.rule_id.as_str()).cloned() {
|
||||
rule.actions = body.actions.clone();
|
||||
global.underride.replace(rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Sender => {
|
||||
if let Some(mut rule) = global.sender.get(body.rule_id.as_str()).cloned() {
|
||||
rule.actions = body.actions.clone();
|
||||
global.sender.replace(rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Room => {
|
||||
if let Some(mut rule) = global.room.get(body.rule_id.as_str()).cloned() {
|
||||
rule.actions = body.actions.clone();
|
||||
global.room.replace(rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Content => {
|
||||
if let Some(mut rule) = global.content.get(body.rule_id.as_str()).cloned() {
|
||||
rule.actions = body.actions.clone();
|
||||
global.content.replace(rule);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
db.account_data
|
||||
.update(None, sender_user, EventType::PushRules, &event, &db.globals)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_pushrule_actions::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
|
||||
///
|
||||
/// Gets the enabled status of a single specified push rule for this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/pushrules/<_>/<_>/<_>/enabled", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_pushrule_enabled_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_pushrule_enabled::Request<'_>>,
|
||||
) -> ConduitResult<get_pushrule_enabled::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != "global" {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut event: PushRulesEvent = db
|
||||
.account_data
|
||||
.get(None, sender_user, EventType::PushRules)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let global = &mut event.content.global;
|
||||
let enabled = match body.kind {
|
||||
RuleKind::Override => global
|
||||
.override_
|
||||
.iter()
|
||||
.find(|rule| rule.rule_id == body.rule_id)
|
||||
.map_or(false, |rule| rule.enabled),
|
||||
RuleKind::Underride => global
|
||||
.underride
|
||||
.iter()
|
||||
.find(|rule| rule.rule_id == body.rule_id)
|
||||
.map_or(false, |rule| rule.enabled),
|
||||
RuleKind::Sender => global
|
||||
.sender
|
||||
.iter()
|
||||
.find(|rule| rule.rule_id == body.rule_id)
|
||||
.map_or(false, |rule| rule.enabled),
|
||||
RuleKind::Room => global
|
||||
.room
|
||||
.iter()
|
||||
.find(|rule| rule.rule_id == body.rule_id)
|
||||
.map_or(false, |rule| rule.enabled),
|
||||
RuleKind::Content => global
|
||||
.content
|
||||
.iter()
|
||||
.find(|rule| rule.rule_id == body.rule_id)
|
||||
.map_or(false, |rule| rule.enabled),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(get_pushrule_enabled::Response { enabled }.into())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/enabled`
|
||||
///
|
||||
/// Sets the enabled status of a single specified push rule for this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/pushrules/<_>/<_>/<_>/enabled", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_pushrule_enabled_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_pushrule_enabled::Request<'_>>,
|
||||
) -> ConduitResult<set_pushrule_enabled::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != "global" {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut event: PushRulesEvent = db
|
||||
.account_data
|
||||
.get(None, sender_user, EventType::PushRules)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let global = &mut event.content.global;
|
||||
match body.kind {
|
||||
RuleKind::Override => {
|
||||
if let Some(mut rule) = global.override_.get(body.rule_id.as_str()).cloned() {
|
||||
global.override_.remove(&rule);
|
||||
rule.enabled = body.enabled;
|
||||
global.override_.insert(rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Underride => {
|
||||
if let Some(mut rule) = global.underride.get(body.rule_id.as_str()).cloned() {
|
||||
global.underride.remove(&rule);
|
||||
rule.enabled = body.enabled;
|
||||
global.underride.insert(rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Sender => {
|
||||
if let Some(mut rule) = global.sender.get(body.rule_id.as_str()).cloned() {
|
||||
global.sender.remove(&rule);
|
||||
rule.enabled = body.enabled;
|
||||
global.sender.insert(rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Room => {
|
||||
if let Some(mut rule) = global.room.get(body.rule_id.as_str()).cloned() {
|
||||
global.room.remove(&rule);
|
||||
rule.enabled = body.enabled;
|
||||
global.room.insert(rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Content => {
|
||||
if let Some(mut rule) = global.content.get(body.rule_id.as_str()).cloned() {
|
||||
global.content.remove(&rule);
|
||||
rule.enabled = body.enabled;
|
||||
global.content.insert(rule);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
db.account_data
|
||||
.update(None, sender_user, EventType::PushRules, &event, &db.globals)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_pushrule_enabled::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}`
|
||||
///
|
||||
/// Deletes a single specified push rule for this user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
delete("/_matrix/client/r0/pushrules/<_>/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_pushrule_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_pushrule::Request<'_>>,
|
||||
) -> ConduitResult<delete_pushrule::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if body.scope != "global" {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Scopes other than 'global' are not supported.",
|
||||
));
|
||||
}
|
||||
|
||||
let mut event: PushRulesEvent = db
|
||||
.account_data
|
||||
.get(None, sender_user, EventType::PushRules)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"PushRules event not found.",
|
||||
))?;
|
||||
|
||||
let global = &mut event.content.global;
|
||||
match body.kind {
|
||||
RuleKind::Override => {
|
||||
if let Some(rule) = global.override_.get(body.rule_id.as_str()).cloned() {
|
||||
global.override_.remove(&rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Underride => {
|
||||
if let Some(rule) = global.underride.get(body.rule_id.as_str()).cloned() {
|
||||
global.underride.remove(&rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Sender => {
|
||||
if let Some(rule) = global.sender.get(body.rule_id.as_str()).cloned() {
|
||||
global.sender.remove(&rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Room => {
|
||||
if let Some(rule) = global.room.get(body.rule_id.as_str()).cloned() {
|
||||
global.room.remove(&rule);
|
||||
}
|
||||
}
|
||||
RuleKind::Content => {
|
||||
if let Some(rule) = global.content.get(body.rule_id.as_str()).cloned() {
|
||||
global.content.remove(&rule);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
db.account_data
|
||||
.update(None, sender_user, EventType::PushRules, &event, &db.globals)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_pushrule::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/pushers`
|
||||
///
|
||||
/// Gets all currently active pushers for the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/pushers", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_pushers_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_pushers::Request>,
|
||||
) -> ConduitResult<get_pushers::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_pushers::Response {
|
||||
pushers: db.pusher.get_pushers(sender_user)?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/pushers/set`
|
||||
///
|
||||
/// Adds a pusher for the sender user.
|
||||
///
|
||||
/// - TODO: Handle `append`
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/pushers/set", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_pushers_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_pusher::Request>,
|
||||
) -> ConduitResult<set_pusher::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
let pusher = body.pusher.clone();
|
||||
|
||||
db.pusher.set_pusher(sender_user, pusher)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_pusher::Response::default().into())
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Error, Ruma};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::{read_marker::set_read_marker, receipt::create_receipt},
|
||||
},
|
||||
events::{AnyEphemeralRoomEvent, EventType},
|
||||
receipt::ReceiptType,
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::post;
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
|
||||
///
|
||||
/// Sets different types of read markers.
|
||||
///
|
||||
/// - Updates fully-read account data event to `fully_read`
|
||||
/// - If `read_receipt` is set: Update private marker and public read receipt EDU
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/rooms/<_>/read_markers", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn set_read_marker_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<set_read_marker::Request<'_>>,
|
||||
) -> ConduitResult<set_read_marker::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let fully_read_event = ruma::events::fully_read::FullyReadEvent {
|
||||
content: ruma::events::fully_read::FullyReadEventContent {
|
||||
event_id: body.fully_read.clone(),
|
||||
},
|
||||
};
|
||||
db.account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
EventType::FullyRead,
|
||||
&fully_read_event,
|
||||
&db.globals,
|
||||
)?;
|
||||
|
||||
if let Some(event) = &body.read_receipt {
|
||||
db.rooms.edus.private_read_set(
|
||||
&body.room_id,
|
||||
sender_user,
|
||||
db.rooms.get_pdu_count(event)?.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Event does not exist.",
|
||||
))?,
|
||||
&db.globals,
|
||||
)?;
|
||||
db.rooms
|
||||
.reset_notification_counts(sender_user, &body.room_id)?;
|
||||
|
||||
let mut user_receipts = BTreeMap::new();
|
||||
user_receipts.insert(
|
||||
sender_user.clone(),
|
||||
ruma::events::receipt::Receipt {
|
||||
ts: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
},
|
||||
);
|
||||
|
||||
let mut receipts = BTreeMap::new();
|
||||
receipts.insert(ReceiptType::Read, user_receipts);
|
||||
|
||||
let mut receipt_content = BTreeMap::new();
|
||||
receipt_content.insert(event.to_owned(), receipts);
|
||||
|
||||
db.rooms.edus.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
AnyEphemeralRoomEvent::Receipt(ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
}),
|
||||
&db.globals,
|
||||
)?;
|
||||
}
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(set_read_marker::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/r0/rooms/{roomId}/receipt/{receiptType}/{eventId}`
|
||||
///
|
||||
/// Sets private read marker and public read receipt EDU.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/rooms/<_>/receipt/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn create_receipt_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<create_receipt::Request<'_>>,
|
||||
) -> ConduitResult<create_receipt::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
db.rooms.edus.private_read_set(
|
||||
&body.room_id,
|
||||
sender_user,
|
||||
db.rooms
|
||||
.get_pdu_count(&body.event_id)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::InvalidParam,
|
||||
"Event does not exist.",
|
||||
))?,
|
||||
&db.globals,
|
||||
)?;
|
||||
db.rooms
|
||||
.reset_notification_counts(sender_user, &body.room_id)?;
|
||||
|
||||
let mut user_receipts = BTreeMap::new();
|
||||
user_receipts.insert(
|
||||
sender_user.clone(),
|
||||
ruma::events::receipt::Receipt {
|
||||
ts: Some(MilliSecondsSinceUnixEpoch::now()),
|
||||
},
|
||||
);
|
||||
let mut receipts = BTreeMap::new();
|
||||
receipts.insert(ReceiptType::Read, user_receipts);
|
||||
|
||||
let mut receipt_content = BTreeMap::new();
|
||||
receipt_content.insert(body.event_id.to_owned(), receipts);
|
||||
|
||||
db.rooms.edus.readreceipt_update(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
AnyEphemeralRoomEvent::Receipt(ruma::events::receipt::ReceiptEvent {
|
||||
content: ruma::events::receipt::ReceiptEventContent(receipt_content),
|
||||
room_id: body.room_id.clone(),
|
||||
}),
|
||||
&db.globals,
|
||||
)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(create_receipt::Response {}.into())
|
||||
}
|
|
@ -1,328 +0,0 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
database::DatabaseGuard, pdu::PduBuilder, ConduitResult, Database, Error, Result, Ruma,
|
||||
};
|
||||
use ruma::{
|
||||
api::client::{
|
||||
error::ErrorKind,
|
||||
r0::state::{get_state_events, get_state_events_for_key, send_state_event},
|
||||
},
|
||||
events::{
|
||||
room::{
|
||||
canonical_alias::RoomCanonicalAliasEventContent,
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
},
|
||||
AnyStateEventContent, EventType,
|
||||
},
|
||||
serde::Raw,
|
||||
EventId, RoomId, UserId,
|
||||
};
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, put};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}`
|
||||
///
|
||||
/// Sends a state event into the room.
|
||||
///
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is allowed
|
||||
/// - If event is new canonical_alias: Rejects if alias is incorrect
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/rooms/<_>/state/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn send_state_event_for_key_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<send_state_event::Request<'_>>,
|
||||
) -> ConduitResult<send_state_event::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let event_id = send_state_event_for_key_helper(
|
||||
&db,
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
EventType::from(&*body.event_type),
|
||||
&body.body.body, // Yes, I hate it too
|
||||
body.state_key.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
let event_id = (*event_id).to_owned();
|
||||
Ok(send_state_event::Response { event_id }.into())
|
||||
}
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}`
|
||||
///
|
||||
/// Sends a state event into the room.
|
||||
///
|
||||
/// - The only requirement for the content is that it has to be valid json
|
||||
/// - Tries to send the event into the room, auth rules will determine if it is allowed
|
||||
/// - If event is new canonical_alias: Rejects if alias is incorrect
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/rooms/<_>/state/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn send_state_event_for_empty_key_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<send_state_event::Request<'_>>,
|
||||
) -> ConduitResult<send_state_event::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
// Forbid m.room.encryption if encryption is disabled
|
||||
if &body.event_type == "m.room.encryption" && !db.globals.allow_encryption() {
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"Encryption has been disabled",
|
||||
));
|
||||
}
|
||||
|
||||
let event_id = send_state_event_for_key_helper(
|
||||
&db,
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
EventType::from(&*body.event_type),
|
||||
&body.body.body,
|
||||
body.state_key.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
let event_id = (*event_id).to_owned();
|
||||
Ok(send_state_event::Response { event_id }.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomid}/state`
|
||||
///
|
||||
/// Get all state events for a room.
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world readable
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/rooms/<_>/state", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_state_events_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_state_events::Request<'_>>,
|
||||
) -> ConduitResult<get_state_events::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
#[allow(clippy::blocks_in_if_conditions)]
|
||||
// Users not in the room should not be able to access the state unless history_visibility is
|
||||
// WorldReadable
|
||||
if !db.rooms.is_joined(sender_user, &body.room_id)?
|
||||
&& !matches!(
|
||||
db.rooms
|
||||
.room_state_get(&body.room_id, &EventType::RoomHistoryVisibility, "")?
|
||||
.map(|event| {
|
||||
serde_json::from_str(event.content.get())
|
||||
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
|
||||
.map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid room history visibility event in database.",
|
||||
)
|
||||
})
|
||||
}),
|
||||
Some(Ok(HistoryVisibility::WorldReadable))
|
||||
)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(get_state_events::Response {
|
||||
room_state: db
|
||||
.rooms
|
||||
.room_state_full(&body.room_id)?
|
||||
.values()
|
||||
.map(|pdu| pdu.to_state_event())
|
||||
.collect(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomid}/state/{eventType}/{stateKey}`
|
||||
///
|
||||
/// Get single state event of a room.
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world readable
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/rooms/<_>/state/<_>/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_state_events_for_key_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_state_events_for_key::Request<'_>>,
|
||||
) -> ConduitResult<get_state_events_for_key::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
#[allow(clippy::blocks_in_if_conditions)]
|
||||
// Users not in the room should not be able to access the state unless history_visibility is
|
||||
// WorldReadable
|
||||
if !db.rooms.is_joined(sender_user, &body.room_id)?
|
||||
&& !matches!(
|
||||
db.rooms
|
||||
.room_state_get(&body.room_id, &EventType::RoomHistoryVisibility, "")?
|
||||
.map(|event| {
|
||||
serde_json::from_str(event.content.get())
|
||||
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
|
||||
.map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid room history visibility event in database.",
|
||||
)
|
||||
})
|
||||
}),
|
||||
Some(Ok(HistoryVisibility::WorldReadable))
|
||||
)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = db
|
||||
.rooms
|
||||
.room_state_get(&body.room_id, &body.event_type, &body.state_key)?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"State event not found.",
|
||||
))?;
|
||||
|
||||
Ok(get_state_events_for_key::Response {
|
||||
content: serde_json::from_str(event.content.get())
|
||||
.map_err(|_| Error::bad_database("Invalid event content in database"))?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/rooms/{roomid}/state/{eventType}`
|
||||
///
|
||||
/// Get single state event of a room.
|
||||
///
|
||||
/// - If not joined: Only works if current room history visibility is world readable
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/rooms/<_>/state/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_state_events_for_empty_key_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_state_events_for_key::Request<'_>>,
|
||||
) -> ConduitResult<get_state_events_for_key::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
#[allow(clippy::blocks_in_if_conditions)]
|
||||
// Users not in the room should not be able to access the state unless history_visibility is
|
||||
// WorldReadable
|
||||
if !db.rooms.is_joined(sender_user, &body.room_id)?
|
||||
&& !matches!(
|
||||
db.rooms
|
||||
.room_state_get(&body.room_id, &EventType::RoomHistoryVisibility, "")?
|
||||
.map(|event| {
|
||||
serde_json::from_str(event.content.get())
|
||||
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
|
||||
.map_err(|_| {
|
||||
Error::bad_database(
|
||||
"Invalid room history visibility event in database.",
|
||||
)
|
||||
})
|
||||
}),
|
||||
Some(Ok(HistoryVisibility::WorldReadable))
|
||||
)
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You don't have permission to view the room state.",
|
||||
));
|
||||
}
|
||||
|
||||
let event = db
|
||||
.rooms
|
||||
.room_state_get(&body.room_id, &body.event_type, "")?
|
||||
.ok_or(Error::BadRequest(
|
||||
ErrorKind::NotFound,
|
||||
"State event not found.",
|
||||
))?;
|
||||
|
||||
Ok(get_state_events_for_key::Response {
|
||||
content: serde_json::from_str(event.content.get())
|
||||
.map_err(|_| Error::bad_database("Invalid event content in database"))?,
|
||||
}
|
||||
.into())
|
||||
}
|
||||
|
||||
async fn send_state_event_for_key_helper(
|
||||
db: &Database,
|
||||
sender: &UserId,
|
||||
room_id: &RoomId,
|
||||
event_type: EventType,
|
||||
json: &Raw<AnyStateEventContent>,
|
||||
state_key: String,
|
||||
) -> Result<Arc<EventId>> {
|
||||
let sender_user = sender;
|
||||
|
||||
// TODO: Review this check, error if event is unparsable, use event type, allow alias if it
|
||||
// previously existed
|
||||
if let Ok(canonical_alias) =
|
||||
serde_json::from_str::<RoomCanonicalAliasEventContent>(json.json().get())
|
||||
{
|
||||
let mut aliases = canonical_alias.alt_aliases.clone();
|
||||
|
||||
if let Some(alias) = canonical_alias.alias {
|
||||
aliases.push(alias);
|
||||
}
|
||||
|
||||
for alias in aliases {
|
||||
if alias.server_name() != db.globals.server_name()
|
||||
|| db
|
||||
.rooms
|
||||
.id_from_alias(&alias)?
|
||||
.filter(|room| room == room_id) // Make sure it's the right room
|
||||
.is_none()
|
||||
{
|
||||
return Err(Error::BadRequest(
|
||||
ErrorKind::Forbidden,
|
||||
"You are only allowed to send canonical_alias \
|
||||
events when it's aliases already exists",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mutex_state = Arc::clone(
|
||||
db.globals
|
||||
.roomid_mutex_state
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.to_owned())
|
||||
.or_default(),
|
||||
);
|
||||
let state_lock = mutex_state.lock().await;
|
||||
|
||||
let event_id = db.rooms.build_and_append_pdu(
|
||||
PduBuilder {
|
||||
event_type,
|
||||
content: serde_json::from_str(json.json().get()).expect("content is valid json"),
|
||||
unsigned: None,
|
||||
state_key: Some(state_key),
|
||||
redacts: None,
|
||||
},
|
||||
sender_user,
|
||||
room_id,
|
||||
db,
|
||||
&state_lock,
|
||||
)?;
|
||||
|
||||
Ok(event_id)
|
||||
}
|
|
@ -1,928 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Database, Error, Result, Ruma, RumaResponse};
|
||||
use ruma::{
|
||||
api::client::r0::{
|
||||
filter::{IncomingFilterDefinition, LazyLoadOptions},
|
||||
sync::sync_events,
|
||||
uiaa::UiaaResponse,
|
||||
},
|
||||
events::{
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
AnySyncEphemeralRoomEvent, EventType,
|
||||
},
|
||||
serde::Raw,
|
||||
DeviceId, RoomId, UserId,
|
||||
};
|
||||
use std::{
|
||||
collections::{hash_map::Entry, BTreeMap, HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::watch::Sender;
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{get, tokio};
|
||||
|
||||
/// # `GET /_matrix/client/r0/sync`
|
||||
///
|
||||
/// Synchronize the client's state with the latest state on the server.
|
||||
///
|
||||
/// - This endpoint takes a `since` parameter which should be the `next_batch` value from a
|
||||
/// previous request for incremental syncs.
|
||||
///
|
||||
/// Calling this endpoint without a `since` parameter returns:
|
||||
/// - Some of the most recent events of each timeline
|
||||
/// - Notification counts for each room
|
||||
/// - Joined and invited member counts, heroes
|
||||
/// - All state events
|
||||
///
|
||||
/// Calling this endpoint with a `since` parameter from a previous `next_batch` returns:
|
||||
/// For joined rooms:
|
||||
/// - Some of the most recent events of each timeline that happened after since
|
||||
/// - If user joined the room after since: All state events (unless lazy loading is activated) and
|
||||
/// all device list updates in that room
|
||||
/// - If the user was already in the room: A list of all events that are in the state now, but were
|
||||
/// not in the state at `since`
|
||||
/// - If the state we send contains a member event: Joined and invited member counts, heroes
|
||||
/// - Device list updates that happened after `since`
|
||||
/// - If there are events in the timeline we send or the user send updated his read mark: Notification counts
|
||||
/// - EDUs that are active now (read receipts, typing updates, presence)
|
||||
/// - TODO: Allow multiple sync streams to support Pantalaimon
|
||||
///
|
||||
/// For invited rooms:
|
||||
/// - If the user was invited after `since`: A subset of the state of the room at the point of the invite
|
||||
///
|
||||
/// For left rooms:
|
||||
/// - If the user left after `since`: prev_batch token, empty state (TODO: subset of the state at the point of the leave)
|
||||
///
|
||||
/// - Sync is handled in an async task, multiple requests from the same device with the same
|
||||
/// `since` will be cached
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/sync", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn sync_events_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<sync_events::Request<'_>>,
|
||||
) -> Result<RumaResponse<sync_events::Response>, RumaResponse<UiaaResponse>> {
|
||||
let sender_user = body.sender_user.expect("user is authenticated");
|
||||
let sender_device = body.sender_device.expect("user is authenticated");
|
||||
let body = body.body;
|
||||
|
||||
let arc_db = Arc::new(db);
|
||||
|
||||
let mut rx = match arc_db
|
||||
.globals
|
||||
.sync_receivers
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry((sender_user.clone(), sender_device.clone()))
|
||||
{
|
||||
Entry::Vacant(v) => {
|
||||
let (tx, rx) = tokio::sync::watch::channel(None);
|
||||
|
||||
v.insert((body.since.clone(), rx.clone()));
|
||||
|
||||
tokio::spawn(sync_helper_wrapper(
|
||||
Arc::clone(&arc_db),
|
||||
sender_user.clone(),
|
||||
sender_device.clone(),
|
||||
body,
|
||||
tx,
|
||||
));
|
||||
|
||||
rx
|
||||
}
|
||||
Entry::Occupied(mut o) => {
|
||||
if o.get().0 != body.since {
|
||||
let (tx, rx) = tokio::sync::watch::channel(None);
|
||||
|
||||
o.insert((body.since.clone(), rx.clone()));
|
||||
|
||||
tokio::spawn(sync_helper_wrapper(
|
||||
Arc::clone(&arc_db),
|
||||
sender_user.clone(),
|
||||
sender_device.clone(),
|
||||
body,
|
||||
tx,
|
||||
));
|
||||
|
||||
rx
|
||||
} else {
|
||||
o.get().1.clone()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let we_have_to_wait = rx.borrow().is_none();
|
||||
if we_have_to_wait {
|
||||
if let Err(e) = rx.changed().await {
|
||||
error!("Error waiting for sync: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let result = match rx
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.expect("When sync channel changes it's always set to some")
|
||||
{
|
||||
Ok(response) => Ok(response.clone()),
|
||||
Err(error) => Err(error.to_response()),
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn sync_helper_wrapper(
|
||||
db: Arc<DatabaseGuard>,
|
||||
sender_user: Box<UserId>,
|
||||
sender_device: Box<DeviceId>,
|
||||
body: sync_events::IncomingRequest,
|
||||
tx: Sender<Option<ConduitResult<sync_events::Response>>>,
|
||||
) {
|
||||
let since = body.since.clone();
|
||||
|
||||
let r = sync_helper(
|
||||
Arc::clone(&db),
|
||||
sender_user.clone(),
|
||||
sender_device.clone(),
|
||||
body,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok((_, caching_allowed)) = r {
|
||||
if !caching_allowed {
|
||||
match db
|
||||
.globals
|
||||
.sync_receivers
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry((sender_user, sender_device))
|
||||
{
|
||||
Entry::Occupied(o) => {
|
||||
// Only remove if the device didn't start a different /sync already
|
||||
if o.get().0 == since {
|
||||
o.remove();
|
||||
}
|
||||
}
|
||||
Entry::Vacant(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(db);
|
||||
|
||||
let _ = tx.send(Some(r.map(|(r, _)| r.into())));
|
||||
}
|
||||
|
||||
async fn sync_helper(
|
||||
db: Arc<DatabaseGuard>,
|
||||
sender_user: Box<UserId>,
|
||||
sender_device: Box<DeviceId>,
|
||||
body: sync_events::IncomingRequest,
|
||||
// bool = caching allowed
|
||||
) -> Result<(sync_events::Response, bool), Error> {
|
||||
// TODO: match body.set_presence {
|
||||
db.rooms.edus.ping_presence(&sender_user)?;
|
||||
|
||||
// Setup watchers, so if there's no response, we can wait for them
|
||||
let watcher = db.watch(&sender_user, &sender_device);
|
||||
|
||||
let next_batch = db.globals.current_count()?;
|
||||
let next_batch_string = next_batch.to_string();
|
||||
|
||||
// Load filter
|
||||
let filter = match body.filter {
|
||||
None => IncomingFilterDefinition::default(),
|
||||
Some(sync_events::IncomingFilter::FilterDefinition(filter)) => filter,
|
||||
Some(sync_events::IncomingFilter::FilterId(filter_id)) => db
|
||||
.users
|
||||
.get_filter(&sender_user, &filter_id)?
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
let (lazy_load_enabled, lazy_load_send_redundant) = match filter.room.state.lazy_load_options {
|
||||
LazyLoadOptions::Enabled {
|
||||
include_redundant_members: redundant,
|
||||
} => (true, redundant),
|
||||
_ => (false, false),
|
||||
};
|
||||
|
||||
let mut joined_rooms = BTreeMap::new();
|
||||
let since = body
|
||||
.since
|
||||
.clone()
|
||||
.and_then(|string| string.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut presence_updates = HashMap::new();
|
||||
let mut left_encrypted_users = HashSet::new(); // Users that have left any encrypted rooms the sender was in
|
||||
let mut device_list_updates = HashSet::new();
|
||||
let mut device_list_left = HashSet::new();
|
||||
|
||||
// Look for device list updates of this account
|
||||
device_list_updates.extend(
|
||||
db.users
|
||||
.keys_changed(&sender_user.to_string(), since, None)
|
||||
.filter_map(|r| r.ok()),
|
||||
);
|
||||
|
||||
let all_joined_rooms = db.rooms.rooms_joined(&sender_user).collect::<Vec<_>>();
|
||||
for room_id in all_joined_rooms {
|
||||
let room_id = room_id?;
|
||||
|
||||
// Get and drop the lock to wait for remaining operations to finish
|
||||
// This will make sure the we have all events until next_batch
|
||||
let mutex_insert = Arc::clone(
|
||||
db.globals
|
||||
.roomid_mutex_insert
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let insert_lock = mutex_insert.lock().unwrap();
|
||||
drop(insert_lock);
|
||||
|
||||
let mut non_timeline_pdus = db
|
||||
.rooms
|
||||
.pdus_until(&sender_user, &room_id, u64::MAX)?
|
||||
.filter_map(|r| {
|
||||
// Filter out buggy events
|
||||
if r.is_err() {
|
||||
error!("Bad pdu in pdus_since: {:?}", r);
|
||||
}
|
||||
r.ok()
|
||||
})
|
||||
.take_while(|(pduid, _)| {
|
||||
db.rooms
|
||||
.pdu_count(pduid)
|
||||
.map_or(false, |count| count > since)
|
||||
});
|
||||
|
||||
// Take the last 10 events for the timeline
|
||||
let timeline_pdus: Vec<_> = non_timeline_pdus
|
||||
.by_ref()
|
||||
.take(10)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect();
|
||||
|
||||
let send_notification_counts = !timeline_pdus.is_empty()
|
||||
|| db
|
||||
.rooms
|
||||
.edus
|
||||
.last_privateread_update(&sender_user, &room_id)?
|
||||
> since;
|
||||
|
||||
// They /sync response doesn't always return all messages, so we say the output is
|
||||
// limited unless there are events in non_timeline_pdus
|
||||
let limited = non_timeline_pdus.next().is_some();
|
||||
|
||||
let mut timeline_users = HashSet::new();
|
||||
for (_, event) in &timeline_pdus {
|
||||
timeline_users.insert(event.sender.as_str().to_owned());
|
||||
}
|
||||
|
||||
db.rooms
|
||||
.lazy_load_confirm_delivery(&sender_user, &sender_device, &room_id, since)?;
|
||||
|
||||
// Database queries:
|
||||
|
||||
let current_shortstatehash = db
|
||||
.rooms
|
||||
.current_shortstatehash(&room_id)?
|
||||
.expect("All rooms have state");
|
||||
|
||||
let since_shortstatehash = db.rooms.get_token_shortstatehash(&room_id, since)?;
|
||||
|
||||
// Calculates joined_member_count, invited_member_count and heroes
|
||||
let calculate_counts = || {
|
||||
let joined_member_count = db.rooms.room_joined_count(&room_id)?.unwrap_or(0);
|
||||
let invited_member_count = db.rooms.room_invited_count(&room_id)?.unwrap_or(0);
|
||||
|
||||
// Recalculate heroes (first 5 members)
|
||||
let mut heroes = Vec::new();
|
||||
|
||||
if joined_member_count + invited_member_count <= 5 {
|
||||
// Go through all PDUs and for each member event, check if the user is still joined or
|
||||
// invited until we have 5 or we reach the end
|
||||
|
||||
for hero in db
|
||||
.rooms
|
||||
.all_pdus(&sender_user, &room_id)?
|
||||
.filter_map(|pdu| pdu.ok()) // Ignore all broken pdus
|
||||
.filter(|(_, pdu)| pdu.kind == EventType::RoomMember)
|
||||
.map(|(_, pdu)| {
|
||||
let content: RoomMemberEventContent =
|
||||
serde_json::from_str(pdu.content.get()).map_err(|_| {
|
||||
Error::bad_database("Invalid member event in database.")
|
||||
})?;
|
||||
|
||||
if let Some(state_key) = &pdu.state_key {
|
||||
let user_id = UserId::parse(state_key.clone()).map_err(|_| {
|
||||
Error::bad_database("Invalid UserId in member PDU.")
|
||||
})?;
|
||||
|
||||
// The membership was and still is invite or join
|
||||
if matches!(
|
||||
content.membership,
|
||||
MembershipState::Join | MembershipState::Invite
|
||||
) && (db.rooms.is_joined(&user_id, &room_id)?
|
||||
|| db.rooms.is_invited(&user_id, &room_id)?)
|
||||
{
|
||||
Ok::<_, Error>(Some(state_key.clone()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
// Filter out buggy users
|
||||
.filter_map(|u| u.ok())
|
||||
// Filter for possible heroes
|
||||
.flatten()
|
||||
{
|
||||
if heroes.contains(&hero) || hero == sender_user.as_str() {
|
||||
continue;
|
||||
}
|
||||
|
||||
heroes.push(hero);
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, Error>((
|
||||
Some(joined_member_count),
|
||||
Some(invited_member_count),
|
||||
heroes,
|
||||
))
|
||||
};
|
||||
|
||||
let (
|
||||
heroes,
|
||||
joined_member_count,
|
||||
invited_member_count,
|
||||
joined_since_last_sync,
|
||||
state_events,
|
||||
) = if since_shortstatehash.is_none() {
|
||||
// Probably since = 0, we will do an initial sync
|
||||
|
||||
let (joined_member_count, invited_member_count, heroes) = calculate_counts()?;
|
||||
|
||||
let current_state_ids = db.rooms.state_full_ids(current_shortstatehash)?;
|
||||
|
||||
let mut state_events = Vec::new();
|
||||
let mut lazy_loaded = HashSet::new();
|
||||
|
||||
for (shortstatekey, id) in current_state_ids {
|
||||
let (event_type, state_key) = db.rooms.get_statekey_from_short(shortstatekey)?;
|
||||
|
||||
if event_type != EventType::RoomMember {
|
||||
let pdu = match db.rooms.get_pdu(&id)? {
|
||||
Some(pdu) => pdu,
|
||||
None => {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
state_events.push(pdu);
|
||||
} else if !lazy_load_enabled
|
||||
|| body.full_state
|
||||
|| timeline_users.contains(&state_key)
|
||||
{
|
||||
let pdu = match db.rooms.get_pdu(&id)? {
|
||||
Some(pdu) => pdu,
|
||||
None => {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
lazy_loaded.insert(
|
||||
UserId::parse(state_key.as_ref())
|
||||
.expect("they are in timeline_users, so they should be correct"),
|
||||
);
|
||||
state_events.push(pdu);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset lazy loading because this is an initial sync
|
||||
db.rooms
|
||||
.lazy_load_reset(&sender_user, &sender_device, &room_id)?;
|
||||
|
||||
// The state_events above should contain all timeline_users, let's mark them as lazy
|
||||
// loaded.
|
||||
db.rooms.lazy_load_mark_sent(
|
||||
&sender_user,
|
||||
&sender_device,
|
||||
&room_id,
|
||||
lazy_loaded,
|
||||
next_batch,
|
||||
);
|
||||
|
||||
(
|
||||
heroes,
|
||||
joined_member_count,
|
||||
invited_member_count,
|
||||
true,
|
||||
state_events,
|
||||
)
|
||||
} else if timeline_pdus.is_empty() && since_shortstatehash == Some(current_shortstatehash) {
|
||||
// No state changes
|
||||
(Vec::new(), None, None, false, Vec::new())
|
||||
} else {
|
||||
// Incremental /sync
|
||||
let since_shortstatehash = since_shortstatehash.unwrap();
|
||||
|
||||
let since_sender_member: Option<RoomMemberEventContent> = db
|
||||
.rooms
|
||||
.state_get(
|
||||
since_shortstatehash,
|
||||
&EventType::RoomMember,
|
||||
sender_user.as_str(),
|
||||
)?
|
||||
.and_then(|pdu| {
|
||||
serde_json::from_str(pdu.content.get())
|
||||
.map_err(|_| Error::bad_database("Invalid PDU in database."))
|
||||
.ok()
|
||||
});
|
||||
|
||||
let joined_since_last_sync = since_sender_member
|
||||
.map_or(true, |member| member.membership != MembershipState::Join);
|
||||
|
||||
let mut state_events = Vec::new();
|
||||
let mut lazy_loaded = HashSet::new();
|
||||
|
||||
if since_shortstatehash != current_shortstatehash {
|
||||
let current_state_ids = db.rooms.state_full_ids(current_shortstatehash)?;
|
||||
let since_state_ids = db.rooms.state_full_ids(since_shortstatehash)?;
|
||||
|
||||
for (key, id) in current_state_ids {
|
||||
if body.full_state || since_state_ids.get(&key) != Some(&id) {
|
||||
let pdu = match db.rooms.get_pdu(&id)? {
|
||||
Some(pdu) => pdu,
|
||||
None => {
|
||||
error!("Pdu in state not found: {}", id);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if pdu.kind == EventType::RoomMember {
|
||||
match UserId::parse(
|
||||
pdu.state_key
|
||||
.as_ref()
|
||||
.expect("State event has state key")
|
||||
.clone(),
|
||||
) {
|
||||
Ok(state_key_userid) => {
|
||||
lazy_loaded.insert(state_key_userid);
|
||||
}
|
||||
Err(e) => error!("Invalid state key for member event: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
state_events.push(pdu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (_, event) in &timeline_pdus {
|
||||
if lazy_loaded.contains(&event.sender) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !db.rooms.lazy_load_was_sent_before(
|
||||
&sender_user,
|
||||
&sender_device,
|
||||
&room_id,
|
||||
&event.sender,
|
||||
)? || lazy_load_send_redundant
|
||||
{
|
||||
if let Some(member_event) = db.rooms.room_state_get(
|
||||
&room_id,
|
||||
&EventType::RoomMember,
|
||||
event.sender.as_str(),
|
||||
)? {
|
||||
lazy_loaded.insert(event.sender.clone());
|
||||
state_events.push(member_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.rooms.lazy_load_mark_sent(
|
||||
&sender_user,
|
||||
&sender_device,
|
||||
&room_id,
|
||||
lazy_loaded,
|
||||
next_batch,
|
||||
);
|
||||
|
||||
let encrypted_room = db
|
||||
.rooms
|
||||
.state_get(current_shortstatehash, &EventType::RoomEncryption, "")?
|
||||
.is_some();
|
||||
|
||||
let since_encryption =
|
||||
db.rooms
|
||||
.state_get(since_shortstatehash, &EventType::RoomEncryption, "")?;
|
||||
|
||||
// Calculations:
|
||||
let new_encrypted_room = encrypted_room && since_encryption.is_none();
|
||||
|
||||
let send_member_count = state_events
|
||||
.iter()
|
||||
.any(|event| event.kind == EventType::RoomMember);
|
||||
|
||||
if encrypted_room {
|
||||
for state_event in &state_events {
|
||||
if state_event.kind != EventType::RoomMember {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(state_key) = &state_event.state_key {
|
||||
let user_id = UserId::parse(state_key.clone())
|
||||
.map_err(|_| Error::bad_database("Invalid UserId in member PDU."))?;
|
||||
|
||||
if user_id == sender_user {
|
||||
continue;
|
||||
}
|
||||
|
||||
let new_membership = serde_json::from_str::<RoomMemberEventContent>(
|
||||
state_event.content.get(),
|
||||
)
|
||||
.map_err(|_| Error::bad_database("Invalid PDU in database."))?
|
||||
.membership;
|
||||
|
||||
match new_membership {
|
||||
MembershipState::Join => {
|
||||
// A new user joined an encrypted room
|
||||
if !share_encrypted_room(&db, &sender_user, &user_id, &room_id)? {
|
||||
device_list_updates.insert(user_id);
|
||||
}
|
||||
}
|
||||
MembershipState::Leave => {
|
||||
// Write down users that have left encrypted rooms we are in
|
||||
left_encrypted_users.insert(user_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if joined_since_last_sync && encrypted_room || new_encrypted_room {
|
||||
// If the user is in a new encrypted room, give them all joined users
|
||||
device_list_updates.extend(
|
||||
db.rooms
|
||||
.room_members(&room_id)
|
||||
.flatten()
|
||||
.filter(|user_id| {
|
||||
// Don't send key updates from the sender to the sender
|
||||
&sender_user != user_id
|
||||
})
|
||||
.filter(|user_id| {
|
||||
// Only send keys if the sender doesn't share an encrypted room with the target already
|
||||
!share_encrypted_room(&db, &sender_user, user_id, &room_id)
|
||||
.unwrap_or(false)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let (joined_member_count, invited_member_count, heroes) = if send_member_count {
|
||||
calculate_counts()?
|
||||
} else {
|
||||
(None, None, Vec::new())
|
||||
};
|
||||
|
||||
(
|
||||
heroes,
|
||||
joined_member_count,
|
||||
invited_member_count,
|
||||
joined_since_last_sync,
|
||||
state_events,
|
||||
)
|
||||
};
|
||||
|
||||
// Look for device list updates in this room
|
||||
device_list_updates.extend(
|
||||
db.users
|
||||
.keys_changed(&room_id.to_string(), since, None)
|
||||
.filter_map(|r| r.ok()),
|
||||
);
|
||||
|
||||
let notification_count = if send_notification_counts {
|
||||
Some(
|
||||
db.rooms
|
||||
.notification_count(&sender_user, &room_id)?
|
||||
.try_into()
|
||||
.expect("notification count can't go that high"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let highlight_count = if send_notification_counts {
|
||||
Some(
|
||||
db.rooms
|
||||
.highlight_count(&sender_user, &room_id)?
|
||||
.try_into()
|
||||
.expect("highlight count can't go that high"),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let prev_batch = timeline_pdus
|
||||
.first()
|
||||
.map_or(Ok::<_, Error>(None), |(pdu_id, _)| {
|
||||
Ok(Some(db.rooms.pdu_count(pdu_id)?.to_string()))
|
||||
})?;
|
||||
|
||||
let room_events: Vec<_> = timeline_pdus
|
||||
.iter()
|
||||
.map(|(_, pdu)| pdu.to_sync_room_event())
|
||||
.collect();
|
||||
|
||||
let mut edus: Vec<_> = db
|
||||
.rooms
|
||||
.edus
|
||||
.readreceipts_since(&room_id, since)
|
||||
.filter_map(|r| r.ok()) // Filter out buggy events
|
||||
.map(|(_, _, v)| v)
|
||||
.collect();
|
||||
|
||||
if db.rooms.edus.last_typing_update(&room_id, &db.globals)? > since {
|
||||
edus.push(
|
||||
serde_json::from_str(
|
||||
&serde_json::to_string(&AnySyncEphemeralRoomEvent::Typing(
|
||||
db.rooms.edus.typings_all(&room_id)?,
|
||||
))
|
||||
.expect("event is valid, we just created it"),
|
||||
)
|
||||
.expect("event is valid, we just created it"),
|
||||
);
|
||||
}
|
||||
|
||||
// Save the state after this sync so we can send the correct state diff next sync
|
||||
db.rooms
|
||||
.associate_token_shortstatehash(&room_id, next_batch, current_shortstatehash)?;
|
||||
|
||||
let joined_room = sync_events::JoinedRoom {
|
||||
account_data: sync_events::RoomAccountData {
|
||||
events: db
|
||||
.account_data
|
||||
.changes_since(Some(&room_id), &sender_user, since)?
|
||||
.into_iter()
|
||||
.filter_map(|(_, v)| {
|
||||
serde_json::from_str(v.json().get())
|
||||
.map_err(|_| Error::bad_database("Invalid account event in database."))
|
||||
.ok()
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
summary: sync_events::RoomSummary {
|
||||
heroes,
|
||||
joined_member_count: joined_member_count.map(|n| (n as u32).into()),
|
||||
invited_member_count: invited_member_count.map(|n| (n as u32).into()),
|
||||
},
|
||||
unread_notifications: sync_events::UnreadNotificationsCount {
|
||||
highlight_count,
|
||||
notification_count,
|
||||
},
|
||||
timeline: sync_events::Timeline {
|
||||
limited: limited || joined_since_last_sync,
|
||||
prev_batch,
|
||||
events: room_events,
|
||||
},
|
||||
state: sync_events::State {
|
||||
events: state_events
|
||||
.iter()
|
||||
.map(|pdu| pdu.to_sync_state_event())
|
||||
.collect(),
|
||||
},
|
||||
ephemeral: sync_events::Ephemeral { events: edus },
|
||||
};
|
||||
|
||||
if !joined_room.is_empty() {
|
||||
joined_rooms.insert(room_id.clone(), joined_room);
|
||||
}
|
||||
|
||||
// Take presence updates from this room
|
||||
for (user_id, presence) in
|
||||
db.rooms
|
||||
.edus
|
||||
.presence_since(&room_id, since, &db.rooms, &db.globals)?
|
||||
{
|
||||
match presence_updates.entry(user_id) {
|
||||
Entry::Vacant(v) => {
|
||||
v.insert(presence);
|
||||
}
|
||||
Entry::Occupied(mut o) => {
|
||||
let p = o.get_mut();
|
||||
|
||||
// Update existing presence event with more info
|
||||
p.content.presence = presence.content.presence;
|
||||
if let Some(status_msg) = presence.content.status_msg {
|
||||
p.content.status_msg = Some(status_msg);
|
||||
}
|
||||
if let Some(last_active_ago) = presence.content.last_active_ago {
|
||||
p.content.last_active_ago = Some(last_active_ago);
|
||||
}
|
||||
if let Some(displayname) = presence.content.displayname {
|
||||
p.content.displayname = Some(displayname);
|
||||
}
|
||||
if let Some(avatar_url) = presence.content.avatar_url {
|
||||
p.content.avatar_url = Some(avatar_url);
|
||||
}
|
||||
if let Some(currently_active) = presence.content.currently_active {
|
||||
p.content.currently_active = Some(currently_active);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut left_rooms = BTreeMap::new();
|
||||
let all_left_rooms: Vec<_> = db.rooms.rooms_left(&sender_user).collect();
|
||||
for result in all_left_rooms {
|
||||
let (room_id, left_state_events) = result?;
|
||||
|
||||
// Get and drop the lock to wait for remaining operations to finish
|
||||
let mutex_insert = Arc::clone(
|
||||
db.globals
|
||||
.roomid_mutex_insert
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let insert_lock = mutex_insert.lock().unwrap();
|
||||
drop(insert_lock);
|
||||
|
||||
let left_count = db.rooms.get_left_count(&room_id, &sender_user)?;
|
||||
|
||||
// Left before last sync
|
||||
if Some(since) >= left_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
left_rooms.insert(
|
||||
room_id.clone(),
|
||||
sync_events::LeftRoom {
|
||||
account_data: sync_events::RoomAccountData { events: Vec::new() },
|
||||
timeline: sync_events::Timeline {
|
||||
limited: false,
|
||||
prev_batch: Some(next_batch_string.clone()),
|
||||
events: Vec::new(),
|
||||
},
|
||||
state: sync_events::State {
|
||||
events: left_state_events,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let mut invited_rooms = BTreeMap::new();
|
||||
let all_invited_rooms: Vec<_> = db.rooms.rooms_invited(&sender_user).collect();
|
||||
for result in all_invited_rooms {
|
||||
let (room_id, invite_state_events) = result?;
|
||||
|
||||
// Get and drop the lock to wait for remaining operations to finish
|
||||
let mutex_insert = Arc::clone(
|
||||
db.globals
|
||||
.roomid_mutex_insert
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(room_id.clone())
|
||||
.or_default(),
|
||||
);
|
||||
let insert_lock = mutex_insert.lock().unwrap();
|
||||
drop(insert_lock);
|
||||
|
||||
let invite_count = db.rooms.get_invite_count(&room_id, &sender_user)?;
|
||||
|
||||
// Invited before last sync
|
||||
if Some(since) >= invite_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
invited_rooms.insert(
|
||||
room_id.clone(),
|
||||
sync_events::InvitedRoom {
|
||||
invite_state: sync_events::InviteState {
|
||||
events: invite_state_events,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for user_id in left_encrypted_users {
|
||||
let still_share_encrypted_room = db
|
||||
.rooms
|
||||
.get_shared_rooms(vec![sender_user.clone(), user_id.clone()])?
|
||||
.filter_map(|r| r.ok())
|
||||
.filter_map(|other_room_id| {
|
||||
Some(
|
||||
db.rooms
|
||||
.room_state_get(&other_room_id, &EventType::RoomEncryption, "")
|
||||
.ok()?
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.all(|encrypted| !encrypted);
|
||||
// If the user doesn't share an encrypted room with the target anymore, we need to tell
|
||||
// them
|
||||
if still_share_encrypted_room {
|
||||
device_list_left.insert(user_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all to-device events the device received *last time*
|
||||
db.users
|
||||
.remove_to_device_events(&sender_user, &sender_device, since)?;
|
||||
|
||||
let response = sync_events::Response {
|
||||
next_batch: next_batch_string,
|
||||
rooms: sync_events::Rooms {
|
||||
leave: left_rooms,
|
||||
join: joined_rooms,
|
||||
invite: invited_rooms,
|
||||
knock: BTreeMap::new(), // TODO
|
||||
},
|
||||
presence: sync_events::Presence {
|
||||
events: presence_updates
|
||||
.into_iter()
|
||||
.map(|(_, v)| Raw::new(&v).expect("PresenceEvent always serializes successfully"))
|
||||
.collect(),
|
||||
},
|
||||
account_data: sync_events::GlobalAccountData {
|
||||
events: db
|
||||
.account_data
|
||||
.changes_since(None, &sender_user, since)?
|
||||
.into_iter()
|
||||
.filter_map(|(_, v)| {
|
||||
serde_json::from_str(v.json().get())
|
||||
.map_err(|_| Error::bad_database("Invalid account event in database."))
|
||||
.ok()
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
device_lists: sync_events::DeviceLists {
|
||||
changed: device_list_updates.into_iter().collect(),
|
||||
left: device_list_left.into_iter().collect(),
|
||||
},
|
||||
device_one_time_keys_count: db.users.count_one_time_keys(&sender_user, &sender_device)?,
|
||||
to_device: sync_events::ToDevice {
|
||||
events: db
|
||||
.users
|
||||
.get_to_device_events(&sender_user, &sender_device)?,
|
||||
},
|
||||
// Fallback keys are not yet supported
|
||||
device_unused_fallback_key_types: None,
|
||||
};
|
||||
|
||||
// TODO: Retry the endpoint instead of returning (waiting for #118)
|
||||
if !body.full_state
|
||||
&& response.rooms.is_empty()
|
||||
&& response.presence.is_empty()
|
||||
&& response.account_data.is_empty()
|
||||
&& response.device_lists.is_empty()
|
||||
&& response.to_device.is_empty()
|
||||
{
|
||||
// Hang a few seconds so requests are not spammed
|
||||
// Stop hanging if new info arrives
|
||||
let mut duration = body.timeout.unwrap_or_default();
|
||||
if duration.as_secs() > 30 {
|
||||
duration = Duration::from_secs(30);
|
||||
}
|
||||
let _ = tokio::time::timeout(duration, watcher).await;
|
||||
Ok((response, false))
|
||||
} else {
|
||||
Ok((response, since != next_batch)) // Only cache if we made progress
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(db))]
|
||||
fn share_encrypted_room(
|
||||
db: &Database,
|
||||
sender_user: &UserId,
|
||||
user_id: &UserId,
|
||||
ignore_room: &RoomId,
|
||||
) -> Result<bool> {
|
||||
Ok(db
|
||||
.rooms
|
||||
.get_shared_rooms(vec![sender_user.to_owned(), user_id.to_owned()])?
|
||||
.filter_map(|r| r.ok())
|
||||
.filter(|room_id| room_id != ignore_room)
|
||||
.filter_map(|other_room_id| {
|
||||
Some(
|
||||
db.rooms
|
||||
.room_state_get(&other_room_id, &EventType::RoomEncryption, "")
|
||||
.ok()?
|
||||
.is_some(),
|
||||
)
|
||||
})
|
||||
.any(|encrypted| encrypted))
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Ruma};
|
||||
use ruma::{
|
||||
api::client::r0::tag::{create_tag, delete_tag, get_tags},
|
||||
events::{
|
||||
tag::{TagEvent, TagEventContent},
|
||||
EventType,
|
||||
},
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::{delete, get, put};
|
||||
|
||||
/// # `PUT /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
|
||||
///
|
||||
/// Adds a tag to the room.
|
||||
///
|
||||
/// - Inserts the tag into the tag event of the room account data.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/user/<_>/rooms/<_>/tags/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn update_tag_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<create_tag::Request<'_>>,
|
||||
) -> ConduitResult<create_tag::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut tags_event = db
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, EventType::Tag)?
|
||||
.unwrap_or_else(|| TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
});
|
||||
tags_event
|
||||
.content
|
||||
.tags
|
||||
.insert(body.tag.clone().into(), body.tag_info.clone());
|
||||
|
||||
db.account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
EventType::Tag,
|
||||
&tags_event,
|
||||
&db.globals,
|
||||
)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(create_tag::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `DELETE /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags/{tag}`
|
||||
///
|
||||
/// Deletes a tag from the room.
|
||||
///
|
||||
/// - Removes the tag from the tag event of the room account data.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
delete("/_matrix/client/r0/user/<_>/rooms/<_>/tags/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn delete_tag_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<delete_tag::Request<'_>>,
|
||||
) -> ConduitResult<delete_tag::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let mut tags_event = db
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, EventType::Tag)?
|
||||
.unwrap_or_else(|| TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
});
|
||||
tags_event.content.tags.remove(&body.tag.clone().into());
|
||||
|
||||
db.account_data.update(
|
||||
Some(&body.room_id),
|
||||
sender_user,
|
||||
EventType::Tag,
|
||||
&tags_event,
|
||||
&db.globals,
|
||||
)?;
|
||||
|
||||
db.flush()?;
|
||||
|
||||
Ok(delete_tag::Response {}.into())
|
||||
}
|
||||
|
||||
/// # `GET /_matrix/client/r0/user/{userId}/rooms/{roomId}/tags`
|
||||
///
|
||||
/// Returns tags on the room.
|
||||
///
|
||||
/// - Gets the tag event of the room account data.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/user/<_>/rooms/<_>/tags", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn get_tags_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<get_tags::Request<'_>>,
|
||||
) -> ConduitResult<get_tags::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
Ok(get_tags::Response {
|
||||
tags: db
|
||||
.account_data
|
||||
.get(Some(&body.room_id), sender_user, EventType::Tag)?
|
||||
.unwrap_or_else(|| TagEvent {
|
||||
content: TagEventContent {
|
||||
tags: BTreeMap::new(),
|
||||
},
|
||||
})
|
||||
.content
|
||||
.tags,
|
||||
}
|
||||
.into())
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
use crate::ConduitResult;
|
||||
use ruma::api::client::r0::thirdparty::get_protocols;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::get;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// # `GET /_matrix/client/r0/thirdparty/protocols`
|
||||
///
|
||||
/// TODO: Fetches all metadata about protocols supported by the homeserver.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/thirdparty/protocols")
|
||||
)]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_protocols_route() -> ConduitResult<get_protocols::Response> {
|
||||
// TODO
|
||||
Ok(get_protocols::Response {
|
||||
protocols: BTreeMap::new(),
|
||||
}
|
||||
.into())
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, utils, ConduitResult, Ruma};
|
||||
use create_typing_event::Typing;
|
||||
use ruma::api::client::r0::typing::create_typing_event;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::put;
|
||||
|
||||
/// # `PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId}`
|
||||
///
|
||||
/// Sets the typing state of the sender user.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
put("/_matrix/client/r0/rooms/<_>/typing/<_>", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub fn create_typing_event_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<create_typing_event::Request<'_>>,
|
||||
) -> ConduitResult<create_typing_event::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
if let Typing::Yes(duration) = body.state {
|
||||
db.rooms.edus.typing_add(
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
duration.as_millis() as u64 + utils::millis_since_unix_epoch(),
|
||||
&db.globals,
|
||||
)?;
|
||||
} else {
|
||||
db.rooms
|
||||
.edus
|
||||
.typing_remove(sender_user, &body.room_id, &db.globals)?;
|
||||
}
|
||||
|
||||
Ok(create_typing_event::Response {}.into())
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
use std::{collections::BTreeMap, iter::FromIterator};
|
||||
|
||||
use crate::ConduitResult;
|
||||
use ruma::api::client::unversioned::get_supported_versions;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::get;
|
||||
|
||||
/// # `GET /_matrix/client/versions`
|
||||
///
|
||||
/// Get the versions of the specification and unstable features supported by this server.
|
||||
///
|
||||
/// - Versions take the form MAJOR.MINOR.PATCH
|
||||
/// - Only the latest PATCH release will be reported for each MAJOR.MINOR value
|
||||
/// - Unstable features are namespaced and may include version information in their name
|
||||
///
|
||||
/// Note: Unstable features are used while developing new features. Clients should avoid using
|
||||
/// unstable features in their stable releases
|
||||
#[cfg_attr(feature = "conduit_bin", get("/_matrix/client/versions"))]
|
||||
#[tracing::instrument]
|
||||
pub async fn get_supported_versions_route() -> ConduitResult<get_supported_versions::Response> {
|
||||
let resp = get_supported_versions::Response {
|
||||
versions: vec!["r0.5.0".to_owned(), "r0.6.0".to_owned()],
|
||||
unstable_features: BTreeMap::from_iter([("org.matrix.e2e_cross_signing".to_owned(), true)]),
|
||||
};
|
||||
|
||||
Ok(resp.into())
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Ruma};
|
||||
use ruma::api::client::r0::user_directory::search_users;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::post;
|
||||
|
||||
/// # `POST /_matrix/client/r0/user_directory/search`
|
||||
///
|
||||
/// Searches all known users for a match.
|
||||
///
|
||||
/// - TODO: Hide users that are not in any public rooms?
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
post("/_matrix/client/r0/user_directory/search", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(db, body))]
|
||||
pub async fn search_users_route(
|
||||
db: DatabaseGuard,
|
||||
body: Ruma<search_users::Request<'_>>,
|
||||
) -> ConduitResult<search_users::Response> {
|
||||
let limit = u64::from(body.limit) as usize;
|
||||
|
||||
let mut users = db.users.iter().filter_map(|user_id| {
|
||||
// Filter out buggy users (they should not exist, but you never know...)
|
||||
let user_id = user_id.ok()?;
|
||||
|
||||
let user = search_users::User {
|
||||
user_id: user_id.clone(),
|
||||
display_name: db.users.displayname(&user_id).ok()?,
|
||||
avatar_url: db.users.avatar_url(&user_id).ok()?,
|
||||
};
|
||||
|
||||
let user_id_matches = user
|
||||
.user_id
|
||||
.to_string()
|
||||
.to_lowercase()
|
||||
.contains(&body.search_term.to_lowercase());
|
||||
|
||||
let user_displayname_matches = user
|
||||
.display_name
|
||||
.as_ref()
|
||||
.filter(|name| {
|
||||
name.to_lowercase()
|
||||
.contains(&body.search_term.to_lowercase())
|
||||
})
|
||||
.is_some();
|
||||
|
||||
if !user_id_matches && !user_displayname_matches {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(user)
|
||||
});
|
||||
|
||||
let results = users.by_ref().take(limit).collect();
|
||||
let limited = users.next().is_some();
|
||||
|
||||
Ok(search_users::Response { results, limited }.into())
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
use crate::{database::DatabaseGuard, ConduitResult, Ruma};
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use ruma::api::client::r0::voip::get_turn_server_info;
|
||||
use ruma::SecondsSinceUnixEpoch;
|
||||
use sha1::Sha1;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
use rocket::get;
|
||||
|
||||
/// # `GET /_matrix/client/r0/voip/turnServer`
|
||||
///
|
||||
/// TODO: Returns information about the recommended turn server.
|
||||
#[cfg_attr(
|
||||
feature = "conduit_bin",
|
||||
get("/_matrix/client/r0/voip/turnServer", data = "<body>")
|
||||
)]
|
||||
#[tracing::instrument(skip(body, db))]
|
||||
pub async fn turn_server_route(
|
||||
body: Ruma<get_turn_server_info::Request>,
|
||||
db: DatabaseGuard,
|
||||
) -> ConduitResult<get_turn_server_info::Response> {
|
||||
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
||||
|
||||
let turn_secret = db.globals.turn_secret();
|
||||
|
||||
let (username, password) = if !turn_secret.is_empty() {
|
||||
let expiry = SecondsSinceUnixEpoch::from_system_time(
|
||||
SystemTime::now() + Duration::from_secs(db.globals.turn_ttl()),
|
||||
)
|
||||
.expect("time is valid");
|
||||
|
||||
let username: String = format!("{}:{}", expiry.get(), sender_user);
|
||||
|
||||
let mut mac = HmacSha1::new_from_slice(turn_secret.as_bytes())
|
||||
.expect("HMAC can take key of any size");
|
||||
mac.update(username.as_bytes());
|
||||
|
||||
let password: String = base64::encode_config(mac.finalize().into_bytes(), base64::STANDARD);
|
||||
|
||||
(username, password)
|
||||
} else {
|
||||
(
|
||||
db.globals.turn_username().clone(),
|
||||
db.globals.turn_password().clone(),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(get_turn_server_info::Response {
|
||||
username,
|
||||
password,
|
||||
uris: db.globals.turn_uris().to_vec(),
|
||||
ttl: Duration::from_secs(db.globals.turn_ttl()),
|
||||
}
|
||||
.into())
|
||||
}
|
131
src/config.rs
131
src/config.rs
|
@ -1,131 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use ruma::ServerName;
|
||||
use serde::{de::IgnoredAny, Deserialize};
|
||||
use tracing::warn;
|
||||
|
||||
mod proxy;
|
||||
|
||||
use self::proxy::ProxyConfig;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub server_name: Box<ServerName>,
|
||||
#[serde(default = "default_database_backend")]
|
||||
pub database_backend: String,
|
||||
pub database_path: String,
|
||||
#[serde(default = "default_db_cache_capacity_mb")]
|
||||
pub db_cache_capacity_mb: f64,
|
||||
#[serde(default = "default_conduit_cache_capacity_modifier")]
|
||||
pub conduit_cache_capacity_modifier: f64,
|
||||
#[serde(default = "default_rocksdb_max_open_files")]
|
||||
pub rocksdb_max_open_files: i32,
|
||||
#[serde(default = "default_pdu_cache_capacity")]
|
||||
pub pdu_cache_capacity: u32,
|
||||
#[serde(default = "default_cleanup_second_interval")]
|
||||
pub cleanup_second_interval: u32,
|
||||
#[serde(default = "default_max_request_size")]
|
||||
pub max_request_size: u32,
|
||||
#[serde(default = "default_max_concurrent_requests")]
|
||||
pub max_concurrent_requests: u16,
|
||||
#[serde(default = "false_fn")]
|
||||
pub allow_registration: bool,
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_encryption: bool,
|
||||
#[serde(default = "false_fn")]
|
||||
pub allow_federation: bool,
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_room_creation: bool,
|
||||
#[serde(default = "false_fn")]
|
||||
pub allow_jaeger: bool,
|
||||
#[serde(default = "false_fn")]
|
||||
pub tracing_flame: bool,
|
||||
#[serde(default)]
|
||||
pub proxy: ProxyConfig,
|
||||
pub jwt_secret: Option<String>,
|
||||
#[serde(default = "Vec::new")]
|
||||
pub trusted_servers: Vec<Box<ServerName>>,
|
||||
#[serde(default = "default_log")]
|
||||
pub log: String,
|
||||
#[serde(default)]
|
||||
pub turn_username: String,
|
||||
#[serde(default)]
|
||||
pub turn_password: String,
|
||||
#[serde(default = "Vec::new")]
|
||||
pub turn_uris: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub turn_secret: String,
|
||||
#[serde(default = "default_turn_ttl")]
|
||||
pub turn_ttl: u64,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub catchall: BTreeMap<String, IgnoredAny>,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &["cache_capacity"];
|
||||
|
||||
impl Config {
|
||||
pub fn warn_deprecated(&self) {
|
||||
let mut was_deprecated = false;
|
||||
for key in self
|
||||
.catchall
|
||||
.keys()
|
||||
.filter(|key| DEPRECATED_KEYS.iter().any(|s| s == key))
|
||||
{
|
||||
warn!("Config parameter {} is deprecated", key);
|
||||
was_deprecated = true;
|
||||
}
|
||||
|
||||
if was_deprecated {
|
||||
warn!("Read conduit documentation and check your configuration if any new configuration parameters should be adjusted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn false_fn() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn true_fn() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_database_backend() -> String {
|
||||
"sqlite".to_owned()
|
||||
}
|
||||
|
||||
fn default_db_cache_capacity_mb() -> f64 {
|
||||
10.0
|
||||
}
|
||||
|
||||
fn default_conduit_cache_capacity_modifier() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
fn default_rocksdb_max_open_files() -> i32 {
|
||||
20
|
||||
}
|
||||
|
||||
fn default_pdu_cache_capacity() -> u32 {
|
||||
150_000
|
||||
}
|
||||
|
||||
fn default_cleanup_second_interval() -> u32 {
|
||||
1 * 60 // every minute
|
||||
}
|
||||
|
||||
fn default_max_request_size() -> u32 {
|
||||
20 * 1024 * 1024 // Default to 20 MB
|
||||
}
|
||||
|
||||
fn default_max_concurrent_requests() -> u16 {
|
||||
100
|
||||
}
|
||||
|
||||
fn default_log() -> String {
|
||||
"info,state_res=warn,rocket=off,_=off,sled=off".to_owned()
|
||||
}
|
||||
|
||||
fn default_turn_ttl() -> u64 {
|
||||
60 * 60 * 24
|
||||
}
|
277
src/config/mod.rs
Normal file
277
src/config/mod.rs
Normal file
|
@ -0,0 +1,277 @@
|
|||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
};
|
||||
|
||||
use ruma::{OwnedServerName, RoomVersionId};
|
||||
use serde::{de::IgnoredAny, Deserialize};
|
||||
use tracing::warn;
|
||||
|
||||
mod proxy;
|
||||
|
||||
use self::proxy::ProxyConfig;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_address")]
|
||||
pub address: IpAddr,
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
pub tls: Option<TlsConfig>,
|
||||
|
||||
pub server_name: OwnedServerName,
|
||||
#[serde(default = "default_database_backend")]
|
||||
pub database_backend: String,
|
||||
pub database_path: String,
|
||||
#[serde(default = "default_db_cache_capacity_mb")]
|
||||
pub db_cache_capacity_mb: f64,
|
||||
#[serde(default = "true_fn")]
|
||||
pub enable_lightning_bolt: bool,
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_check_for_updates: bool,
|
||||
#[serde(default = "default_conduit_cache_capacity_modifier")]
|
||||
pub conduit_cache_capacity_modifier: f64,
|
||||
#[serde(default = "default_rocksdb_max_open_files")]
|
||||
pub rocksdb_max_open_files: i32,
|
||||
#[serde(default = "default_pdu_cache_capacity")]
|
||||
pub pdu_cache_capacity: u32,
|
||||
#[serde(default = "default_cleanup_second_interval")]
|
||||
pub cleanup_second_interval: u32,
|
||||
#[serde(default = "default_max_request_size")]
|
||||
pub max_request_size: u32,
|
||||
#[serde(default = "default_max_concurrent_requests")]
|
||||
pub max_concurrent_requests: u16,
|
||||
#[serde(default = "default_max_fetch_prev_events")]
|
||||
pub max_fetch_prev_events: u16,
|
||||
#[serde(default = "false_fn")]
|
||||
pub allow_registration: bool,
|
||||
pub registration_token: Option<String>,
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_encryption: bool,
|
||||
#[serde(default = "false_fn")]
|
||||
pub allow_federation: bool,
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_room_creation: bool,
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_unstable_room_versions: bool,
|
||||
#[serde(default = "default_default_room_version")]
|
||||
pub default_room_version: RoomVersionId,
|
||||
pub well_known_client: Option<String>,
|
||||
#[serde(default = "false_fn")]
|
||||
pub allow_jaeger: bool,
|
||||
#[serde(default = "false_fn")]
|
||||
pub tracing_flame: bool,
|
||||
#[serde(default)]
|
||||
pub proxy: ProxyConfig,
|
||||
pub jwt_secret: Option<String>,
|
||||
#[serde(default = "default_trusted_servers")]
|
||||
pub trusted_servers: Vec<OwnedServerName>,
|
||||
#[serde(default = "default_log")]
|
||||
pub log: String,
|
||||
#[serde(default)]
|
||||
pub turn_username: String,
|
||||
#[serde(default)]
|
||||
pub turn_password: String,
|
||||
#[serde(default = "Vec::new")]
|
||||
pub turn_uris: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub turn_secret: String,
|
||||
#[serde(default = "default_turn_ttl")]
|
||||
pub turn_ttl: u64,
|
||||
|
||||
pub emergency_password: Option<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub catchall: BTreeMap<String, IgnoredAny>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TlsConfig {
|
||||
pub certs: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &["cache_capacity"];
|
||||
|
||||
impl Config {
|
||||
pub fn warn_deprecated(&self) {
|
||||
let mut was_deprecated = false;
|
||||
for key in self
|
||||
.catchall
|
||||
.keys()
|
||||
.filter(|key| DEPRECATED_KEYS.iter().any(|s| s == key))
|
||||
{
|
||||
warn!("Config parameter {} is deprecated", key);
|
||||
was_deprecated = true;
|
||||
}
|
||||
|
||||
if was_deprecated {
|
||||
warn!("Read conduit documentation and check your configuration if any new configuration parameters should be adjusted");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Config {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Prepare a list of config values to show
|
||||
let lines = [
|
||||
("Server name", self.server_name.host()),
|
||||
("Database backend", &self.database_backend),
|
||||
("Database path", &self.database_path),
|
||||
(
|
||||
"Database cache capacity (MB)",
|
||||
&self.db_cache_capacity_mb.to_string(),
|
||||
),
|
||||
(
|
||||
"Cache capacity modifier",
|
||||
&self.conduit_cache_capacity_modifier.to_string(),
|
||||
),
|
||||
#[cfg(feature = "rocksdb")]
|
||||
(
|
||||
"Maximum open files for RocksDB",
|
||||
&self.rocksdb_max_open_files.to_string(),
|
||||
),
|
||||
("PDU cache capacity", &self.pdu_cache_capacity.to_string()),
|
||||
(
|
||||
"Cleanup interval in seconds",
|
||||
&self.cleanup_second_interval.to_string(),
|
||||
),
|
||||
("Maximum request size", &self.max_request_size.to_string()),
|
||||
(
|
||||
"Maximum concurrent requests",
|
||||
&self.max_concurrent_requests.to_string(),
|
||||
),
|
||||
("Allow registration", &self.allow_registration.to_string()),
|
||||
(
|
||||
"Enabled lightning bolt",
|
||||
&self.enable_lightning_bolt.to_string(),
|
||||
),
|
||||
("Allow encryption", &self.allow_encryption.to_string()),
|
||||
("Allow federation", &self.allow_federation.to_string()),
|
||||
("Allow room creation", &self.allow_room_creation.to_string()),
|
||||
(
|
||||
"JWT secret",
|
||||
match self.jwt_secret {
|
||||
Some(_) => "set",
|
||||
None => "not set",
|
||||
},
|
||||
),
|
||||
("Trusted servers", {
|
||||
let mut lst = vec![];
|
||||
for server in &self.trusted_servers {
|
||||
lst.push(server.host());
|
||||
}
|
||||
&lst.join(", ")
|
||||
}),
|
||||
(
|
||||
"TURN username",
|
||||
if self.turn_username.is_empty() {
|
||||
"not set"
|
||||
} else {
|
||||
&self.turn_username
|
||||
},
|
||||
),
|
||||
("TURN password", {
|
||||
if self.turn_password.is_empty() {
|
||||
"not set"
|
||||
} else {
|
||||
"set"
|
||||
}
|
||||
}),
|
||||
("TURN secret", {
|
||||
if self.turn_secret.is_empty() {
|
||||
"not set"
|
||||
} else {
|
||||
"set"
|
||||
}
|
||||
}),
|
||||
("Turn TTL", &self.turn_ttl.to_string()),
|
||||
("Turn URIs", {
|
||||
let mut lst = vec![];
|
||||
for item in self.turn_uris.iter().cloned().enumerate() {
|
||||
let (_, uri): (usize, String) = item;
|
||||
lst.push(uri);
|
||||
}
|
||||
&lst.join(", ")
|
||||
}),
|
||||
];
|
||||
|
||||
let mut msg: String = "Active config values:\n\n".to_owned();
|
||||
|
||||
for line in lines.into_iter().enumerate() {
|
||||
msg += &format!("{}: {}\n", line.1 .0, line.1 .1);
|
||||
}
|
||||
|
||||
write!(f, "{msg}")
|
||||
}
|
||||
}
|
||||
|
||||
fn false_fn() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn true_fn() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_address() -> IpAddr {
|
||||
Ipv4Addr::LOCALHOST.into()
|
||||
}
|
||||
|
||||
fn default_port() -> u16 {
|
||||
8000
|
||||
}
|
||||
|
||||
fn default_database_backend() -> String {
|
||||
"sqlite".to_owned()
|
||||
}
|
||||
|
||||
fn default_db_cache_capacity_mb() -> f64 {
|
||||
300.0
|
||||
}
|
||||
|
||||
fn default_conduit_cache_capacity_modifier() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
fn default_rocksdb_max_open_files() -> i32 {
|
||||
1000
|
||||
}
|
||||
|
||||
fn default_pdu_cache_capacity() -> u32 {
|
||||
150_000
|
||||
}
|
||||
|
||||
fn default_cleanup_second_interval() -> u32 {
|
||||
60 // every minute
|
||||
}
|
||||
|
||||
fn default_max_request_size() -> u32 {
|
||||
20 * 1024 * 1024 // Default to 20 MB
|
||||
}
|
||||
|
||||
fn default_max_concurrent_requests() -> u16 {
|
||||
100
|
||||
}
|
||||
|
||||
fn default_max_fetch_prev_events() -> u16 {
|
||||
100_u16
|
||||
}
|
||||
|
||||
fn default_trusted_servers() -> Vec<OwnedServerName> {
|
||||
vec![OwnedServerName::try_from("matrix.org").unwrap()]
|
||||
}
|
||||
|
||||
fn default_log() -> String {
|
||||
"warn,state_res=warn,_=off,sled=off".to_owned()
|
||||
}
|
||||
|
||||
fn default_turn_ttl() -> u64 {
|
||||
60 * 60 * 24
|
||||
}
|
||||
|
||||
// I know, it's a great name
|
||||
pub fn default_default_room_version() -> RoomVersionId {
|
||||
RoomVersionId::V9
|
||||
}
|
|
@ -10,13 +10,13 @@ use crate::Result;
|
|||
/// ```
|
||||
/// - Global proxy
|
||||
/// ```toml
|
||||
/// [proxy]
|
||||
/// [global.proxy]
|
||||
/// global = { url = "socks5h://localhost:9050" }
|
||||
/// ```
|
||||
/// - Proxy some domains
|
||||
/// ```toml
|
||||
/// [proxy]
|
||||
/// [[proxy.by_domain]]
|
||||
/// [global.proxy]
|
||||
/// [[global.proxy.by_domain]]
|
||||
/// url = "socks5h://localhost:9050"
|
||||
/// include = ["*.onion", "matrix.myspecial.onion"]
|
||||
/// exclude = ["*.myspecial.onion"]
|
||||
|
@ -29,7 +29,9 @@ use crate::Result;
|
|||
/// would be used for `ordinary.onion`, `matrix.myspecial.onion`, but not `hello.myspecial.onion`.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[derive(Default)]
|
||||
pub enum ProxyConfig {
|
||||
#[default]
|
||||
None,
|
||||
Global {
|
||||
#[serde(deserialize_with = "crate::utils::deserialize_from_str")]
|
||||
|
@ -48,11 +50,6 @@ impl ProxyConfig {
|
|||
})
|
||||
}
|
||||
}
|
||||
impl Default for ProxyConfig {
|
||||
fn default() -> Self {
|
||||
ProxyConfig::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct PartialProxyConfig {
|
||||
|
|
966
src/database.rs
966
src/database.rs
|
@ -1,966 +0,0 @@
|
|||
pub mod abstraction;
|
||||
|
||||
pub mod account_data;
|
||||
pub mod admin;
|
||||
pub mod appservice;
|
||||
pub mod globals;
|
||||
pub mod key_backups;
|
||||
pub mod media;
|
||||
pub mod pusher;
|
||||
pub mod rooms;
|
||||
pub mod sending;
|
||||
pub mod transaction_ids;
|
||||
pub mod uiaa;
|
||||
pub mod users;
|
||||
|
||||
use crate::{utils, Config, Error, Result};
|
||||
use abstraction::DatabaseEngine;
|
||||
use directories::ProjectDirs;
|
||||
use lru_cache::LruCache;
|
||||
use rocket::{
|
||||
futures::{channel::mpsc, stream::FuturesUnordered, StreamExt},
|
||||
outcome::{try_outcome, IntoOutcome},
|
||||
request::{FromRequest, Request},
|
||||
Shutdown, State,
|
||||
};
|
||||
use ruma::{DeviceId, EventId, RoomId, UserId};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
fs::{self, remove_dir_all},
|
||||
io::Write,
|
||||
mem::size_of,
|
||||
ops::Deref,
|
||||
path::Path,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
use tokio::sync::{OwnedRwLockReadGuard, RwLock as TokioRwLock, Semaphore};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use self::admin::create_admin_room;
|
||||
|
||||
pub struct Database {
|
||||
_db: Arc<dyn DatabaseEngine>,
|
||||
pub globals: globals::Globals,
|
||||
pub users: users::Users,
|
||||
pub uiaa: uiaa::Uiaa,
|
||||
pub rooms: rooms::Rooms,
|
||||
pub account_data: account_data::AccountData,
|
||||
pub media: media::Media,
|
||||
pub key_backups: key_backups::KeyBackups,
|
||||
pub transaction_ids: transaction_ids::TransactionIds,
|
||||
pub sending: sending::Sending,
|
||||
pub admin: admin::Admin,
|
||||
pub appservice: appservice::Appservice,
|
||||
pub pusher: pusher::PushData,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Tries to remove the old database but ignores all errors.
|
||||
pub fn try_remove(server_name: &str) -> Result<()> {
|
||||
let mut path = ProjectDirs::from("xyz", "koesters", "conduit")
|
||||
.ok_or_else(|| Error::bad_config("The OS didn't return a valid home directory path."))?
|
||||
.data_dir()
|
||||
.to_path_buf();
|
||||
path.push(server_name);
|
||||
let _ = remove_dir_all(path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_db_setup(config: &Config) -> Result<()> {
|
||||
let path = Path::new(&config.database_path);
|
||||
|
||||
let sled_exists = path.join("db").exists();
|
||||
let sqlite_exists = path.join("conduit.db").exists();
|
||||
let rocksdb_exists = path.join("IDENTITY").exists();
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
if sled_exists {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if sqlite_exists {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if rocksdb_exists {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if count > 1 {
|
||||
warn!("Multiple databases at database_path detected");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if sled_exists && config.database_backend != "sled" {
|
||||
return Err(Error::bad_config(
|
||||
"Found sled at database_path, but is not specified in config.",
|
||||
));
|
||||
}
|
||||
|
||||
if sqlite_exists && config.database_backend != "sqlite" {
|
||||
return Err(Error::bad_config(
|
||||
"Found sqlite at database_path, but is not specified in config.",
|
||||
));
|
||||
}
|
||||
|
||||
if rocksdb_exists && config.database_backend != "rocksdb" {
|
||||
return Err(Error::bad_config(
|
||||
"Found rocksdb at database_path, but is not specified in config.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load an existing database or create a new one.
|
||||
pub async fn load_or_create(config: &Config) -> Result<Arc<TokioRwLock<Self>>> {
|
||||
Self::check_db_setup(config)?;
|
||||
|
||||
if !Path::new(&config.database_path).exists() {
|
||||
std::fs::create_dir_all(&config.database_path)
|
||||
.map_err(|_| Error::BadConfig("Database folder doesn't exists and couldn't be created (e.g. due to missing permissions). Please create the database folder yourself."))?;
|
||||
}
|
||||
|
||||
let builder: Arc<dyn DatabaseEngine> = match &*config.database_backend {
|
||||
"sqlite" => {
|
||||
#[cfg(not(feature = "sqlite"))]
|
||||
return Err(Error::BadConfig("Database backend not found."));
|
||||
#[cfg(feature = "sqlite")]
|
||||
Arc::new(Arc::<abstraction::sqlite::Engine>::open(config)?)
|
||||
}
|
||||
"rocksdb" => {
|
||||
#[cfg(not(feature = "rocksdb"))]
|
||||
return Err(Error::BadConfig("Database backend not found."));
|
||||
#[cfg(feature = "rocksdb")]
|
||||
Arc::new(Arc::<abstraction::rocksdb::Engine>::open(config)?)
|
||||
}
|
||||
"persy" => {
|
||||
#[cfg(not(feature = "persy"))]
|
||||
return Err(Error::BadConfig("Database backend not found."));
|
||||
#[cfg(feature = "persy")]
|
||||
Arc::new(Arc::<abstraction::persy::Engine>::open(config)?)
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::BadConfig("Database backend not found."));
|
||||
}
|
||||
};
|
||||
|
||||
if config.max_request_size < 1024 {
|
||||
eprintln!("ERROR: Max request size is less than 1KB. Please increase it.");
|
||||
}
|
||||
|
||||
let (admin_sender, admin_receiver) = mpsc::unbounded();
|
||||
let (sending_sender, sending_receiver) = mpsc::unbounded();
|
||||
|
||||
let db = Arc::new(TokioRwLock::from(Self {
|
||||
_db: builder.clone(),
|
||||
users: users::Users {
|
||||
userid_password: builder.open_tree("userid_password")?,
|
||||
userid_displayname: builder.open_tree("userid_displayname")?,
|
||||
userid_avatarurl: builder.open_tree("userid_avatarurl")?,
|
||||
userid_blurhash: builder.open_tree("userid_blurhash")?,
|
||||
userdeviceid_token: builder.open_tree("userdeviceid_token")?,
|
||||
userdeviceid_metadata: builder.open_tree("userdeviceid_metadata")?,
|
||||
userid_devicelistversion: builder.open_tree("userid_devicelistversion")?,
|
||||
token_userdeviceid: builder.open_tree("token_userdeviceid")?,
|
||||
onetimekeyid_onetimekeys: builder.open_tree("onetimekeyid_onetimekeys")?,
|
||||
userid_lastonetimekeyupdate: builder.open_tree("userid_lastonetimekeyupdate")?,
|
||||
keychangeid_userid: builder.open_tree("keychangeid_userid")?,
|
||||
keyid_key: builder.open_tree("keyid_key")?,
|
||||
userid_masterkeyid: builder.open_tree("userid_masterkeyid")?,
|
||||
userid_selfsigningkeyid: builder.open_tree("userid_selfsigningkeyid")?,
|
||||
userid_usersigningkeyid: builder.open_tree("userid_usersigningkeyid")?,
|
||||
userfilterid_filter: builder.open_tree("userfilterid_filter")?,
|
||||
todeviceid_events: builder.open_tree("todeviceid_events")?,
|
||||
},
|
||||
uiaa: uiaa::Uiaa {
|
||||
userdevicesessionid_uiaainfo: builder.open_tree("userdevicesessionid_uiaainfo")?,
|
||||
userdevicesessionid_uiaarequest: RwLock::new(BTreeMap::new()),
|
||||
},
|
||||
rooms: rooms::Rooms {
|
||||
edus: rooms::RoomEdus {
|
||||
readreceiptid_readreceipt: builder.open_tree("readreceiptid_readreceipt")?,
|
||||
roomuserid_privateread: builder.open_tree("roomuserid_privateread")?, // "Private" read receipt
|
||||
roomuserid_lastprivatereadupdate: builder
|
||||
.open_tree("roomuserid_lastprivatereadupdate")?,
|
||||
typingid_userid: builder.open_tree("typingid_userid")?,
|
||||
roomid_lasttypingupdate: builder.open_tree("roomid_lasttypingupdate")?,
|
||||
presenceid_presence: builder.open_tree("presenceid_presence")?,
|
||||
userid_lastpresenceupdate: builder.open_tree("userid_lastpresenceupdate")?,
|
||||
},
|
||||
pduid_pdu: builder.open_tree("pduid_pdu")?,
|
||||
eventid_pduid: builder.open_tree("eventid_pduid")?,
|
||||
roomid_pduleaves: builder.open_tree("roomid_pduleaves")?,
|
||||
|
||||
alias_roomid: builder.open_tree("alias_roomid")?,
|
||||
aliasid_alias: builder.open_tree("aliasid_alias")?,
|
||||
publicroomids: builder.open_tree("publicroomids")?,
|
||||
|
||||
tokenids: builder.open_tree("tokenids")?,
|
||||
|
||||
roomserverids: builder.open_tree("roomserverids")?,
|
||||
serverroomids: builder.open_tree("serverroomids")?,
|
||||
userroomid_joined: builder.open_tree("userroomid_joined")?,
|
||||
roomuserid_joined: builder.open_tree("roomuserid_joined")?,
|
||||
roomid_joinedcount: builder.open_tree("roomid_joinedcount")?,
|
||||
roomid_invitedcount: builder.open_tree("roomid_invitedcount")?,
|
||||
roomuseroncejoinedids: builder.open_tree("roomuseroncejoinedids")?,
|
||||
userroomid_invitestate: builder.open_tree("userroomid_invitestate")?,
|
||||
roomuserid_invitecount: builder.open_tree("roomuserid_invitecount")?,
|
||||
userroomid_leftstate: builder.open_tree("userroomid_leftstate")?,
|
||||
roomuserid_leftcount: builder.open_tree("roomuserid_leftcount")?,
|
||||
|
||||
lazyloadedids: builder.open_tree("lazyloadedids")?,
|
||||
|
||||
userroomid_notificationcount: builder.open_tree("userroomid_notificationcount")?,
|
||||
userroomid_highlightcount: builder.open_tree("userroomid_highlightcount")?,
|
||||
|
||||
statekey_shortstatekey: builder.open_tree("statekey_shortstatekey")?,
|
||||
shortstatekey_statekey: builder.open_tree("shortstatekey_statekey")?,
|
||||
|
||||
shorteventid_authchain: builder.open_tree("shorteventid_authchain")?,
|
||||
|
||||
roomid_shortroomid: builder.open_tree("roomid_shortroomid")?,
|
||||
|
||||
shortstatehash_statediff: builder.open_tree("shortstatehash_statediff")?,
|
||||
eventid_shorteventid: builder.open_tree("eventid_shorteventid")?,
|
||||
shorteventid_eventid: builder.open_tree("shorteventid_eventid")?,
|
||||
shorteventid_shortstatehash: builder.open_tree("shorteventid_shortstatehash")?,
|
||||
roomid_shortstatehash: builder.open_tree("roomid_shortstatehash")?,
|
||||
roomsynctoken_shortstatehash: builder.open_tree("roomsynctoken_shortstatehash")?,
|
||||
statehash_shortstatehash: builder.open_tree("statehash_shortstatehash")?,
|
||||
|
||||
eventid_outlierpdu: builder.open_tree("eventid_outlierpdu")?,
|
||||
softfailedeventids: builder.open_tree("softfailedeventids")?,
|
||||
|
||||
referencedevents: builder.open_tree("referencedevents")?,
|
||||
pdu_cache: Mutex::new(LruCache::new(
|
||||
config
|
||||
.pdu_cache_capacity
|
||||
.try_into()
|
||||
.expect("pdu cache capacity fits into usize"),
|
||||
)),
|
||||
auth_chain_cache: Mutex::new(LruCache::new(
|
||||
(100_000.0 * config.conduit_cache_capacity_modifier) as usize,
|
||||
)),
|
||||
shorteventid_cache: Mutex::new(LruCache::new(
|
||||
(100_000.0 * config.conduit_cache_capacity_modifier) as usize,
|
||||
)),
|
||||
eventidshort_cache: Mutex::new(LruCache::new(
|
||||
(100_000.0 * config.conduit_cache_capacity_modifier) as usize,
|
||||
)),
|
||||
shortstatekey_cache: Mutex::new(LruCache::new(
|
||||
(100_000.0 * config.conduit_cache_capacity_modifier) as usize,
|
||||
)),
|
||||
statekeyshort_cache: Mutex::new(LruCache::new(
|
||||
(100_000.0 * config.conduit_cache_capacity_modifier) as usize,
|
||||
)),
|
||||
our_real_users_cache: RwLock::new(HashMap::new()),
|
||||
appservice_in_room_cache: RwLock::new(HashMap::new()),
|
||||
lazy_load_waiting: Mutex::new(HashMap::new()),
|
||||
stateinfo_cache: Mutex::new(LruCache::new(
|
||||
(100.0 * config.conduit_cache_capacity_modifier) as usize,
|
||||
)),
|
||||
},
|
||||
account_data: account_data::AccountData {
|
||||
roomuserdataid_accountdata: builder.open_tree("roomuserdataid_accountdata")?,
|
||||
roomusertype_roomuserdataid: builder.open_tree("roomusertype_roomuserdataid")?,
|
||||
},
|
||||
media: media::Media {
|
||||
mediaid_file: builder.open_tree("mediaid_file")?,
|
||||
},
|
||||
key_backups: key_backups::KeyBackups {
|
||||
backupid_algorithm: builder.open_tree("backupid_algorithm")?,
|
||||
backupid_etag: builder.open_tree("backupid_etag")?,
|
||||
backupkeyid_backup: builder.open_tree("backupkeyid_backup")?,
|
||||
},
|
||||
transaction_ids: transaction_ids::TransactionIds {
|
||||
userdevicetxnid_response: builder.open_tree("userdevicetxnid_response")?,
|
||||
},
|
||||
sending: sending::Sending {
|
||||
servername_educount: builder.open_tree("servername_educount")?,
|
||||
servernameevent_data: builder.open_tree("servernameevent_data")?,
|
||||
servercurrentevent_data: builder.open_tree("servercurrentevent_data")?,
|
||||
maximum_requests: Arc::new(Semaphore::new(config.max_concurrent_requests as usize)),
|
||||
sender: sending_sender,
|
||||
},
|
||||
admin: admin::Admin {
|
||||
sender: admin_sender,
|
||||
},
|
||||
appservice: appservice::Appservice {
|
||||
cached_registrations: Arc::new(RwLock::new(HashMap::new())),
|
||||
id_appserviceregistrations: builder.open_tree("id_appserviceregistrations")?,
|
||||
},
|
||||
pusher: pusher::PushData {
|
||||
senderkey_pusher: builder.open_tree("senderkey_pusher")?,
|
||||
},
|
||||
globals: globals::Globals::load(
|
||||
builder.open_tree("global")?,
|
||||
builder.open_tree("server_signingkeys")?,
|
||||
config.clone(),
|
||||
)?,
|
||||
}));
|
||||
|
||||
let guard = db.read().await;
|
||||
|
||||
// Matrix resource ownership is based on the server name; changing it
|
||||
// requires recreating the database from scratch.
|
||||
if guard.users.count()? > 0 {
|
||||
let conduit_user =
|
||||
UserId::parse_with_server_name("conduit", guard.globals.server_name())
|
||||
.expect("@conduit:server_name is valid");
|
||||
|
||||
if !guard.users.exists(&conduit_user)? {
|
||||
error!(
|
||||
"The {} server user does not exist, and the database is not new.",
|
||||
conduit_user
|
||||
);
|
||||
return Err(Error::bad_database(
|
||||
"Cannot reuse an existing database after changing the server name, please delete the old one first."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// If the database has any data, perform data migrations before starting
|
||||
let latest_database_version = 11;
|
||||
|
||||
if guard.users.count()? > 0 {
|
||||
let db = &*guard;
|
||||
// MIGRATIONS
|
||||
if db.globals.database_version()? < 1 {
|
||||
for (roomserverid, _) in db.rooms.roomserverids.iter() {
|
||||
let mut parts = roomserverid.split(|&b| b == 0xff);
|
||||
let room_id = parts.next().expect("split always returns one element");
|
||||
let servername = match parts.next() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
error!("Migration: Invalid roomserverid in db.");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let mut serverroomid = servername.to_vec();
|
||||
serverroomid.push(0xff);
|
||||
serverroomid.extend_from_slice(room_id);
|
||||
|
||||
db.rooms.serverroomids.insert(&serverroomid, &[])?;
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(1)?;
|
||||
|
||||
warn!("Migration: 0 -> 1 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 2 {
|
||||
// We accidentally inserted hashed versions of "" into the db instead of just ""
|
||||
for (userid, password) in db.users.userid_password.iter() {
|
||||
let password = utils::string_from_bytes(&password);
|
||||
|
||||
let empty_hashed_password = password.map_or(false, |password| {
|
||||
argon2::verify_encoded(&password, b"").unwrap_or(false)
|
||||
});
|
||||
|
||||
if empty_hashed_password {
|
||||
db.users.userid_password.insert(&userid, b"")?;
|
||||
}
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(2)?;
|
||||
|
||||
warn!("Migration: 1 -> 2 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 3 {
|
||||
// Move media to filesystem
|
||||
for (key, content) in db.media.mediaid_file.iter() {
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = db.globals.get_media_file(&key);
|
||||
let mut file = fs::File::create(path)?;
|
||||
file.write_all(&content)?;
|
||||
db.media.mediaid_file.insert(&key, &[])?;
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(3)?;
|
||||
|
||||
warn!("Migration: 2 -> 3 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 4 {
|
||||
// Add federated users to db as deactivated
|
||||
for our_user in db.users.iter() {
|
||||
let our_user = our_user?;
|
||||
if db.users.is_deactivated(&our_user)? {
|
||||
continue;
|
||||
}
|
||||
for room in db.rooms.rooms_joined(&our_user) {
|
||||
for user in db.rooms.room_members(&room?) {
|
||||
let user = user?;
|
||||
if user.server_name() != db.globals.server_name() {
|
||||
println!("Migration: Creating user {}", user);
|
||||
db.users.create(&user, None)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(4)?;
|
||||
|
||||
warn!("Migration: 3 -> 4 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 5 {
|
||||
// Upgrade user data store
|
||||
for (roomuserdataid, _) in db.account_data.roomuserdataid_accountdata.iter() {
|
||||
let mut parts = roomuserdataid.split(|&b| b == 0xff);
|
||||
let room_id = parts.next().unwrap();
|
||||
let user_id = parts.next().unwrap();
|
||||
let event_type = roomuserdataid.rsplit(|&b| b == 0xff).next().unwrap();
|
||||
|
||||
let mut key = room_id.to_vec();
|
||||
key.push(0xff);
|
||||
key.extend_from_slice(user_id);
|
||||
key.push(0xff);
|
||||
key.extend_from_slice(event_type);
|
||||
|
||||
db.account_data
|
||||
.roomusertype_roomuserdataid
|
||||
.insert(&key, &roomuserdataid)?;
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(5)?;
|
||||
|
||||
warn!("Migration: 4 -> 5 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 6 {
|
||||
// Set room member count
|
||||
for (roomid, _) in db.rooms.roomid_shortstatehash.iter() {
|
||||
let string = utils::string_from_bytes(&roomid).unwrap();
|
||||
let room_id = <&RoomId>::try_from(string.as_str()).unwrap();
|
||||
db.rooms.update_joined_count(room_id, &db)?;
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(6)?;
|
||||
|
||||
warn!("Migration: 5 -> 6 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 7 {
|
||||
// Upgrade state store
|
||||
let mut last_roomstates: HashMap<Box<RoomId>, u64> = HashMap::new();
|
||||
let mut current_sstatehash: Option<u64> = None;
|
||||
let mut current_room = None;
|
||||
let mut current_state = HashSet::new();
|
||||
let mut counter = 0;
|
||||
|
||||
let mut handle_state =
|
||||
|current_sstatehash: u64,
|
||||
current_room: &RoomId,
|
||||
current_state: HashSet<_>,
|
||||
last_roomstates: &mut HashMap<_, _>| {
|
||||
counter += 1;
|
||||
println!("counter: {}", counter);
|
||||
let last_roomsstatehash = last_roomstates.get(current_room);
|
||||
|
||||
let states_parents = last_roomsstatehash.map_or_else(
|
||||
|| Ok(Vec::new()),
|
||||
|&last_roomsstatehash| {
|
||||
db.rooms.load_shortstatehash_info(dbg!(last_roomsstatehash))
|
||||
},
|
||||
)?;
|
||||
|
||||
let (statediffnew, statediffremoved) =
|
||||
if let Some(parent_stateinfo) = states_parents.last() {
|
||||
let statediffnew = current_state
|
||||
.difference(&parent_stateinfo.1)
|
||||
.copied()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let statediffremoved = parent_stateinfo
|
||||
.1
|
||||
.difference(¤t_state)
|
||||
.copied()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
(statediffnew, statediffremoved)
|
||||
} else {
|
||||
(current_state, HashSet::new())
|
||||
};
|
||||
|
||||
db.rooms.save_state_from_diff(
|
||||
dbg!(current_sstatehash),
|
||||
statediffnew,
|
||||
statediffremoved,
|
||||
2, // every state change is 2 event changes on average
|
||||
states_parents,
|
||||
)?;
|
||||
|
||||
/*
|
||||
let mut tmp = db.rooms.load_shortstatehash_info(¤t_sstatehash, &db)?;
|
||||
let state = tmp.pop().unwrap();
|
||||
println!(
|
||||
"{}\t{}{:?}: {:?} + {:?} - {:?}",
|
||||
current_room,
|
||||
" ".repeat(tmp.len()),
|
||||
utils::u64_from_bytes(¤t_sstatehash).unwrap(),
|
||||
tmp.last().map(|b| utils::u64_from_bytes(&b.0).unwrap()),
|
||||
state
|
||||
.2
|
||||
.iter()
|
||||
.map(|b| utils::u64_from_bytes(&b[size_of::<u64>()..]).unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
state
|
||||
.3
|
||||
.iter()
|
||||
.map(|b| utils::u64_from_bytes(&b[size_of::<u64>()..]).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
*/
|
||||
|
||||
Ok::<_, Error>(())
|
||||
};
|
||||
|
||||
for (k, seventid) in db._db.open_tree("stateid_shorteventid")?.iter() {
|
||||
let sstatehash = utils::u64_from_bytes(&k[0..size_of::<u64>()])
|
||||
.expect("number of bytes is correct");
|
||||
let sstatekey = k[size_of::<u64>()..].to_vec();
|
||||
if Some(sstatehash) != current_sstatehash {
|
||||
if let Some(current_sstatehash) = current_sstatehash {
|
||||
handle_state(
|
||||
current_sstatehash,
|
||||
current_room.as_deref().unwrap(),
|
||||
current_state,
|
||||
&mut last_roomstates,
|
||||
)?;
|
||||
last_roomstates
|
||||
.insert(current_room.clone().unwrap(), current_sstatehash);
|
||||
}
|
||||
current_state = HashSet::new();
|
||||
current_sstatehash = Some(sstatehash);
|
||||
|
||||
let event_id = db
|
||||
.rooms
|
||||
.shorteventid_eventid
|
||||
.get(&seventid)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let string = utils::string_from_bytes(&event_id).unwrap();
|
||||
let event_id = <&EventId>::try_from(string.as_str()).unwrap();
|
||||
let pdu = db.rooms.get_pdu(event_id).unwrap().unwrap();
|
||||
|
||||
if Some(&pdu.room_id) != current_room.as_ref() {
|
||||
current_room = Some(pdu.room_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut val = sstatekey;
|
||||
val.extend_from_slice(&seventid);
|
||||
current_state.insert(val.try_into().expect("size is correct"));
|
||||
}
|
||||
|
||||
if let Some(current_sstatehash) = current_sstatehash {
|
||||
handle_state(
|
||||
current_sstatehash,
|
||||
current_room.as_deref().unwrap(),
|
||||
current_state,
|
||||
&mut last_roomstates,
|
||||
)?;
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(7)?;
|
||||
|
||||
warn!("Migration: 6 -> 7 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 8 {
|
||||
// Generate short room ids for all rooms
|
||||
for (room_id, _) in db.rooms.roomid_shortstatehash.iter() {
|
||||
let shortroomid = db.globals.next_count()?.to_be_bytes();
|
||||
db.rooms.roomid_shortroomid.insert(&room_id, &shortroomid)?;
|
||||
info!("Migration: 8");
|
||||
}
|
||||
// Update pduids db layout
|
||||
let mut batch = db.rooms.pduid_pdu.iter().filter_map(|(key, v)| {
|
||||
if !key.starts_with(b"!") {
|
||||
return None;
|
||||
}
|
||||
let mut parts = key.splitn(2, |&b| b == 0xff);
|
||||
let room_id = parts.next().unwrap();
|
||||
let count = parts.next().unwrap();
|
||||
|
||||
let short_room_id = db
|
||||
.rooms
|
||||
.roomid_shortroomid
|
||||
.get(room_id)
|
||||
.unwrap()
|
||||
.expect("shortroomid should exist");
|
||||
|
||||
let mut new_key = short_room_id;
|
||||
new_key.extend_from_slice(count);
|
||||
|
||||
Some((new_key, v))
|
||||
});
|
||||
|
||||
db.rooms.pduid_pdu.insert_batch(&mut batch)?;
|
||||
|
||||
let mut batch2 = db.rooms.eventid_pduid.iter().filter_map(|(k, value)| {
|
||||
if !value.starts_with(b"!") {
|
||||
return None;
|
||||
}
|
||||
let mut parts = value.splitn(2, |&b| b == 0xff);
|
||||
let room_id = parts.next().unwrap();
|
||||
let count = parts.next().unwrap();
|
||||
|
||||
let short_room_id = db
|
||||
.rooms
|
||||
.roomid_shortroomid
|
||||
.get(room_id)
|
||||
.unwrap()
|
||||
.expect("shortroomid should exist");
|
||||
|
||||
let mut new_value = short_room_id;
|
||||
new_value.extend_from_slice(count);
|
||||
|
||||
Some((k, new_value))
|
||||
});
|
||||
|
||||
db.rooms.eventid_pduid.insert_batch(&mut batch2)?;
|
||||
|
||||
db.globals.bump_database_version(8)?;
|
||||
|
||||
warn!("Migration: 7 -> 8 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 9 {
|
||||
// Update tokenids db layout
|
||||
let mut iter = db
|
||||
.rooms
|
||||
.tokenids
|
||||
.iter()
|
||||
.filter_map(|(key, _)| {
|
||||
if !key.starts_with(b"!") {
|
||||
return None;
|
||||
}
|
||||
let mut parts = key.splitn(4, |&b| b == 0xff);
|
||||
let room_id = parts.next().unwrap();
|
||||
let word = parts.next().unwrap();
|
||||
let _pdu_id_room = parts.next().unwrap();
|
||||
let pdu_id_count = parts.next().unwrap();
|
||||
|
||||
let short_room_id = db
|
||||
.rooms
|
||||
.roomid_shortroomid
|
||||
.get(room_id)
|
||||
.unwrap()
|
||||
.expect("shortroomid should exist");
|
||||
let mut new_key = short_room_id;
|
||||
new_key.extend_from_slice(word);
|
||||
new_key.push(0xff);
|
||||
new_key.extend_from_slice(pdu_id_count);
|
||||
println!("old {:?}", key);
|
||||
println!("new {:?}", new_key);
|
||||
Some((new_key, Vec::new()))
|
||||
})
|
||||
.peekable();
|
||||
|
||||
while iter.peek().is_some() {
|
||||
db.rooms
|
||||
.tokenids
|
||||
.insert_batch(&mut iter.by_ref().take(1000))?;
|
||||
println!("smaller batch done");
|
||||
}
|
||||
|
||||
info!("Deleting starts");
|
||||
|
||||
let batch2: Vec<_> = db
|
||||
.rooms
|
||||
.tokenids
|
||||
.iter()
|
||||
.filter_map(|(key, _)| {
|
||||
if key.starts_with(b"!") {
|
||||
println!("del {:?}", key);
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for key in batch2 {
|
||||
println!("del");
|
||||
db.rooms.tokenids.remove(&key)?;
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(9)?;
|
||||
|
||||
warn!("Migration: 8 -> 9 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 10 {
|
||||
// Add other direction for shortstatekeys
|
||||
for (statekey, shortstatekey) in db.rooms.statekey_shortstatekey.iter() {
|
||||
db.rooms
|
||||
.shortstatekey_statekey
|
||||
.insert(&shortstatekey, &statekey)?;
|
||||
}
|
||||
|
||||
// Force E2EE device list updates so we can send them over federation
|
||||
for user_id in db.users.iter().filter_map(|r| r.ok()) {
|
||||
db.users
|
||||
.mark_device_key_update(&user_id, &db.rooms, &db.globals)?;
|
||||
}
|
||||
|
||||
db.globals.bump_database_version(10)?;
|
||||
|
||||
warn!("Migration: 9 -> 10 finished");
|
||||
}
|
||||
|
||||
if db.globals.database_version()? < 11 {
|
||||
db._db
|
||||
.open_tree("userdevicesessionid_uiaarequest")?
|
||||
.clear()?;
|
||||
db.globals.bump_database_version(11)?;
|
||||
|
||||
warn!("Migration: 10 -> 11 finished");
|
||||
}
|
||||
|
||||
assert_eq!(11, latest_database_version);
|
||||
|
||||
info!(
|
||||
"Loaded {} database with version {}",
|
||||
config.database_backend, latest_database_version
|
||||
);
|
||||
} else {
|
||||
guard
|
||||
.globals
|
||||
.bump_database_version(latest_database_version)?;
|
||||
|
||||
// Create the admin room and server user on first run
|
||||
create_admin_room(&guard).await?;
|
||||
|
||||
warn!(
|
||||
"Created new {} database with version {}",
|
||||
config.database_backend, latest_database_version
|
||||
);
|
||||
}
|
||||
|
||||
// This data is probably outdated
|
||||
guard.rooms.edus.presenceid_presence.clear()?;
|
||||
|
||||
guard.admin.start_handler(Arc::clone(&db), admin_receiver);
|
||||
guard
|
||||
.sending
|
||||
.start_handler(Arc::clone(&db), sending_receiver);
|
||||
|
||||
drop(guard);
|
||||
|
||||
Self::start_cleanup_task(Arc::clone(&db), config).await;
|
||||
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
#[cfg(feature = "conduit_bin")]
|
||||
pub async fn start_on_shutdown_tasks(db: Arc<TokioRwLock<Self>>, shutdown: Shutdown) {
|
||||
tokio::spawn(async move {
|
||||
shutdown.await;
|
||||
|
||||
info!(target: "shutdown-sync", "Received shutdown notification, notifying sync helpers...");
|
||||
|
||||
db.read().await.globals.rotate.fire();
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) {
|
||||
let userid_bytes = user_id.as_bytes().to_vec();
|
||||
let mut userid_prefix = userid_bytes.clone();
|
||||
userid_prefix.push(0xff);
|
||||
|
||||
let mut userdeviceid_prefix = userid_prefix.clone();
|
||||
userdeviceid_prefix.extend_from_slice(device_id.as_bytes());
|
||||
userdeviceid_prefix.push(0xff);
|
||||
|
||||
let mut futures = FuturesUnordered::new();
|
||||
|
||||
// Return when *any* user changed his key
|
||||
// TODO: only send for user they share a room with
|
||||
futures.push(
|
||||
self.users
|
||||
.todeviceid_events
|
||||
.watch_prefix(&userdeviceid_prefix),
|
||||
);
|
||||
|
||||
futures.push(self.rooms.userroomid_joined.watch_prefix(&userid_prefix));
|
||||
futures.push(
|
||||
self.rooms
|
||||
.userroomid_invitestate
|
||||
.watch_prefix(&userid_prefix),
|
||||
);
|
||||
futures.push(self.rooms.userroomid_leftstate.watch_prefix(&userid_prefix));
|
||||
futures.push(
|
||||
self.rooms
|
||||
.userroomid_notificationcount
|
||||
.watch_prefix(&userid_prefix),
|
||||
);
|
||||
futures.push(
|
||||
self.rooms
|
||||
.userroomid_highlightcount
|
||||
.watch_prefix(&userid_prefix),
|
||||
);
|
||||
|
||||
// Events for rooms we are in
|
||||
for room_id in self.rooms.rooms_joined(user_id).filter_map(|r| r.ok()) {
|
||||
let short_roomid = self
|
||||
.rooms
|
||||
.get_shortroomid(&room_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("room exists")
|
||||
.to_be_bytes()
|
||||
.to_vec();
|
||||
|
||||
let roomid_bytes = room_id.as_bytes().to_vec();
|
||||
let mut roomid_prefix = roomid_bytes.clone();
|
||||
roomid_prefix.push(0xff);
|
||||
|
||||
// PDUs
|
||||
futures.push(self.rooms.pduid_pdu.watch_prefix(&short_roomid));
|
||||
|
||||
// EDUs
|
||||
futures.push(
|
||||
self.rooms
|
||||
.edus
|
||||
.roomid_lasttypingupdate
|
||||
.watch_prefix(&roomid_bytes),
|
||||
);
|
||||
|
||||
futures.push(
|
||||
self.rooms
|
||||
.edus
|
||||
.readreceiptid_readreceipt
|
||||
.watch_prefix(&roomid_prefix),
|
||||
);
|
||||
|
||||
// Key changes
|
||||
futures.push(self.users.keychangeid_userid.watch_prefix(&roomid_prefix));
|
||||
|
||||
// Room account data
|
||||
let mut roomuser_prefix = roomid_prefix.clone();
|
||||
roomuser_prefix.extend_from_slice(&userid_prefix);
|
||||
|
||||
futures.push(
|
||||
self.account_data
|
||||
.roomusertype_roomuserdataid
|
||||
.watch_prefix(&roomuser_prefix),
|
||||
);
|
||||
}
|
||||
|
||||
let mut globaluserdata_prefix = vec![0xff];
|
||||
globaluserdata_prefix.extend_from_slice(&userid_prefix);
|
||||
|
||||
futures.push(
|
||||
self.account_data
|
||||
.roomusertype_roomuserdataid
|
||||
.watch_prefix(&globaluserdata_prefix),
|
||||
);
|
||||
|
||||
// More key changes (used when user is not joined to any rooms)
|
||||
futures.push(self.users.keychangeid_userid.watch_prefix(&userid_prefix));
|
||||
|
||||
// One time keys
|
||||
futures.push(
|
||||
self.users
|
||||
.userid_lastonetimekeyupdate
|
||||
.watch_prefix(&userid_bytes),
|
||||
);
|
||||
|
||||
futures.push(Box::pin(self.globals.rotate.watch()));
|
||||
|
||||
// Wait until one of them finds something
|
||||
futures.next().await;
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn flush(&self) -> Result<()> {
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let res = self._db.flush();
|
||||
|
||||
debug!("flush: took {:?}", start.elapsed());
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(db, config))]
|
||||
pub async fn start_cleanup_task(db: Arc<TokioRwLock<Self>>, config: &Config) {
|
||||
use tokio::time::interval;
|
||||
|
||||
#[cfg(unix)]
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
use tracing::info;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
let timer_interval = Duration::from_secs(config.cleanup_second_interval as u64);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut i = interval(timer_interval);
|
||||
#[cfg(unix)]
|
||||
let mut s = signal(SignalKind::hangup()).unwrap();
|
||||
|
||||
loop {
|
||||
#[cfg(unix)]
|
||||
tokio::select! {
|
||||
_ = i.tick() => {
|
||||
info!("cleanup: Timer ticked");
|
||||
}
|
||||
_ = s.recv() => {
|
||||
info!("cleanup: Received SIGHUP");
|
||||
}
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
i.tick().await;
|
||||
info!("cleanup: Timer ticked")
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
if let Err(e) = db.read().await._db.cleanup() {
|
||||
error!("cleanup: Errored: {}", e);
|
||||
} else {
|
||||
info!("cleanup: Finished in {:?}", start.elapsed());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DatabaseGuard(OwnedRwLockReadGuard<Database>);
|
||||
|
||||
impl Deref for DatabaseGuard {
|
||||
type Target = OwnedRwLockReadGuard<Database>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for DatabaseGuard {
|
||||
type Error = ();
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> rocket::request::Outcome<Self, ()> {
|
||||
let db = try_outcome!(req.guard::<&State<Arc<TokioRwLock<Database>>>>().await);
|
||||
|
||||
Ok(DatabaseGuard(Arc::clone(db).read_owned().await)).or_forward(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OwnedRwLockReadGuard<Database>> for DatabaseGuard {
|
||||
fn from(val: OwnedRwLockReadGuard<Database>) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
}
|
|
@ -26,11 +26,11 @@ pub mod persy;
|
|||
))]
|
||||
pub mod watchers;
|
||||
|
||||
pub trait DatabaseEngine: Send + Sync {
|
||||
pub trait KeyValueDatabaseEngine: Send + Sync {
|
||||
fn open(config: &Config) -> Result<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
fn open_tree(&self, name: &'static str) -> Result<Arc<dyn Tree>>;
|
||||
fn open_tree(&self, name: &'static str) -> Result<Arc<dyn KvTree>>;
|
||||
fn flush(&self) -> Result<()>;
|
||||
fn cleanup(&self) -> Result<()> {
|
||||
Ok(())
|
||||
|
@ -38,9 +38,10 @@ pub trait DatabaseEngine: Send + Sync {
|
|||
fn memory_usage(&self) -> Result<String> {
|
||||
Ok("Current database engine does not support memory usage reporting.".to_owned())
|
||||
}
|
||||
fn clear_caches(&self) {}
|
||||
}
|
||||
|
||||
pub trait Tree: Send + Sync {
|
||||
pub trait KvTree: Send + Sync {
|
||||
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>>;
|
||||
|
||||
fn insert(&self, key: &[u8], value: &[u8]) -> Result<()>;
|
||||
|
|
|
@ -69,7 +69,6 @@ impl DatabaseEngine for Engine {
|
|||
}
|
||||
|
||||
impl EngineTree {
|
||||
#[tracing::instrument(skip(self, tree, from, backwards))]
|
||||
fn iter_from_thread(
|
||||
&self,
|
||||
tree: Arc<heed::UntypedDatabase>,
|
||||
|
@ -94,7 +93,6 @@ impl EngineTree {
|
|||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(tree, txn, from, backwards))]
|
||||
fn iter_from_thread_work(
|
||||
tree: Arc<heed::UntypedDatabase>,
|
||||
txn: &heed::RoTxn<'_>,
|
||||
|
@ -126,7 +124,6 @@ fn iter_from_thread_work(
|
|||
}
|
||||
|
||||
impl Tree for EngineTree {
|
||||
#[tracing::instrument(skip(self, key))]
|
||||
fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>> {
|
||||
let txn = self.engine.env.read_txn().map_err(convert_error)?;
|
||||
Ok(self
|
||||
|
@ -136,7 +133,6 @@ impl Tree for EngineTree {
|
|||
.map(|s| s.to_vec()))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, key, value))]
|
||||
fn insert(&self, key: &[u8], value: &[u8]) -> Result<()> {
|
||||
let mut txn = self.engine.env.write_txn().map_err(convert_error)?;
|
||||
self.tree
|
||||
|
@ -147,7 +143,6 @@ impl Tree for EngineTree {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, key))]
|
||||
fn remove(&self, key: &[u8]) -> Result<()> {
|
||||
let mut txn = self.engine.env.write_txn().map_err(convert_error)?;
|
||||
self.tree.delete(&mut txn, &key).map_err(convert_error)?;
|
||||
|
@ -155,12 +150,10 @@ impl Tree for EngineTree {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn iter<'a>(&'a self) -> Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + Send + 'a> {
|
||||
self.iter_from(&[], false)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, from, backwards))]
|
||||
fn iter_from(
|
||||
&self,
|
||||
from: &[u8],
|
||||
|
@ -169,7 +162,6 @@ impl Tree for EngineTree {
|
|||
self.iter_from_thread(Arc::clone(&self.tree), from.to_vec(), backwards)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, key))]
|
||||
fn increment(&self, key: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut txn = self.engine.env.write_txn().map_err(convert_error)?;
|
||||
|
||||
|
@ -186,7 +178,6 @@ impl Tree for EngineTree {
|
|||
Ok(new)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, prefix))]
|
||||
fn scan_prefix<'a>(
|
||||
&'a self,
|
||||
prefix: Vec<u8>,
|
||||
|
@ -197,7 +188,6 @@ impl Tree for EngineTree {
|
|||
)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, prefix))]
|
||||
fn watch_prefix<'a>(&'a self, prefix: &[u8]) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
||||
self.watchers.watch(prefix)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue