mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-06 19:25:59 +00:00
Compare commits
588 Commits
20230611-2
...
feat/repls
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9643ebd8d | ||
|
|
f181edc8f2 | ||
|
|
ccda6f5c53 | ||
|
|
52ed5100c2 | ||
|
|
c2d4369e16 | ||
|
|
91783b9066 | ||
|
|
735d644e04 | ||
|
|
d1e72cee42 | ||
|
|
6107f242fd | ||
|
|
a5cd08a655 | ||
|
|
98c7b1b5a5 | ||
|
|
ec22deb2cd | ||
|
|
ecb94e278c | ||
|
|
1a9d5b4b65 | ||
|
|
abfb3925fd | ||
|
|
2a73d82250 | ||
|
|
edfa97db10 | ||
|
|
cf2cedcee1 | ||
|
|
b830631bd2 | ||
|
|
3fa0abf47f | ||
|
|
5191bd16b2 | ||
|
|
2547fc6cd3 | ||
|
|
5701b5c18c | ||
|
|
dc4438bc3c | ||
|
|
f2fc1ee00c | ||
|
|
ab525699bd | ||
|
|
d56135e0cb | ||
|
|
52c0d2f266 | ||
|
|
49c6289580 | ||
|
|
b0b728fb0d | ||
|
|
d629e89304 | ||
|
|
24d0d2b726 | ||
|
|
f777e2863c | ||
|
|
a80ad1cbe5 | ||
|
|
0b522ebddc | ||
|
|
6bd045eb79 | ||
|
|
0698e115e8 | ||
|
|
8e3765df4c | ||
|
|
1c98ead695 | ||
|
|
8849d03cd2 | ||
|
|
a75e4eabfc | ||
|
|
74f0c537e6 | ||
|
|
1f8ea963ad | ||
|
|
7c7f4f24de | ||
|
|
a11986ba1a | ||
|
|
2e87696c83 | ||
|
|
94cb916231 | ||
|
|
95c7dbc793 | ||
|
|
ba798490e2 | ||
|
|
5132270f2e | ||
|
|
4a0957b5ec | ||
|
|
1bfb2a7214 | ||
|
|
84483cee7a | ||
|
|
4bfd79c42c | ||
|
|
413aa04dca | ||
|
|
5d3e65e076 | ||
|
|
98da8a28a0 | ||
|
|
84b57dee57 | ||
|
|
1fef495d5e | ||
|
|
310d82819d | ||
|
|
5611dab86f | ||
|
|
01ded209c6 | ||
|
|
a1a21252d0 | ||
|
|
0dc5442498 | ||
|
|
9fb55f74dc | ||
|
|
24cfc33907 | ||
|
|
ce2a4eccbd | ||
|
|
ff68679bad | ||
|
|
9bdb7d7feb | ||
|
|
3c51fad74d | ||
|
|
4344419cdb | ||
|
|
65efea020d | ||
|
|
b9360b4d88 | ||
|
|
12c187a386 | ||
|
|
2846f09c45 | ||
|
|
50835f08c8 | ||
|
|
c413fbcb58 | ||
|
|
c674a9fd4e | ||
|
|
b2da2a8587 | ||
|
|
d0ea88f1f8 | ||
|
|
92b466a5f0 | ||
|
|
aeeafebefc | ||
|
|
bf0048a052 | ||
|
|
b4ba02218f | ||
|
|
d13609c372 | ||
|
|
58fb269450 | ||
|
|
303e52b476 | ||
|
|
868894f500 | ||
|
|
67d49b461f | ||
|
|
f89ee5a370 | ||
|
|
2afea56e56 | ||
|
|
a5387b64af | ||
|
|
0b178fc791 | ||
|
|
5d27a91e90 | ||
|
|
e2016a2ab4 | ||
|
|
924c7f5b18 | ||
|
|
22e46194ab | ||
|
|
ffdb2b6900 | ||
|
|
a7b870c397 | ||
|
|
b48eb28fc2 | ||
|
|
dc9c82aa4e | ||
|
|
24fedf8c4d | ||
|
|
dda20f44ae | ||
|
|
0b7c132ace | ||
|
|
e006cdd051 | ||
|
|
ac2971a78d | ||
|
|
b7c2ca84ab | ||
|
|
82b492e529 | ||
|
|
d567155f12 | ||
|
|
27ea7345ea | ||
|
|
5b534242c4 | ||
|
|
e36fc9738b | ||
|
|
657a3f08e5 | ||
|
|
af78ac0586 | ||
|
|
d65c1a1ab3 | ||
|
|
4fb99e3bd0 | ||
|
|
f0a83abcfa | ||
|
|
6f1c715b8c | ||
|
|
38dd4d1079 | ||
|
|
154204742d | ||
|
|
db55998546 | ||
|
|
5885e067a6 | ||
|
|
a92152d86d | ||
|
|
b5cd5e30ef | ||
|
|
b0c977b324 | ||
|
|
bfe4018e43 | ||
|
|
cc7a7962a8 | ||
|
|
fb4011084d | ||
|
|
3a3415915f | ||
|
|
964884a5de | ||
|
|
db57706794 | ||
|
|
a186732ca5 | ||
|
|
e635bc23ec | ||
|
|
3cb7da95e3 | ||
|
|
dea0f675dd | ||
|
|
480f210ce8 | ||
|
|
caa8607468 | ||
|
|
32e6600272 | ||
|
|
5335124306 | ||
|
|
1561481eb4 | ||
|
|
8f9015a6ff | ||
|
|
ed22b3a5ce | ||
|
|
65fbd36624 | ||
|
|
050ba16d4a | ||
|
|
65bc6c8fc6 | ||
|
|
6ad3da5f35 | ||
|
|
947eb15771 | ||
|
|
f4ee35fb09 | ||
|
|
6048587d34 | ||
|
|
80cf8fc4e8 | ||
|
|
4f54227495 | ||
|
|
aab1734615 | ||
|
|
40a41ffd64 | ||
|
|
d30ceea373 | ||
|
|
3e8a401077 | ||
|
|
99f400bc7b | ||
|
|
73b576a75f | ||
|
|
4e4e598daf | ||
|
|
77daf82b94 | ||
|
|
e00603f276 | ||
|
|
1b2c7b2fa1 | ||
|
|
c526095d4f | ||
|
|
8cc4bbea4d | ||
|
|
911ffc767e | ||
|
|
1690df998d | ||
|
|
519d3c08a8 | ||
|
|
9846d8aac2 | ||
|
|
c74b6255dd | ||
|
|
df91b8c990 | ||
|
|
c4728c696d | ||
|
|
8153f5f17a | ||
|
|
99a80f723d | ||
|
|
4f13f5899b | ||
|
|
5362e84730 | ||
|
|
be89e62d54 | ||
|
|
a72134f4ad | ||
|
|
69ae767175 | ||
|
|
2aff76c369 | ||
|
|
01e0f9e558 | ||
|
|
8ea8f50fb8 | ||
|
|
68bcfcb626 | ||
|
|
9632a0dc1b | ||
|
|
a4a0934475 | ||
|
|
2061794305 | ||
|
|
3a7ebad883 | ||
|
|
6daa8223b2 | ||
|
|
4a58d6eae0 | ||
|
|
a8a2e491b0 | ||
|
|
f98f09a5ea | ||
|
|
7a4e12a732 | ||
|
|
aa8f8575bc | ||
|
|
70d6a5ac08 | ||
|
|
7f85ad5e7b | ||
|
|
c785ae615d | ||
|
|
9c8a362e9f | ||
|
|
e708c612cf | ||
|
|
f75d635c81 | ||
|
|
29b6ac3acf | ||
|
|
a1b0e4767a | ||
|
|
f4281c7050 | ||
|
|
d2e83c94f3 | ||
|
|
2540860129 | ||
|
|
93a5de769d | ||
|
|
717d5e9a88 | ||
|
|
18f06467bb | ||
|
|
8d88ea9963 | ||
|
|
e3e1cab02e | ||
|
|
00bd56b687 | ||
|
|
a7727bba5c | ||
|
|
6b8005f530 | ||
|
|
714fba7ea6 | ||
|
|
ac05fb385c | ||
|
|
537c147c4b | ||
|
|
7bee609b84 | ||
|
|
859c0214ec | ||
|
|
5eea0bbc9a | ||
|
|
15dec656a8 | ||
|
|
bcf6561c0c | ||
|
|
b177a3e201 | ||
|
|
d1236bf8f2 | ||
|
|
01490f5723 | ||
|
|
3ceefb840b | ||
|
|
eb5f5f91cd | ||
|
|
52e1f0ddde | ||
|
|
defc9ec79b | ||
|
|
7026961df4 | ||
|
|
078380d305 | ||
|
|
add3f40b23 | ||
|
|
7216e9909b | ||
|
|
a1e6a19210 | ||
|
|
5f39403ce7 | ||
|
|
e525ffe5e4 | ||
|
|
e3723d647e | ||
|
|
5f84daa9db | ||
|
|
e957af4ca3 | ||
|
|
439bacf067 | ||
|
|
fa55e88dd9 | ||
|
|
71e7fe3086 | ||
|
|
03f2e3b1bf | ||
|
|
8bbb579d23 | ||
|
|
6448af071b | ||
|
|
c2a0ab71df | ||
|
|
7a17165c24 | ||
|
|
d7213fa409 | ||
|
|
479f0402ca | ||
|
|
b62eeef80c | ||
|
|
7b15006a50 | ||
|
|
ac731e547d | ||
|
|
443f374f23 | ||
|
|
42367f477c | ||
|
|
3cb91151ca | ||
|
|
4c46054bff | ||
|
|
f01794af93 | ||
|
|
49f7f9549c | ||
|
|
72e3129557 | ||
|
|
75a07a84c0 | ||
|
|
b55765d7c7 | ||
|
|
ed5ded5e45 | ||
|
|
249749e14d | ||
|
|
4c00a7dfb7 | ||
|
|
acc4317246 | ||
|
|
b9ae333b02 | ||
|
|
bf0fc504a9 | ||
|
|
705e517871 | ||
|
|
6f99ac2160 | ||
|
|
2fcc714546 | ||
|
|
1e72f7bc77 | ||
|
|
af0d24c7c6 | ||
|
|
ab58177dfa | ||
|
|
df07426019 | ||
|
|
c30d9a2620 | ||
|
|
7b44317705 | ||
|
|
876068a37e | ||
|
|
397b9878e1 | ||
|
|
d9deadc65a | ||
|
|
58d3c5cc2e | ||
|
|
520fb02fb6 | ||
|
|
2cb12a3d59 | ||
|
|
18888eae5f | ||
|
|
f31020fb6e | ||
|
|
bb202079e0 | ||
|
|
bb6bcda8bd | ||
|
|
68099bd2b7 | ||
|
|
a8db1cb40d | ||
|
|
efa7ba78ed | ||
|
|
25fc692dc1 | ||
|
|
530d68fe89 | ||
|
|
afd8c906ba | ||
|
|
c596dd5458 | ||
|
|
ed78b253ff | ||
|
|
66c84e0ad9 | ||
|
|
21335b3297 | ||
|
|
8fc791f81a | ||
|
|
1689ee5ddc | ||
|
|
7a061bb3c6 | ||
|
|
6c3b8eaa92 | ||
|
|
5e1b2e165f | ||
|
|
e270b5df6a | ||
|
|
cbf9e81256 | ||
|
|
3080ec1f5a | ||
|
|
4fd66b2719 | ||
|
|
916f47e2f5 | ||
|
|
a3c5b1bf87 | ||
|
|
ace6c30ba5 | ||
|
|
f4104612b2 | ||
|
|
6209bc7152 | ||
|
|
1a6a8a809b | ||
|
|
5485781e7c | ||
|
|
8b21825ffe | ||
|
|
530ff4f9c7 | ||
|
|
2eb596fe99 | ||
|
|
0bbb9e7072 | ||
|
|
2433fa661a | ||
|
|
70bdeecf77 | ||
|
|
24dc96f80f | ||
|
|
ebbbb5e174 | ||
|
|
78757ac7f1 | ||
|
|
01368960f3 | ||
|
|
ae1d5d07e3 | ||
|
|
5ad72abca7 | ||
|
|
c1b92ef56e | ||
|
|
acbc1b8956 | ||
|
|
bce24c8b1b | ||
|
|
31262315e7 | ||
|
|
d8acaa1107 | ||
|
|
730039eda6 | ||
|
|
79e1388000 | ||
|
|
5c8ece0727 | ||
|
|
ab6b92aa2a | ||
|
|
239710951a | ||
|
|
28960979ec | ||
|
|
1938ebc3fa | ||
|
|
1fb4032d9e | ||
|
|
ae2194cce8 | ||
|
|
949a0cf649 | ||
|
|
f6c57b23b4 | ||
|
|
12ae781621 | ||
|
|
a52d610e6c | ||
|
|
f513a9cb5e | ||
|
|
535f01604b | ||
|
|
f8ec6ba5ff | ||
|
|
b8fdc07508 | ||
|
|
5ce7530206 | ||
|
|
b36bac3225 | ||
|
|
beef06ebe3 | ||
|
|
4fc46f765b | ||
|
|
b45ae2cd4d | ||
|
|
0a8089787b | ||
|
|
475fa6f2e6 | ||
|
|
96bf73cb57 | ||
|
|
87a9fb79f2 | ||
|
|
c50435d499 | ||
|
|
d2f4823ceb | ||
|
|
39230c559a | ||
|
|
32d1d5df2e | ||
|
|
7547fbe245 | ||
|
|
9f13cb6f47 | ||
|
|
d94571fbf9 | ||
|
|
389ecc0e5c | ||
|
|
412f4a99d7 | ||
|
|
caa0795d07 | ||
|
|
5c82343bb3 | ||
|
|
e17af1c064 | ||
|
|
a9e309395e | ||
|
|
8468ce19cb | ||
|
|
27f15f7b02 | ||
|
|
78b9fa168b | ||
|
|
e290d168ac | ||
|
|
78cd89ec32 | ||
|
|
f9d9059e73 | ||
|
|
0954d71f51 | ||
|
|
9ea2bd9f2f | ||
|
|
d6bcb844db | ||
|
|
ce20e689cc | ||
|
|
d9cfc9d0e1 | ||
|
|
64a07d09f4 | ||
|
|
00e881799f | ||
|
|
fa598dd6f8 | ||
|
|
ac4e2cb10b | ||
|
|
ec578ab0ef | ||
|
|
ea6ba59841 | ||
|
|
f903f716e3 | ||
|
|
c5f4b94aa5 | ||
|
|
1d5dae4751 | ||
|
|
ac20b6bc99 | ||
|
|
6ec8007e4e | ||
|
|
e5eea267cf | ||
|
|
de5add09d0 | ||
|
|
1ec8f46c1d | ||
|
|
80666848cc | ||
|
|
e8e9613169 | ||
|
|
eda36436a8 | ||
|
|
93e05e9f18 | ||
|
|
090d8cba71 | ||
|
|
f16e72e329 | ||
|
|
8248a4a5b8 | ||
|
|
cf0b4274c8 | ||
|
|
36ecb48c7b | ||
|
|
ba411fef43 | ||
|
|
4055363cff | ||
|
|
d179d1e695 | ||
|
|
fc9ceb7540 | ||
|
|
a31a3e2e65 | ||
|
|
b12e728514 | ||
|
|
962c7d62c7 | ||
|
|
d677716f93 | ||
|
|
8099310f89 | ||
|
|
5c40f66010 | ||
|
|
8e7dd21bce | ||
|
|
4868205df4 | ||
|
|
96fb0eecca | ||
|
|
2cb20618da | ||
|
|
92e948aabc | ||
|
|
cb4435d700 | ||
|
|
4c6e78e1a5 | ||
|
|
da9a91e05f | ||
|
|
506968634e | ||
|
|
a5d0cdf0dd | ||
|
|
c59aad43c8 | ||
|
|
19e72babc9 | ||
|
|
bfcdb13d6d | ||
|
|
878c1a83ad | ||
|
|
471e0f55e4 | ||
|
|
08127218ce | ||
|
|
b69202c222 | ||
|
|
b3494b12fc | ||
|
|
7703475868 | ||
|
|
4be3bdc4c3 | ||
|
|
3e26e7e89d | ||
|
|
13b95d383e | ||
|
|
3c76005c33 | ||
|
|
3065e153d5 | ||
|
|
a1eeedabb9 | ||
|
|
0fc59b8e2a | ||
|
|
265d3ba8ef | ||
|
|
20d398d02c | ||
|
|
6bc74749d2 | ||
|
|
301676fb54 | ||
|
|
6b488f347e | ||
|
|
1a8e43d280 | ||
|
|
acb4190f70 | ||
|
|
9e42567050 | ||
|
|
78fb6e1982 | ||
|
|
9de963fb56 | ||
|
|
03340a2b5a | ||
|
|
b2d3344ddd | ||
|
|
49bb235938 | ||
|
|
4e7049d51e | ||
|
|
2de7598f8d | ||
|
|
f6a565385e | ||
|
|
569bd1d5e1 | ||
|
|
aca1fe6dff | ||
|
|
aded2e3239 | ||
|
|
54a4eff623 | ||
|
|
50c36dcefd | ||
|
|
f16c275bcb | ||
|
|
74ff072199 | ||
|
|
52431bd1dd | ||
|
|
fa67e87467 | ||
|
|
8f9b18444a | ||
|
|
7b18e983c9 | ||
|
|
c0e64c9052 | ||
|
|
0d54ccd342 | ||
|
|
d4d5b23c7d | ||
|
|
a641034895 | ||
|
|
9dd24526ef | ||
|
|
6d4f772d78 | ||
|
|
4c4dada3da | ||
|
|
cf82972a80 | ||
|
|
ea00f0fec1 | ||
|
|
226dbca6e0 | ||
|
|
e4b5a23f71 | ||
|
|
9663caa1ff | ||
|
|
f9f0f409a8 | ||
|
|
8f4d6c864b | ||
|
|
513461c99a | ||
|
|
41ef76ff3e | ||
|
|
c451e431cd | ||
|
|
09a848f5a6 | ||
|
|
308193377d | ||
|
|
3d3326e68a | ||
|
|
88c2232a6c | ||
|
|
25016efbc2 | ||
|
|
db63ac4064 | ||
|
|
1f593a1708 | ||
|
|
219c16a69c | ||
|
|
aed8f69f34 | ||
|
|
196a856c68 | ||
|
|
9bf60e87e9 | ||
|
|
b90e7a4412 | ||
|
|
cb813e91f0 | ||
|
|
a6bc9fcbb3 | ||
|
|
cd0737911b | ||
|
|
7705354d4d | ||
|
|
39ac8b2278 | ||
|
|
c5494aa1ca | ||
|
|
1a96bf5a4b | ||
|
|
37a1244751 | ||
|
|
bf39f18f8d | ||
|
|
95be7dc87a | ||
|
|
9be171c7b6 | ||
|
|
786fbb8ae4 | ||
|
|
6c0210e8a1 | ||
|
|
456bf7b42a | ||
|
|
21ffea0f9f | ||
|
|
96d90215d2 | ||
|
|
29dcf714f2 | ||
|
|
cb97004d3f | ||
|
|
0b6e5dd9db | ||
|
|
7132877201 | ||
|
|
4d42fb74e7 | ||
|
|
866ace3381 | ||
|
|
0798e81862 | ||
|
|
2fa5ac41ac | ||
|
|
25ae1555a2 | ||
|
|
881888bd1c | ||
|
|
41f20c2239 | ||
|
|
38f1381fea | ||
|
|
6838b0b1f0 | ||
|
|
055deab105 | ||
|
|
6d2073120d | ||
|
|
9789909061 | ||
|
|
895de86f1e | ||
|
|
98b8c5403b | ||
|
|
aa7c75c60e | ||
|
|
4270f0c5d4 | ||
|
|
eb1f45d208 | ||
|
|
9929f7499a | ||
|
|
279d9ef1b5 | ||
|
|
5a9bb9e68d | ||
|
|
a8fc9e928a | ||
|
|
da231ae390 | ||
|
|
7a31da4189 | ||
|
|
7d0b5392b5 | ||
|
|
1e1d44c0e4 | ||
|
|
1c4588967f | ||
|
|
853366a297 | ||
|
|
7568f49755 | ||
|
|
0542788567 | ||
|
|
73f7b8f007 | ||
|
|
c2412190a8 | ||
|
|
5372296dc0 | ||
|
|
41a47bdf8f | ||
|
|
51c26e324d | ||
|
|
3a55d00c6a | ||
|
|
004e84ece9 | ||
|
|
3d0914ce52 | ||
|
|
bde432cb75 | ||
|
|
28d225952a | ||
|
|
2fb9e7a802 | ||
|
|
431046cb4c | ||
|
|
92032cd9bc | ||
|
|
bfdd713d38 | ||
|
|
430df03b20 | ||
|
|
f5f70287e7 | ||
|
|
d5ba4ebf0c | ||
|
|
3bfead4ff1 | ||
|
|
d04621b1d6 | ||
|
|
a681df04bd | ||
|
|
9f3c1036d0 | ||
|
|
c9011ac692 | ||
|
|
42f977f536 | ||
|
|
a0580c5f5a | ||
|
|
121a9cd87c | ||
|
|
d87d608d9e | ||
|
|
bd9ecc1519 | ||
|
|
f5a113da47 | ||
|
|
11fdb0c1dc | ||
|
|
e0033ceb12 | ||
|
|
7318ec6ef6 | ||
|
|
a516c7adcf | ||
|
|
c48109ca66 | ||
|
|
f0ef781cd7 | ||
|
|
d9dd040db4 | ||
|
|
fec947b4c8 | ||
|
|
ec6df36c25 | ||
|
|
76150db293 | ||
|
|
815c3fe99e | ||
|
|
54878e8e8d | ||
|
|
6e4798f1d4 | ||
|
|
55bd6fb087 | ||
|
|
49035f4817 | ||
|
|
c0ebaa0bd3 | ||
|
|
b98b244fc3 | ||
|
|
c8d5128b0c | ||
|
|
0578a05a05 | ||
|
|
9a412b3e08 | ||
|
|
8deec1f80a |
2
.config/nextest.toml
Normal file
2
.config/nextest.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "5s", terminate-after = 10 }
|
||||
79
.env.example
79
.env.example
@@ -1,79 +0,0 @@
|
||||
# MongoDB URI
|
||||
MONGODB=mongodb://localhost
|
||||
|
||||
# URL to where the Revolt app is publicly accessible
|
||||
REVOLT_APP_URL=http://local.revolt.chat:5000
|
||||
|
||||
# URL to where the API is publicly accessible
|
||||
REVOLT_PUBLIC_URL=http://local.revolt.chat:8000
|
||||
VITE_API_URL=http://local.revolt.chat:8000
|
||||
|
||||
# URL to where the WebSocket server is publicly accessible
|
||||
REVOLT_EXTERNAL_WS_URL=ws://local.revolt.chat:9000
|
||||
|
||||
# URL to where Autumn is publicly available
|
||||
AUTUMN_PUBLIC_URL=http://local.revolt.chat:3000
|
||||
|
||||
# URL to where January is publicly available
|
||||
JANUARY_PUBLIC_URL=http://local.revolt.chat:7000
|
||||
|
||||
# URL to where Vortex is publicly available
|
||||
# VOSO_PUBLIC_URL=https://voso.revolt.chat
|
||||
|
||||
|
||||
##
|
||||
## hCaptcha Settings
|
||||
##
|
||||
|
||||
# If you are sure that you don't want to use hCaptcha, set to 1.
|
||||
REVOLT_UNSAFE_NO_CAPTCHA=1
|
||||
|
||||
# hCaptcha API key
|
||||
# REVOLT_HCAPTCHA_KEY=0x0000000000000000000000000000000000000000
|
||||
|
||||
# hCaptcha site key
|
||||
# REVOLT_HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
|
||||
|
||||
|
||||
##
|
||||
## Email Settings
|
||||
##
|
||||
|
||||
# If you are sure that you don't want to use email verification, set to 1.
|
||||
REVOLT_UNSAFE_NO_EMAIL=1
|
||||
|
||||
# SMTP host
|
||||
# REVOLT_SMTP_HOST=smtp.example.com
|
||||
|
||||
# SMTP username
|
||||
# REVOLT_SMTP_USERNAME=noreply@example.com
|
||||
|
||||
# SMTP password
|
||||
# REVOLT_SMTP_PASSWORD=CHANGEME
|
||||
|
||||
# SMTP From header
|
||||
# REVOLT_SMTP_FROM=Revolt <noreply@example.com>
|
||||
|
||||
|
||||
##
|
||||
## Application Settings
|
||||
##
|
||||
|
||||
# Whether to only allow users to sign up if they have an invite code
|
||||
REVOLT_INVITE_ONLY=0
|
||||
|
||||
# Maximum number of people that can be in a group chat
|
||||
REVOLT_MAX_GROUP_SIZE=150
|
||||
|
||||
# VAPID keys for push notifications
|
||||
# Generate using this guide: https://gitlab.insrt.uk/revolt/delta/-/wikis/vapid
|
||||
# --> Please replace these keys before going into production! <--
|
||||
REVOLT_VAPID_PRIVATE_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw=
|
||||
|
||||
|
||||
##
|
||||
## Vortex configuration
|
||||
##
|
||||
|
||||
# VOSO_MANAGE_TOKEN=CHANGEME
|
||||
147
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
147
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
name: Bug Report
|
||||
description: Report a bug in Stoat — please fill out every required field to help us triage quickly.
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
- unconfirmed
|
||||
|
||||
body:
|
||||
# ── Thank you section ────────────────────────────────────────────────────────
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for taking the time to report a bug for Stoat!**
|
||||
|
||||
Please follow the instructions below and provide as much detail as possible to help us understand and reproduce the issue.
|
||||
|
||||
# ── Pre-flight checks ────────────────────────────────────────────────────────
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Pre-flight checklist
|
||||
description: Please confirm all of the following before submitting.
|
||||
options:
|
||||
- label: I am on the latest available version of Stoat.
|
||||
required: true
|
||||
- label: I searched existing issues and did not find a duplicate.
|
||||
required: true
|
||||
- label: This is a bug report, not a support question or feature request.
|
||||
required: true
|
||||
|
||||
# ── Bug description ──────────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: >
|
||||
What happened? What did you expect to happen instead?
|
||||
Keep this brief — detailed steps go in the next field.
|
||||
placeholder: "Example: When I open a DM thread, new messages from the other person do not appear unless I reload the page."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Steps to reproduce ───────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: >
|
||||
Provide a numbered list of every step needed to trigger the bug.
|
||||
The clearer this is, the faster we can fix it.
|
||||
placeholder: |
|
||||
1. Log in to Stoat on the web client.
|
||||
2. Open a direct message conversation.
|
||||
3. Ask the other person to send a message.
|
||||
4. Observe that the message does not appear without reloading.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Expected behavior ────────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What should have happened?
|
||||
placeholder: "New messages should appear in real time without a page reload."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Client / platform ────────────────────────────────────────────────────────
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Client / platform
|
||||
description: Which Stoat client are you using?
|
||||
options:
|
||||
- Web (browser)
|
||||
- Stoat for Desktop
|
||||
- Android
|
||||
- iOS
|
||||
- Self-hosted server
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Client version ───────────────────────────────────────────────────────────
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Client version
|
||||
description: >
|
||||
Find this in **Settings**. For the web client, include the build
|
||||
number shown in the footer or About page.
|
||||
placeholder: "e.g. Stoat for Desktop 1.3.0 or 0.2.1 (2025-10-10)"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── OS / browser details ─────────────────────────────────────────────────────
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: OS / browser details
|
||||
description: Your operating system and, for web bugs, your browser and its version.
|
||||
placeholder: "e.g. Windows 11, Chrome 121 or macOS 14.3, Stoat for Desktop 1.3.0 or Android 14"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Reproducibility ──────────────────────────────────────────────────────────
|
||||
- type: dropdown
|
||||
id: reproducibility
|
||||
attributes:
|
||||
label: Reproducibility
|
||||
description: How consistently does this bug occur?
|
||||
options:
|
||||
- Always
|
||||
- Usually
|
||||
- Rarely
|
||||
- Only once
|
||||
- Unknown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Screenshots or video ─────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots or video
|
||||
description: Paste images or drag-and-drop a screen recording here. GitHub accepts common image and video formats.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
# ── Additional context ───────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: >
|
||||
Anything else that might help: workarounds you found, links to related issues,
|
||||
self-hosted configuration details (Redis, MongoDB, LiveKit, SMTP), network environment, etc.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
# ── Closing note ─────────────────────────────────────────────────────────────
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
---
|
||||
A maintainer will review your report. Please watch for follow-up questions — issues
|
||||
that go unanswered are harder to resolve and may be closed after 30 days of inactivity.
|
||||
36
.github/workflows/cla.yml
vendored
36
.github/workflows/cla.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened,closed,synchronize]
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.1.3-beta
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PAT }}
|
||||
with:
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-document: 'https://github.com/revoltchat/cla/blob/master/CLA.md' # e.g. a CLA or a DCO document
|
||||
# branch should not be protected
|
||||
branch: 'master'
|
||||
allowlist: insertish,bot*
|
||||
|
||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||
remote-organization-name: revoltchat
|
||||
remote-repository-name: cla
|
||||
create-file-commit-message: 'cla(create): creating file for storing CLA Signatures'
|
||||
signed-commit-message: 'cla(sign): $contributorName has signed the CLA in #$pullRequestNo'
|
||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
217
.github/workflows/docker.yaml
vendored
217
.github/workflows/docker.yaml
vendored
@@ -2,20 +2,9 @@ name: Docker Test & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
@@ -24,10 +13,15 @@ permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
base:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build base image
|
||||
name: Test base image build
|
||||
runs-on: arc-runner-set
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
@@ -35,33 +29,21 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# Authenticate with GHCR
|
||||
- name: Login to Github Container Registry
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Build the image
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: false
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
cache-from: type=gha,scope=buildx-base-multi-arch
|
||||
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
|
||||
|
||||
publish:
|
||||
needs: [base]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: arc-runner-set
|
||||
if: github.event_name != 'pull_request'
|
||||
strategy:
|
||||
matrix:
|
||||
project: [delta, bonfire]
|
||||
name: Build ${{ matrix.project }} image
|
||||
name: Publish Docker images
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
@@ -70,12 +52,6 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# Authenticate with Docker Hub and GHCR
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -83,42 +59,163 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Resolve the correct project
|
||||
- uses: kanga333/variable-mapper@master
|
||||
id: export
|
||||
# Build the image
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
key: "${{ matrix.project }}"
|
||||
map: |
|
||||
{
|
||||
"delta": {
|
||||
"path": "crates/delta",
|
||||
"tag": "${{ github.repository_owner }}/server"
|
||||
},
|
||||
"bonfire": {
|
||||
"path": "crates/bonfire",
|
||||
"tag": "${{ github.repository_owner }}/bonfire"
|
||||
}
|
||||
}
|
||||
export_to: output
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
|
||||
# Configure metadata
|
||||
# stoatchat/api
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
id: meta-delta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/${{ steps.export.outputs.tag }}
|
||||
ghcr.io/${{ steps.export.outputs.tag }}
|
||||
|
||||
# Build crate image
|
||||
ghcr.io/stoatchat/api
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ${{ steps.export.outputs.path }}/Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
file: crates/delta/Dockerfile
|
||||
tags: ${{ steps.meta-delta.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta-delta.outputs.labels }}
|
||||
|
||||
# stoatchat/events
|
||||
- name: Docker meta
|
||||
id: meta-bonfire
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/events
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/bonfire/Dockerfile
|
||||
tags: ${{ steps.meta-bonfire.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-bonfire.outputs.labels }}
|
||||
|
||||
# stoatchat/file-server
|
||||
- name: Docker meta
|
||||
id: meta-autumn
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/file-server
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/services/autumn/Dockerfile
|
||||
tags: ${{ steps.meta-autumn.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-autumn.outputs.labels }}
|
||||
|
||||
# stoatchat/proxy
|
||||
- name: Docker meta
|
||||
id: meta-january
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/proxy
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/services/january/Dockerfile
|
||||
tags: ${{ steps.meta-january.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-january.outputs.labels }}
|
||||
|
||||
# stoatchat/gifbox
|
||||
- name: Docker meta
|
||||
id: meta-gifbox
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/gifbox
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/services/gifbox/Dockerfile
|
||||
tags: ${{ steps.meta-gifbox.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-gifbox.outputs.labels }}
|
||||
|
||||
# stoatchat/crond
|
||||
- name: Docker meta
|
||||
id: meta-crond
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/crond
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/daemons/crond/Dockerfile
|
||||
tags: ${{ steps.meta-crond.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-crond.outputs.labels }}
|
||||
|
||||
# stoatchat/pushd
|
||||
- name: Docker meta
|
||||
id: meta-pushd
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/pushd
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/daemons/pushd/Dockerfile
|
||||
tags: ${{ steps.meta-pushd.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-pushd.outputs.labels }}
|
||||
|
||||
# stoatchat/voice-ingress
|
||||
- name: Docker meta
|
||||
id: meta-voice-ingress
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/voice-ingress
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/daemons/voice-ingress/Dockerfile
|
||||
tags: ${{ steps.meta-voice-ingress.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-voice-ingress.outputs.labels }}
|
||||
|
||||
23
.github/workflows/docs-test.yml
vendored
Normal file
23
.github/workflows/docs-test.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Documentation (test)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-deploy:
|
||||
name: Test deployment
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./docs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: mise docs:build
|
||||
48
.github/workflows/docs.yml
vendored
Normal file
48
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docusaurus
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./docs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: mise docs:build
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./docs/build
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
23
.github/workflows/git-town.yml
vendored
Normal file
23
.github/workflows/git-town.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: Git Town
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
git-town:
|
||||
name: Display the branch stack
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !startsWith(github.head_ref, 'release-please--') }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: stoatchat/action-git-town@4bc5c942e4603bffa0806b51d5fe5f0bc5deb0ac
|
||||
continue-on-error: true
|
||||
24
.github/workflows/publish-crates.yml
vendored
Normal file
24
.github/workflows/publish-crates.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Publish Crates
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish Crates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
run: mise publish --workspace
|
||||
63
.github/workflows/release-please.yml
vendored
Normal file
63
.github/workflows/release-please.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Release Please
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main] # updates/opens the release PR when commits land on main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: release-please
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
name: Release Please
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_created: ${{ steps.rp.outputs.release_created }}
|
||||
tag_name: ${{ steps.rp.outputs.tag_name }}
|
||||
steps:
|
||||
- id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_STOAT_RELEASE_APP_ID }}
|
||||
private-key: ${{ secrets.GH_STOAT_RELEASE_APP_PRIVATE_KEY }}
|
||||
- id: rp
|
||||
uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
config-file: release-please-config.json
|
||||
|
||||
- name: Install latest stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
- name: Update Cargo.lock
|
||||
if: ${{ steps.rp.outputs.prs_created == 'true' || steps.rp.outputs.prs_updated == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
RP_PR_OUTPUT_JSON: ${{ steps.rp.outputs.prs }}
|
||||
run: |
|
||||
PR_NUMBER=$(echo "$RP_PR_OUTPUT_JSON" | jq -r '.[0].number')
|
||||
gh pr checkout "$PR_NUMBER"
|
||||
|
||||
cargo update -w
|
||||
|
||||
if git diff --quiet Cargo.lock; then
|
||||
echo "No changes to Cargo.lock"
|
||||
else
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add Cargo.lock
|
||||
git commit -s -m "chore: update Cargo.lock"
|
||||
git push
|
||||
fi
|
||||
26
.github/workflows/release-webhook.yml
vendored
Normal file
26
.github/workflows/release-webhook.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: Release Webhook
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
release-webhook:
|
||||
name: Send Release Webhook
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Send release notification webhook
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
WEBHOOK_URL: ${{ secrets.STOAT_WEBHOOK_UPDATES_URL }}
|
||||
run: |
|
||||
RELEASE_URL="https://github.com/${REPOSITORY}/releases/tag/${TAG_NAME}"
|
||||
curl -X POST "$WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"content\": \"$RELEASE_URL\"}"
|
||||
90
.github/workflows/rust.yaml
vendored
90
.github/workflows/rust.yaml
vendored
@@ -2,8 +2,12 @@ name: Rust build, test, and generate specification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -11,71 +15,69 @@ env:
|
||||
jobs:
|
||||
check:
|
||||
name: Rust project
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: arc-runner-set
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install latest stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
# Using our own runners for now:
|
||||
# - name: Free up disk space
|
||||
# run: |
|
||||
# sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
command: build
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run services in background
|
||||
run: |
|
||||
docker-compose -f docker-compose.db.yml up -d
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
- name: Run cargo test (with MongoDB)
|
||||
uses: actions-rs/cargo@v1
|
||||
- run: mise build
|
||||
- run: mise docker:start
|
||||
|
||||
- name: Reference Test
|
||||
env:
|
||||
MONGODB: mongodb://localhost
|
||||
with:
|
||||
command: test
|
||||
|
||||
- name: Copy .env.example
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cp .env.example .env
|
||||
mise test
|
||||
|
||||
- name: MongoDB Test
|
||||
env:
|
||||
TEST_DB: MONGODB
|
||||
MONGODB: mongodb://localhost
|
||||
run: |
|
||||
mise test
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo run --bin revolt-delta &
|
||||
mise build --bin revolt-delta && (mise service:api &)
|
||||
|
||||
- name: Wait for API to go up
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
uses: nev7n/wait_for_response@v1
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: nev7n/wait_for_response@7fef3c1a6e8939d0b09062f14fec50d3c5d15fa1 # v1.0.1
|
||||
with:
|
||||
url: "http://localhost:8000/"
|
||||
url: "http://localhost:14702/"
|
||||
|
||||
- name: Checkout API repository
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
uses: actions/checkout@v3
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: revoltchat/api
|
||||
repository: stoatchat/javascript-client-api
|
||||
path: api
|
||||
token: ${{ secrets.PAT }}
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY_JAVASCRIPT_CLIENT_API }}
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
run: curl http://localhost:14702/openapi.json -o api/OpenAPI.json
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
||||
with:
|
||||
cwd: "api"
|
||||
add: "*.json"
|
||||
author_name: Revolt CI
|
||||
author_email: revolt-ci@users.noreply.github.com
|
||||
author_name: Stoat CI
|
||||
author_email: stoat-ci@users.noreply.github.com
|
||||
message: "chore: generate OpenAPI specification"
|
||||
|
||||
54
.github/workflows/triage_issue.yml
vendored
54
.github/workflows/triage_issue.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: Add Issue to Board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
79
.github/workflows/triage_pr.yml
vendored
79
.github/workflows/triage_pr.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation (
|
||||
$project: ID!
|
||||
$item: ID!
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: {
|
||||
singleSelectOptionId: $status_value
|
||||
}
|
||||
}) {
|
||||
projectV2Item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
23
.github/workflows/validate-pr-title.yml
vendored
Normal file
23
.github/workflows/validate-pr-title.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: "Lint PR"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,6 +1,15 @@
|
||||
Rocket.toml
|
||||
Revolt.*.toml
|
||||
compose.override.yml
|
||||
|
||||
target
|
||||
.data
|
||||
.env
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
.vercel
|
||||
.DS_Store
|
||||
|
||||
livekit.yml
|
||||
.idea
|
||||
19
.mise/config.toml
Normal file
19
.mise/config.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[tools]
|
||||
node = "25.4.0"
|
||||
pnpm = "10.28.1"
|
||||
|
||||
gh = "2.25.0"
|
||||
|
||||
rust = "1.92.0"
|
||||
"cargo:cargo-nextest" = "0.9.122"
|
||||
|
||||
"github:git-town/git-town" = "22.4.0"
|
||||
|
||||
[settings]
|
||||
experimental = true
|
||||
idiomatic_version_file_enable_tools = ["rust"]
|
||||
|
||||
[tasks.start]
|
||||
description = "Run all services"
|
||||
depends = ["docker:start", "build"]
|
||||
run = [{ task = "service:*" }]
|
||||
5
.mise/tasks/build
Executable file
5
.mise/tasks/build
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Build project"
|
||||
set -e
|
||||
|
||||
cargo build "$@"
|
||||
5
.mise/tasks/check
Executable file
5
.mise/tasks/check
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Check project with clippy"
|
||||
set -e
|
||||
|
||||
cargo clippy
|
||||
5
.mise/tasks/docker/start
Executable file
5
.mise/tasks/docker/start
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Start Docker containers"
|
||||
set -e
|
||||
|
||||
docker compose up -d
|
||||
5
.mise/tasks/docker/stop
Executable file
5
.mise/tasks/docker/stop
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Stop Docker containers"
|
||||
set -e
|
||||
|
||||
docker compose down
|
||||
7
.mise/tasks/docs/_default
Executable file
7
.mise/tasks/docs/_default
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Start the Stoat Developers website"
|
||||
#MISE depends=["docs:install"]
|
||||
#MISE dir="{{config_root}}/docs"
|
||||
set -e
|
||||
|
||||
pnpm build
|
||||
7
.mise/tasks/docs/build
Executable file
7
.mise/tasks/docs/build
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Build the Stoat Developers website"
|
||||
#MISE depends=["docs:install"]
|
||||
#MISE dir="{{config_root}}/docs"
|
||||
set -e
|
||||
|
||||
pnpm build
|
||||
6
.mise/tasks/docs/install
Executable file
6
.mise/tasks/docs/install
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Install dependencies for docs site"
|
||||
#MISE dir="{{config_root}}/docs"
|
||||
set -e
|
||||
|
||||
pnpm i --frozen-lockfile
|
||||
5
.mise/tasks/publish
Executable file
5
.mise/tasks/publish
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Publish project"
|
||||
set -e
|
||||
|
||||
cargo publish "$@"
|
||||
5
.mise/tasks/service/api
Executable file
5
.mise/tasks/service/api
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run API server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-delta
|
||||
5
.mise/tasks/service/crond
Executable file
5
.mise/tasks/service/crond
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run cron daemon"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-crond
|
||||
5
.mise/tasks/service/events
Executable file
5
.mise/tasks/service/events
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run events server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-bonfire
|
||||
5
.mise/tasks/service/files
Executable file
5
.mise/tasks/service/files
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run file server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-autumn
|
||||
5
.mise/tasks/service/gifbox
Executable file
5
.mise/tasks/service/gifbox
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run GIF proxy server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-gifbox
|
||||
5
.mise/tasks/service/proxy
Executable file
5
.mise/tasks/service/proxy
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run proxy server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-january
|
||||
5
.mise/tasks/service/pushd
Executable file
5
.mise/tasks/service/pushd
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run push daemon"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-pushd
|
||||
8
.mise/tasks/test
Executable file
8
.mise/tasks/test
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Test project"
|
||||
set -e
|
||||
|
||||
: "${TEST_DB:=REFERENCE}"
|
||||
export TEST_DB
|
||||
|
||||
cargo nextest run
|
||||
3
.release-please-manifest.json
Normal file
3
.release-please-manifest.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "0.11.5"
|
||||
}
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy"
|
||||
}
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"nixEnvSelector.suggestion": false,
|
||||
"nixEnvSelector.nixFile": "${workspaceFolder}/default.nix"
|
||||
}
|
||||
|
||||
149
CHANGELOG.md
Normal file
149
CHANGELOG.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# Changelog
|
||||
|
||||
## [0.11.5](https://github.com/stoatchat/stoatchat/compare/v0.11.4...v0.11.5) (2026-02-17)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* disable user update events ([#593](https://github.com/stoatchat/stoatchat/issues/593)) ([1c98ead](https://github.com/stoatchat/stoatchat/commit/1c98ead69579b4700be0b51c9020bb8402336cc6))
|
||||
|
||||
## [0.11.4](https://github.com/stoatchat/stoatchat/compare/v0.11.3...v0.11.4) (2026-02-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add separate config option for redis events replica url ([#590](https://github.com/stoatchat/stoatchat/issues/590)) ([a75e4ea](https://github.com/stoatchat/stoatchat/commit/a75e4eabfc4b34aba7620c82ba77558a32d9e10a))
|
||||
|
||||
## [0.11.3](https://github.com/stoatchat/stoatchat/compare/v0.11.2...v0.11.3) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cut presence traffic too while we engineer a new events architecture ([#561](https://github.com/stoatchat/stoatchat/issues/561)) ([1f8ea96](https://github.com/stoatchat/stoatchat/commit/1f8ea963ad742f693f405e6438f1c343c81e6579))
|
||||
|
||||
## [0.11.2](https://github.com/stoatchat/stoatchat/compare/v0.11.1...v0.11.2) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cut events traffic while we engineer a new events architecture ([#559](https://github.com/stoatchat/stoatchat/issues/559)) ([a11986b](https://github.com/stoatchat/stoatchat/commit/a11986ba1ad16b672ff1080913a684567d88adbb))
|
||||
|
||||
## [0.11.1](https://github.com/stoatchat/stoatchat/compare/v0.11.0...v0.11.1) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bots in multiple voice channel logic ([#544](https://github.com/stoatchat/stoatchat/issues/544)) ([94cb916](https://github.com/stoatchat/stoatchat/commit/94cb916231b9b8befb2e94065917ff40815bec52))
|
||||
|
||||
## [0.11.0](https://github.com/stoatchat/stoatchat/compare/v0.10.3...v0.11.0) (2026-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* appeal to the almighty Spamhaus ([#524](https://github.com/stoatchat/stoatchat/issues/524)) ([5132270](https://github.com/stoatchat/stoatchat/commit/5132270f2edd6df25ce414daa42ed1b2aa6fa7a9))
|
||||
|
||||
## [0.10.3](https://github.com/stoatchat/stoatchat/compare/v0.10.2...v0.10.3) (2026-02-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update `Revolt` -> `Stoat` in email titles/desc. ([#508](https://github.com/stoatchat/stoatchat/issues/508)) ([84483ce](https://github.com/stoatchat/stoatchat/commit/84483cee7af3e5dfa16f7fe13e334c4d9f5abd60))
|
||||
|
||||
## [0.10.2](https://github.com/stoatchat/stoatchat/compare/v0.10.1...v0.10.2) (2026-01-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* thumbnailification requires rgb8/rgba8 ([#505](https://github.com/stoatchat/stoatchat/issues/505)) ([413aa04](https://github.com/stoatchat/stoatchat/commit/413aa04dcaf8bff3935ed1e5f31432e11a03ce6f))
|
||||
|
||||
## [0.10.1](https://github.com/stoatchat/stoatchat/compare/v0.10.0...v0.10.1) (2026-01-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use Rust 1.92.0 for Docker build ([#503](https://github.com/stoatchat/stoatchat/issues/503)) ([98da8a2](https://github.com/stoatchat/stoatchat/commit/98da8a28a0aa2fee4e8eee1d86bd7c49e3187477))
|
||||
|
||||
## [0.10.0](https://github.com/stoatchat/stoatchat/compare/v0.9.4...v0.10.0) (2026-01-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow kicking members from voice channels ([#495](https://github.com/stoatchat/stoatchat/issues/495)) ([0dc5442](https://github.com/stoatchat/stoatchat/commit/0dc544249825a49c793309edee5ec1838458a6da))
|
||||
* repository architecture for files crate w. added tests ([#498](https://github.com/stoatchat/stoatchat/issues/498)) ([01ded20](https://github.com/stoatchat/stoatchat/commit/01ded209c62208fc906d6aab9b08c04e860e10ef))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* expose ratelimit headers via cors ([#496](https://github.com/stoatchat/stoatchat/issues/496)) ([a1a2125](https://github.com/stoatchat/stoatchat/commit/a1a21252d0ad58937e41f16e5fb86f96bebd2a51))
|
||||
|
||||
## [0.9.4](https://github.com/stoatchat/stoatchat/compare/v0.9.3...v0.9.4) (2026-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* checkout repo. before bumping lock ([#490](https://github.com/stoatchat/stoatchat/issues/490)) ([b2da2a8](https://github.com/stoatchat/stoatchat/commit/b2da2a858787853be43136fd526a0bd72baf78ef))
|
||||
* persist credentials for git repo ([#492](https://github.com/stoatchat/stoatchat/issues/492)) ([c674a9f](https://github.com/stoatchat/stoatchat/commit/c674a9fd4e0abbd51569870e4b38074d4a1de03c))
|
||||
|
||||
## [0.9.3](https://github.com/stoatchat/stoatchat/compare/v0.9.2...v0.9.3) (2026-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pipeline fixes ([#487](https://github.com/stoatchat/stoatchat/issues/487)) ([aeeafeb](https://github.com/stoatchat/stoatchat/commit/aeeafebefc36a43a656cf797c9251ca50292733c))
|
||||
|
||||
## [0.9.2](https://github.com/stoatchat/stoatchat/compare/v0.9.1...v0.9.2) (2026-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disable publish for services ([#485](https://github.com/stoatchat/stoatchat/issues/485)) ([d13609c](https://github.com/stoatchat/stoatchat/commit/d13609c37279d6a40445dcd99564e5c3dd03bac1))
|
||||
|
||||
## [0.9.1](https://github.com/stoatchat/stoatchat/compare/v0.9.0...v0.9.1) (2026-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** pipeline fixes (marked as fix to force release) ([#483](https://github.com/stoatchat/stoatchat/issues/483)) ([303e52b](https://github.com/stoatchat/stoatchat/commit/303e52b476585eea81c33837f1b01506ce387684))
|
||||
|
||||
## [0.9.0](https://github.com/stoatchat/stoatchat/compare/v0.8.8...v0.9.0) (2026-01-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add id field to role ([#470](https://github.com/stoatchat/stoatchat/issues/470)) ([2afea56](https://github.com/stoatchat/stoatchat/commit/2afea56e56017f02de98e67316b4457568ad5b26))
|
||||
* add ratelimits to gifbox ([1542047](https://github.com/stoatchat/stoatchat/commit/154204742d21cbeff6e2577b00f50b495ea44631))
|
||||
* include groups and dms in fetch mutuals ([caa8607](https://github.com/stoatchat/stoatchat/commit/caa86074680d46223cebc20f41e9c91c41ec825d))
|
||||
* include member payload in ServerMemberJoin event ([480f210](https://github.com/stoatchat/stoatchat/commit/480f210ce85271e13d1dac58a5dae08de108579d))
|
||||
* initial work on tenor gif searching ([b0c977b](https://github.com/stoatchat/stoatchat/commit/b0c977b324b8144c1152589546eb8fec5954c3e7))
|
||||
* make message lexer use unowned string ([1561481](https://github.com/stoatchat/stoatchat/commit/1561481eb4cdc0f385fbf0a81e4950408050e11f))
|
||||
* ready payload field customisation ([db57706](https://github.com/stoatchat/stoatchat/commit/db577067948f13e830b5fb773034e9713a1abaff))
|
||||
* require auth for search ([b5cd5e3](https://github.com/stoatchat/stoatchat/commit/b5cd5e30ef7d5e56e8964fb7c543965fa6bf5a4a))
|
||||
* trending and categories routes ([5885e06](https://github.com/stoatchat/stoatchat/commit/5885e067a627b8fff1c8ce2bf9e852ff8cf3f07a))
|
||||
* voice chats v2 ([#414](https://github.com/stoatchat/stoatchat/issues/414)) ([d567155](https://github.com/stoatchat/stoatchat/commit/d567155f124e4da74115b1a8f810062f7c6559d9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add license to revolt-parser ([5335124](https://github.com/stoatchat/stoatchat/commit/53351243064cac8d499dd74284be73928fa78a43))
|
||||
* allow for disabling default features ([65fbd36](https://github.com/stoatchat/stoatchat/commit/65fbd3662462aed1333b79e59155fa6377e83fcc))
|
||||
* apple music to use original url instead of metadata url ([bfe4018](https://github.com/stoatchat/stoatchat/commit/bfe4018e436a4075bae780dd4d35a9b58315e12f))
|
||||
* apply uname fix to january and autumn ([8f9015a](https://github.com/stoatchat/stoatchat/commit/8f9015a6ff181d208d9269ab8691bd417d39811a))
|
||||
* **ci:** publish images under stoatchat and remove docker hub ([d65c1a1](https://github.com/stoatchat/stoatchat/commit/d65c1a1ab3bdc7e5684b03f280af77d881661a3d))
|
||||
* correct miniz_oxide in lockfile ([#478](https://github.com/stoatchat/stoatchat/issues/478)) ([5d27a91](https://github.com/stoatchat/stoatchat/commit/5d27a91e901dd2ea3e860aeaed8468db6c5f3214))
|
||||
* correct shebang for try-tag-and-release ([050ba16](https://github.com/stoatchat/stoatchat/commit/050ba16d4adad5d0fb247867aa3e94e3d42bd12d))
|
||||
* correct string_cache in lockfile ([#479](https://github.com/stoatchat/stoatchat/issues/479)) ([0b178fc](https://github.com/stoatchat/stoatchat/commit/0b178fc791583064bf9ca94b1d39b42d021e1d79))
|
||||
* don't remove timeouts when a member leaves a server ([#409](https://github.com/stoatchat/stoatchat/issues/409)) ([e635bc2](https://github.com/stoatchat/stoatchat/commit/e635bc23ec857d648d5705e1a3875d7bc3402b0d))
|
||||
* don't update the same field while trying to remove it ([f4ee35f](https://github.com/stoatchat/stoatchat/commit/f4ee35fb093ca49f0a64ff4b17fd61587df28145)), closes [#392](https://github.com/stoatchat/stoatchat/issues/392)
|
||||
* github webhook incorrect payload and formatting ([#468](https://github.com/stoatchat/stoatchat/issues/468)) ([dc9c82a](https://github.com/stoatchat/stoatchat/commit/dc9c82aa4e9667ea6639256c65ac8de37a24d1f7))
|
||||
* implement Serialize to ClientMessage ([dea0f67](https://github.com/stoatchat/stoatchat/commit/dea0f675dde7a63c7a59b38d469f878b7a8a3af4))
|
||||
* newly created roles should be ranked the lowest ([947eb15](https://github.com/stoatchat/stoatchat/commit/947eb15771ed6785b3dcd16c354c03ded5e4cbe0))
|
||||
* permit empty `remove` array in edit requests ([6ad3da5](https://github.com/stoatchat/stoatchat/commit/6ad3da5f35f989a2e7d8e29718b98374248e76af))
|
||||
* preserve order of replies in message ([#447](https://github.com/stoatchat/stoatchat/issues/447)) ([657a3f0](https://github.com/stoatchat/stoatchat/commit/657a3f08e5d652814bbf0647e089ed9ebb139bbf))
|
||||
* prevent timing out members which have TimeoutMembers permission ([e36fc97](https://github.com/stoatchat/stoatchat/commit/e36fc9738bac0de4f3fcbccba521f1e3754f7ae7))
|
||||
* relax settings name regex ([3a34159](https://github.com/stoatchat/stoatchat/commit/3a3415915f0d0fdce1499d47a2b7fa097f5946ea))
|
||||
* remove authentication tag bytes from attachment download ([32e6600](https://github.com/stoatchat/stoatchat/commit/32e6600272b885c595c094f0bc69459250220dcb))
|
||||
* rename openapi operation ids ([6048587](https://github.com/stoatchat/stoatchat/commit/6048587d348fbca0dc3a9b47690c56df8fece576)), closes [#406](https://github.com/stoatchat/stoatchat/issues/406)
|
||||
* respond with 201 if no body in requests ([#465](https://github.com/stoatchat/stoatchat/issues/465)) ([24fedf8](https://github.com/stoatchat/stoatchat/commit/24fedf8c4d9cd3160bdec97aa451520f8beaa739))
|
||||
* swap to using reqwest for query building ([38dd4d1](https://github.com/stoatchat/stoatchat/commit/38dd4d10797b3e6e397fc219e818f379bdff19f2))
|
||||
* use `trust_cloudflare` config value instead of env var ([cc7a796](https://github.com/stoatchat/stoatchat/commit/cc7a7962a882e1627fcd0bc75858a017415e8cfc))
|
||||
* use our own result types instead of tenors types ([a92152d](https://github.com/stoatchat/stoatchat/commit/a92152d86da136997817e797c7af8e38731cdde8))
|
||||
9062
Cargo.lock
generated
9062
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
50
Cargo.toml
@@ -1,6 +1,50 @@
|
||||
[workspace]
|
||||
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"crates/delta",
|
||||
"crates/bonfire",
|
||||
"crates/core/*",
|
||||
"crates/services/*",
|
||||
"crates/daemons/*",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||
redis = { git = "https://github.com/insertish/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
|
||||
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
|
||||
#authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
|
||||
#rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[workspace.dependencies]
|
||||
# Async
|
||||
async-trait = "0.1.89"
|
||||
tokio = { version = "1.49.0", features = ["macros", "rt"] }
|
||||
|
||||
# Error Handling
|
||||
anyhow = "1.0.100"
|
||||
thiserror = "2.0.18"
|
||||
|
||||
# Other Utilities
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
|
||||
# Axum (HTTP server)
|
||||
axum-macros = "0.4.1"
|
||||
axum_typed_multipart = "0.12.1"
|
||||
axum = { version = "0.7.5", features = ["multipart"] }
|
||||
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
|
||||
|
||||
# Image Processing
|
||||
jxl-oxide = { version = "0.12.5", features = ["image"] }
|
||||
image = "0.25.9"
|
||||
|
||||
# OpenTelemetry
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", features = [
|
||||
"env-filter",
|
||||
] } # consider https://crates.io/crates/better-tracing
|
||||
opentelemetry = { version = "0.31.0", features = ["logs"] }
|
||||
opentelemetry_sdk = { version = "0.31.0", features = ["logs"] }
|
||||
opentelemetry-otlp = { version = "0.31.0", features = ["logs"] }
|
||||
opentelemetry-appender-tracing = { version = "0.31.1" }
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.92.0-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
@@ -19,12 +19,22 @@ RUN sh /tmp/build-image-layer.sh tools
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/quark/Cargo.toml ./crates/quark/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/files/Cargo.toml ./crates/core/files/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
COPY crates/core/parser/Cargo.toml ./crates/core/parser/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
|
||||
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
|
||||
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
||||
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
|
||||
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
|
||||
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
|
||||
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
|
||||
38
Dockerfile.useCurrentArch
Normal file
38
Dockerfile.useCurrentArch
Normal file
@@ -0,0 +1,38 @@
|
||||
# Build Stage
|
||||
FROM rust:1.92.0-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
# Install build requirements
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
make \
|
||||
pkg-config \
|
||||
libssl-dev
|
||||
COPY scripts/build-image-layer.sh /tmp/
|
||||
|
||||
# Build all dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/files/Cargo.toml ./crates/core/files/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
COPY crates/core/parser/Cargo.toml ./crates/core/parser/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
|
||||
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
|
||||
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
||||
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
|
||||
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
|
||||
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
|
||||
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
COPY crates ./crates
|
||||
RUN sh /tmp/build-image-layer.sh apps
|
||||
5
LICENSE
5
LICENSE
@@ -1,4 +1,7 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
With the exception of crates that specify their own LICENSE file,
|
||||
the following license applies to the source code of this project.
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
|
||||
224
README.md
224
README.md
@@ -1,31 +1,215 @@
|
||||
# Revolt Backend
|
||||
<div align="center">
|
||||
<h1>
|
||||
Stoat Backend
|
||||
|
||||
[](https://github.com/stoatchat/stoatchat/stargazers)
|
||||
[](https://github.com/stoatchat/stoatchat/network/members)
|
||||
[](https://github.com/stoatchat/stoatchat/pulls)
|
||||
[](https://github.com/stoatchat/stoatchat/issues)
|
||||
[](https://github.com/stoatchat/stoatchat/graphs/contributors)
|
||||
[](https://github.com/stoatchat/stoatchat/blob/main/LICENSE)
|
||||
</h1>
|
||||
The services and libraries that power the Stoat service.<br/>
|
||||
<br/>
|
||||
|
||||
This is a monorepo for the Revolt backend.
|
||||
| Crate | Path | Description | |
|
||||
| ------------------ | -------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |     |
|
||||
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |     |
|
||||
| `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines |     |
|
||||
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |     |
|
||||
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |     |
|
||||
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |     |
|
||||
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |     |
|
||||
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service |     |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |  |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |  |
|
||||
| `services/january` | [crates/services/january](crates/services/january) | Proxy server |  |
|
||||
| `services/gifbox` | [crates/services/gifbox](crates/services/gifbox) | Tenor proxy server |  |
|
||||
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server |  |
|
||||
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server |  |
|
||||
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server |  |
|
||||
|
||||
| Crate | Path | Description |
|
||||
| ---------------- | ---------------------------------------------- | ------------------------------------ |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
|
||||
| `quark` | [crates/quark](crates/quark) | Models and logic |
|
||||
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer |
|
||||
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer |
|
||||
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |-->
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
## Resources
|
||||
Rust 1.86.0 or higher.
|
||||
|
||||
### Revolt
|
||||
## Development Guide
|
||||
|
||||
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
|
||||
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
|
||||
- [Contribution Guide](https://developers.revolt.chat/contributing)
|
||||
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.stoat.chat/developing/contrib/) and the [technical documentation for this project](https://developers.stoat.chat/).
|
||||
|
||||
## Contributing
|
||||
Before getting started, you'll want to install:
|
||||
|
||||
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
|
||||
Please note that a pull request should only take care of one issue so that we can review it quickly.
|
||||
- mise
|
||||
- Docker
|
||||
- Git
|
||||
- mold (optional, faster compilation)
|
||||
|
||||
> A **default.nix** is available for Nix users!
|
||||
> Run `nix-shell` to activate mise.
|
||||
|
||||
As a heads-up, the development environment uses the following ports:
|
||||
|
||||
| Service | Port |
|
||||
| ------------------------- | :------------: |
|
||||
| MongoDB | 27017 |
|
||||
| Redis | 6379 |
|
||||
| MinIO | 14009 |
|
||||
| Maildev | 14025<br>14080 |
|
||||
| Revolt Web App | 14701 |
|
||||
| RabbitMQ | 5672<br>15672 |
|
||||
| `crates/delta` | 14702 |
|
||||
| `crates/bonfire` | 14703 |
|
||||
| `crates/services/autumn` | 14704 |
|
||||
| `crates/services/january` | 14705 |
|
||||
| `crates/services/gifbox` | 14706 |
|
||||
|
||||
Now you can clone and build the project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/stoatchat/stoatchat stoat-backend
|
||||
cd stoat-backend
|
||||
mise install
|
||||
mise build
|
||||
```
|
||||
|
||||
A default configuration `Revolt.toml` is present in this project that is suited for development.
|
||||
|
||||
If you'd like to change anything, create a `Revolt.overrides.toml` file and specify relevant variables.
|
||||
|
||||
> [!TIP]
|
||||
> Use Sentry to catch unexpected service errors:
|
||||
>
|
||||
> ```toml
|
||||
> # Revolt.overrides.toml
|
||||
> [sentry]
|
||||
> api = "https://abc@your.sentry/1"
|
||||
> events = "https://abc@your.sentry/1"
|
||||
> files = "https://abc@your.sentry/1"
|
||||
> proxy = "https://abc@your.sentry/1"
|
||||
> ```
|
||||
|
||||
> [!TIP]
|
||||
> If you have port conflicts on common services, you can try the following:
|
||||
>
|
||||
> ```yaml
|
||||
> # compose.override.yml
|
||||
> services:
|
||||
> redis:
|
||||
> ports: !override
|
||||
> - "14079:6379"
|
||||
>
|
||||
> database:
|
||||
> ports: !override
|
||||
> - "14017:27017"
|
||||
>
|
||||
> rabbit:
|
||||
> ports: !override
|
||||
> - "14072:5672"
|
||||
> - "14672:15672"
|
||||
> ```
|
||||
>
|
||||
> And corresponding Revolt configuration:
|
||||
>
|
||||
> ```toml
|
||||
> # Revolt.overrides.toml
|
||||
> # and Revolt.test-overrides.toml
|
||||
> [database]
|
||||
> mongodb = "mongodb://127.0.0.1:14017"
|
||||
> redis = "redis://127.0.0.1:14079/"
|
||||
>
|
||||
> [rabbit]
|
||||
> port = 14072
|
||||
> ```
|
||||
|
||||
Then continue:
|
||||
|
||||
```bash
|
||||
# start other necessary services
|
||||
docker compose up -d
|
||||
|
||||
# run the API server
|
||||
cargo run --bin revolt-delta
|
||||
# run the events server
|
||||
cargo run --bin revolt-bonfire
|
||||
# run the file server
|
||||
cargo run --bin revolt-autumn
|
||||
# run the proxy server
|
||||
cargo run --bin revolt-january
|
||||
# run the tenor proxy
|
||||
cargo run --bin revolt-gifbox
|
||||
# run the push daemon (not usually needed in regular development)
|
||||
cargo run --bin revolt-pushd
|
||||
|
||||
# hint:
|
||||
# mold -run <cargo build, cargo run, etc...>
|
||||
# mold -run ./scripts/start.sh
|
||||
```
|
||||
|
||||
You can start a web client by doing the following:
|
||||
|
||||
```bash
|
||||
# if you do not have yarn yet and have a modern Node.js:
|
||||
corepack enable
|
||||
|
||||
# clone the web client and run it:
|
||||
git clone --recursive https://github.com/stoatchat/for-web stoat-web
|
||||
cd stoat-web
|
||||
# refer to stoat-web/README.md for startup, creating an account and loging in
|
||||
```
|
||||
|
||||
When signing up, go to http://localhost:14080 to find confirmation/password reset emails.
|
||||
|
||||
## Deployment Guide
|
||||
|
||||
### Cutting new crate releases
|
||||
|
||||
Begin by bumping crate versions:
|
||||
|
||||
```bash
|
||||
just patch # 0.0.X
|
||||
just minor # 0.X.0
|
||||
just major # X.0.0
|
||||
```
|
||||
|
||||
Then commit the changes to package files.
|
||||
|
||||
Proceed to publish all the new crates:
|
||||
|
||||
```bash
|
||||
just publish
|
||||
```
|
||||
|
||||
### Cutting new binary releases
|
||||
|
||||
Tag and push a new release by running:
|
||||
|
||||
```bash
|
||||
just release
|
||||
```
|
||||
|
||||
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/stoatchat/stoatchat/releases/new) to create a changelog.
|
||||
|
||||
## Testing
|
||||
|
||||
First, start the required services:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.db.yml up -d
|
||||
```
|
||||
|
||||
Now run tests for whichever database:
|
||||
|
||||
```sh
|
||||
TEST_DB=REFERENCE cargo nextest run
|
||||
TEST_DB=MONGODB cargo nextest run
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). Please check individual crates for further license information.
|
||||
The Stoat backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/stoatchat/stoatchat/blob/main/LICENSE).
|
||||
|
||||
**Individual crates may supply their own licenses!**
|
||||
|
||||
70
Revolt.toml
Normal file
70
Revolt.toml
Normal file
@@ -0,0 +1,70 @@
|
||||
# ⚠️ This configuration is intended for development environment.
|
||||
# If you'd like to override anything, create a Revolt.overrides.toml
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://127.0.0.1:27017?directConnection=true&replicaSet=rs0"
|
||||
# Redis connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
redis = "redis://127.0.0.1:6379/"
|
||||
|
||||
[rabbit]
|
||||
host = "127.0.0.1"
|
||||
|
||||
[hosts]
|
||||
# Web locations of various services
|
||||
# Defaults assume all services are reverse-proxied
|
||||
# See https://github.com/stoatchat/self-hosted/blob/main/Caddyfile
|
||||
#
|
||||
# Remember to change these to https/wss where appropriate in production!
|
||||
app = "http://local.revolt.chat:14701"
|
||||
api = "http://local.revolt.chat:14702"
|
||||
events = "ws://local.revolt.chat:14703"
|
||||
autumn = "http://local.revolt.chat:14704"
|
||||
january = "http://local.revolt.chat:14705"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
# Public urls for livekit nodes
|
||||
# each entry here should have a corresponding entry under `api.livekit.nodes`
|
||||
[hosts.livekit]
|
||||
worldwide = "ws://local.revolt.chat:14706"
|
||||
|
||||
[api]
|
||||
|
||||
[api.smtp]
|
||||
# Email server configuration for verification
|
||||
# Defaults to no email verification (host field is empty)
|
||||
host = "localhost"
|
||||
username = "smtp"
|
||||
password = "smtp"
|
||||
from_address = "development@revolt.chat"
|
||||
reply_to = "support@revolt.chat"
|
||||
port = 14025
|
||||
use_tls = false
|
||||
use_starttls = false
|
||||
|
||||
[api.livekit]
|
||||
|
||||
# Config for livekit nodes
|
||||
# Make sure to change the secret when deploying
|
||||
# The key and secret should match the values livekit is using
|
||||
[api.livekit.nodes.worldwide]
|
||||
url = "http://livekit"
|
||||
lat = 0.0
|
||||
lon = 0.0
|
||||
key = "worldwide"
|
||||
secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0"
|
||||
|
||||
[files.s3]
|
||||
# S3 protocol endpoint
|
||||
endpoint = "http://127.0.0.1:14009"
|
||||
# S3 region name
|
||||
region = "minio"
|
||||
# S3 protocol key ID
|
||||
access_key_id = "minioautumn"
|
||||
# S3 protocol access key
|
||||
secret_access_key = "minioautumn"
|
||||
# Bucket to upload to by default
|
||||
default_bucket = "revolt-uploads"
|
||||
@@ -1,67 +0,0 @@
|
||||
# Code Style Guide
|
||||
|
||||
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
|
||||
|
||||
## `core/database` crate
|
||||
|
||||
w.r.t. `model.rs` files
|
||||
|
||||
- All struct definitions must be commented.
|
||||
```rust
|
||||
/// Server
|
||||
pub struct Server {
|
||||
/// Name of the server
|
||||
pub name: String,
|
||||
```
|
||||
- Struct definitions should not include derives unless necessary (if additional traits such as Hash are required) and instead use `auto_derived!` and `auto_derived_partial!`.
|
||||
```rust
|
||||
auto_derived_partial!(
|
||||
/// Server
|
||||
pub struct Server { .. },
|
||||
"PartialServer"
|
||||
);
|
||||
```
|
||||
- `auto_derived!` macro accepts multiple entries and should be used as such:
|
||||
```rust
|
||||
auto_derived!(
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsServer { .. }
|
||||
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsRole { .. }
|
||||
);
|
||||
```
|
||||
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
|
||||
```rust
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
```
|
||||
- `impl` blocks may be defined below the struct definitions and should be ordered in the same order of definition. Methods in the block must follow the same guidelines as traits where-in: methods are ordered in terms of CRUD, there are empty line breaks, and methods are commented.
|
||||
|
||||
w.r.t. `ops` module for models
|
||||
|
||||
- All traits must use a the name format `AbstractPlural` where Plural is the plural form of the collection. e.g. Servers
|
||||
- Traits defined must follow these guidelines:
|
||||
|
||||
- Methods are ordered in terms of CRUD, create-read-update-delete ordering.
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait AbstractServerMembers: Sync + Send {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()>;
|
||||
|
||||
/// Fetch a server member by their id
|
||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
||||
|
||||
/// Update information for a server member
|
||||
async fn update_member(&self, .. ) -> Result<()>;
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
- There should be an empty line break between each method declaration.
|
||||
- All methods must have an appropriate comment.
|
||||
|
||||
- When implementing the trait defined in `ops.rs` with each driver, the method declaration style should be the same for ease of searching: same ordering, same comments, same line breaks.
|
||||
19
clippy.toml
19
clippy.toml
@@ -1,15 +1,28 @@
|
||||
disallowed-methods = [
|
||||
# Shouldn't need to access these directly
|
||||
"revolt_database::models::bots::model::Bot::remove_field",
|
||||
"revolt_database::models::messages::model::Message::attach_sendable_embed",
|
||||
"revolt_database::models::users::model::User::set_relationship",
|
||||
"revolt_database::models::users::model::User::apply_relationship",
|
||||
|
||||
# Prefer to use Object::create()
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::insert_account_strike",
|
||||
"revolt_database::models::bots::ops::AbstractBots::insert_bot",
|
||||
"revolt_database::models::channel_invites::ops::AbstractChannelInvites::insert_invite",
|
||||
"revolt_database::models::channel_unreads::ops::AbstractChannelUnreads::acknowledge_message",
|
||||
"revolt_database::models::channel_webhooks::ops::AbstractWebhooks::insert_webhook",
|
||||
"revolt_database::models::channels::ops::AbstractChannels::insert_channel",
|
||||
"revolt_database::models::emojis::ops::AbstractEmojis::insert_emoji",
|
||||
"revolt_database::models::files::ops::AbstractAttachments::insert_attachment",
|
||||
"revolt_database::models::messages::ops::AbstractMessages::insert_message",
|
||||
"revolt_database::models::ratelimit_events::ops::AbstractRatelimitEvents::insert_ratelimit_event",
|
||||
"revolt_database::models::server_bans::ops::AbstractServerBans::insert_ban",
|
||||
"revolt_database::models::server_members::ops::AbstractServerMembers::insert_member",
|
||||
"revolt_database::models::servers::ops::AbstractServers::insert_server",
|
||||
"revolt_database::models::users::ops::AbstractUsers::insert_user",
|
||||
|
||||
# Prefer to use Object::update(&self)
|
||||
"revolt_database::models::bots::ops::AbstractBots::update_bot",
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
|
||||
|
||||
# Prefer to use Object::delete(&self)
|
||||
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
|
||||
]
|
||||
|
||||
96
compose.yml
Normal file
96
compose.yml
Normal file
@@ -0,0 +1,96 @@
|
||||
services:
|
||||
# Redis
|
||||
redis:
|
||||
image: eqalpha/keydb
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
# MongoDB
|
||||
database:
|
||||
image: mongo
|
||||
command: ["--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./.data/db:/data/db
|
||||
- ./scripts/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
|
||||
healthcheck:
|
||||
test: >
|
||||
mongosh --quiet --eval "
|
||||
try {
|
||||
const status = rs.status();
|
||||
if (status.ok === 1 && status.members[0].stateStr === 'PRIMARY') {
|
||||
quit(0);
|
||||
} else {
|
||||
quit(1);
|
||||
}
|
||||
} catch (e) {
|
||||
quit(1);
|
||||
}"
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 65536
|
||||
hard: 65536
|
||||
|
||||
# MinIO
|
||||
minio:
|
||||
image: minio/minio
|
||||
command: server /data
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioautumn
|
||||
MINIO_ROOT_PASSWORD: minioautumn
|
||||
volumes:
|
||||
- ./.data/minio:/data
|
||||
ports:
|
||||
- "14009:9000"
|
||||
- "14010:9001"
|
||||
restart: always
|
||||
|
||||
# Create buckets for minio.
|
||||
createbuckets:
|
||||
image: minio/mc
|
||||
depends_on:
|
||||
- minio
|
||||
entrypoint: >
|
||||
/bin/sh -c "while ! /usr/bin/mc ready minio; do
|
||||
/usr/bin/mc alias set minio http://minio:9000 minioautumn minioautumn;
|
||||
echo 'Waiting minio...' && sleep 1;
|
||||
done; /usr/bin/mc mb minio/revolt-uploads; exit 0;"
|
||||
|
||||
# Rabbit
|
||||
rabbit:
|
||||
image: rabbitmq:4-management
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: rabbituser
|
||||
RABBITMQ_DEFAULT_PASS: rabbitpass
|
||||
volumes:
|
||||
- ./.data/rabbit:/var/lib/rabbitmq
|
||||
#- ./rabbit_plugins:/opt/rabbitmq/plugins/
|
||||
#- ./rabbit_enabled_plugins:/etc/rabbitmq/enabled_plugins
|
||||
# uncomment this if you need to enable other plugins
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672" # management UI, for development
|
||||
|
||||
# Mock SMTP server
|
||||
maildev:
|
||||
image: maildev/maildev
|
||||
ports:
|
||||
- "14025:25"
|
||||
- "14080:8080"
|
||||
environment:
|
||||
MAILDEV_SMTP_PORT: 25
|
||||
MAILDEV_WEB_PORT: 8080
|
||||
MAILDEV_INCOMING_USER: smtp
|
||||
MAILDEV_INCOMING_PASS: smtp
|
||||
|
||||
livekit:
|
||||
image: ghcr.io/stoatchat/livekit-server:v1.9.13
|
||||
command: --config /etc/livekit.yml
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./livekit.yml:/etc/livekit.yml
|
||||
@@ -1,23 +1,29 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.6.0"
|
||||
version = "0.11.5"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# util
|
||||
log = "*"
|
||||
sentry = "0.31.5"
|
||||
lru = "0.7.6"
|
||||
ulid = "0.5.0"
|
||||
once_cell = "1.9.0"
|
||||
redis-kiss = "0.1.4"
|
||||
lru_time_cache = "0.11.11"
|
||||
async-channel = "2.3.1"
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
regex = "1.11.1"
|
||||
|
||||
# serde
|
||||
bincode = "1.3.3"
|
||||
serde_json = "1.0.79"
|
||||
rmp-serde = "1.0.0"
|
||||
serde = "1.0.136"
|
||||
@@ -25,7 +31,20 @@ serde = "1.0.136"
|
||||
# async
|
||||
futures = "0.3.21"
|
||||
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
|
||||
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
|
||||
async-std = { version = "1.8.0", features = [
|
||||
"tokio1",
|
||||
"tokio02",
|
||||
"attributes",
|
||||
] }
|
||||
|
||||
# core
|
||||
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }
|
||||
authifier = { version = "1.0.16" }
|
||||
revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database", features = ["voice"] }
|
||||
revolt-permissions = { path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Build Stage
|
||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||
FROM ghcr.io/stoatchat/base:latest AS builder
|
||||
FROM debian:12 AS debian
|
||||
|
||||
# Bundle Stage
|
||||
FROM debian:bullseye-slim
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ca-certificates && \
|
||||
apt-get clean
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
||||
EXPOSE 9000
|
||||
COPY --from=debian /usr/bin/uname /usr/bin/uname
|
||||
|
||||
EXPOSE 14703
|
||||
USER nonroot
|
||||
CMD ["./revolt-bonfire"]
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_quark::{Error, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use revolt_database::events::client::ReadyPayloadFields;
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// matches either a single word ie "users" or a key and value ie "settings[notifications]"
|
||||
static READY_PAYLOAD_FIELD_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap());
|
||||
|
||||
/// Enumeration of supported protocol formats
|
||||
#[derive(Debug)]
|
||||
pub enum ProtocolFormat {
|
||||
@@ -16,6 +23,7 @@ pub struct ProtocolConfiguration {
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
ready_payload_fields: ReadyPayloadFields,
|
||||
}
|
||||
|
||||
impl ProtocolConfiguration {
|
||||
@@ -24,11 +32,13 @@ impl ProtocolConfiguration {
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
ready_payload_fields: ReadyPayloadFields,
|
||||
) -> Self {
|
||||
Self {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
ready_payload_fields,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,16 +47,16 @@ impl ProtocolConfiguration {
|
||||
match self.format {
|
||||
ProtocolFormat::Json => {
|
||||
if let Message::Text(text) = msg {
|
||||
serde_json::from_str(text).map_err(|_| Error::InternalError)
|
||||
serde_json::from_str(text).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
ProtocolFormat::Msgpack => {
|
||||
if let Message::Binary(buf) = msg {
|
||||
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError)
|
||||
rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +93,11 @@ impl ProtocolConfiguration {
|
||||
pub fn get_protocol_format(&self) -> &ProtocolFormat {
|
||||
&self.format
|
||||
}
|
||||
|
||||
/// Get ready payload fields
|
||||
pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields {
|
||||
&self.ready_payload_fields
|
||||
}
|
||||
}
|
||||
|
||||
/// Object holding one side of a channel for receiving the parsed information
|
||||
@@ -112,6 +127,23 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
let mut protocol_version = 1;
|
||||
let mut format = ProtocolFormat::Json;
|
||||
let mut session_token = None;
|
||||
let mut ready_payload_fields = if params.iter().any(|(k, _)| *k == "ready") {
|
||||
// If they pass the ready field, set all fields to false
|
||||
|
||||
ReadyPayloadFields {
|
||||
users: false,
|
||||
servers: false,
|
||||
channels: false,
|
||||
members: false,
|
||||
emojis: false,
|
||||
voice_states: false,
|
||||
user_settings: Vec::new(),
|
||||
channel_unreads: false,
|
||||
policy_changes: false,
|
||||
}
|
||||
} else {
|
||||
ReadyPayloadFields::default()
|
||||
};
|
||||
|
||||
// Parse and map parameters from key-value to known variables.
|
||||
for (key, value) in params {
|
||||
@@ -127,6 +159,31 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
_ => {}
|
||||
},
|
||||
"token" => session_token = Some(value.into()),
|
||||
"ready" => {
|
||||
// Re-enable all the fields the client specifies
|
||||
if let Some(captures) = READY_PAYLOAD_FIELD_REGEX.captures(value) {
|
||||
if let Some(field) = captures.get(0) {
|
||||
match field.as_str() {
|
||||
"users" => ready_payload_fields.users = true,
|
||||
"servers" => ready_payload_fields.servers = true,
|
||||
"channels" => ready_payload_fields.channels = true,
|
||||
"members" => ready_payload_fields.members = true,
|
||||
"emojis" => ready_payload_fields.emojis = true,
|
||||
"voice_states" => ready_payload_fields.voice_states = true,
|
||||
"channel_unreads" => ready_payload_fields.channel_unreads = true,
|
||||
"user_settings" => {
|
||||
if let Some(subkey) = captures.get(1) {
|
||||
ready_payload_fields
|
||||
.user_settings
|
||||
.push(subkey.as_str().to_string());
|
||||
}
|
||||
}
|
||||
"policy_changes" => ready_payload_fields.policy_changes = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -139,6 +196,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
ready_payload_fields,
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use revolt_quark::{Database, DatabaseInfo};
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
static DBCONN: OnceCell<Database> = OnceCell::new();
|
||||
|
||||
@@ -10,7 +10,9 @@ pub async fn connect() {
|
||||
.await
|
||||
.expect("Failed to connect to the database.");
|
||||
|
||||
DBCONN.set(database).expect("Setting `Database`");
|
||||
if DBCONN.set(database).is_err() {
|
||||
panic!("couldn't set database")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the current database.
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
get_relationship,
|
||||
models::{
|
||||
server_member::FieldsMember,
|
||||
user::{PartialUser, Presence, RelationshipStatus},
|
||||
Channel, Member, User,
|
||||
},
|
||||
perms, Database, Permission, Result,
|
||||
use futures::future::join_all;
|
||||
use revolt_database::{
|
||||
events::client::{EventV1, ReadyPayloadFields},
|
||||
util::permissions::DatabasePermissionQuery,
|
||||
voice::{get_channel_voice_state, UserVoiceChannel},
|
||||
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::{
|
||||
client::EventV1,
|
||||
state::{Cache, State},
|
||||
};
|
||||
use super::state::{Cache, State};
|
||||
|
||||
/// Cache Manager
|
||||
impl Cache {
|
||||
/// Check whether the current user can view a channel
|
||||
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
|
||||
#[allow(deprecated)]
|
||||
match &channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
let member = self.members.values().find(|x| &x.id.server == server);
|
||||
|
||||
Channel::TextChannel { server, .. } => {
|
||||
let member = self.members.get(server);
|
||||
let server = self.servers.get(server);
|
||||
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
||||
let mut query =
|
||||
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
|
||||
.channel(channel);
|
||||
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
||||
|
||||
if let Some(member) = member {
|
||||
perms.member.set_ref(member);
|
||||
query = query.member(member);
|
||||
}
|
||||
|
||||
if let Some(server) = server {
|
||||
perms.server.set_ref(server);
|
||||
query = query.server(server);
|
||||
}
|
||||
|
||||
perms
|
||||
.has_permission(db, Permission::ViewChannel)
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.has_channel_permission(ChannelPermission::ViewChannel)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
@@ -64,7 +63,7 @@ impl Cache {
|
||||
/// Check whether we can subscribe to another user
|
||||
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
|
||||
if let Some(user) = self.users.get(&self.user_id) {
|
||||
match get_relationship(user, user_id) {
|
||||
match user.relationship_with(user_id) {
|
||||
RelationshipStatus::Friend
|
||||
| RelationshipStatus::Incoming
|
||||
| RelationshipStatus::Outgoing
|
||||
@@ -95,8 +94,27 @@ impl Cache {
|
||||
/// State Manager
|
||||
impl State {
|
||||
/// Generate a Ready packet for the current user
|
||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
||||
let mut user = self.clone_user();
|
||||
pub async fn generate_ready_payload(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
fields: &ReadyPayloadFields,
|
||||
) -> Result<EventV1> {
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Fetch pending policy changes.
|
||||
let policy_changes = if user.bot.is_some() || !fields.policy_changes {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
db.fetch_policy_changes()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
@@ -106,10 +124,11 @@ impl State {
|
||||
.unwrap_or_default();
|
||||
|
||||
// Fetch all memberships with their corresponding servers.
|
||||
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
|
||||
let mut members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
|
||||
|
||||
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
|
||||
let servers = db.fetch_servers(&server_ids).await?;
|
||||
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
|
||||
// Collect channel ids from servers.
|
||||
let mut channel_ids = vec![];
|
||||
@@ -134,9 +153,57 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
let voice_states = if fields.voice_states {
|
||||
let mut voice_state_server_members: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
|
||||
// fetch voice states for all the channels we can see
|
||||
let mut voice_states = Vec::new();
|
||||
|
||||
for channel in channels.iter().filter(|c| {
|
||||
matches!(
|
||||
c,
|
||||
Channel::DirectMessage { .. }
|
||||
| Channel::Group { .. }
|
||||
| Channel::TextChannel { voice: Some(_), .. }
|
||||
)
|
||||
}) {
|
||||
if let Ok(Some(voice_state)) =
|
||||
get_channel_voice_state(&UserVoiceChannel::from_channel(channel)).await
|
||||
{
|
||||
if let Some(server) = channel.server() {
|
||||
let set = voice_state_server_members
|
||||
.entry(server.to_string())
|
||||
.or_default();
|
||||
|
||||
for participant in &voice_state.participants {
|
||||
user_ids.insert(participant.id.clone());
|
||||
set.insert(participant.id.clone());
|
||||
}
|
||||
} else {
|
||||
for participant in &voice_state.participants {
|
||||
user_ids.insert(participant.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
voice_states.push(voice_state);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all the members for for the participants who are in a server
|
||||
for (server, user_ids) in voice_state_server_members {
|
||||
let user_ids = user_ids.into_iter().collect::<Vec<_>>();
|
||||
let voice_members = db.fetch_members(&server, &user_ids).await?;
|
||||
|
||||
members.extend(voice_members);
|
||||
}
|
||||
|
||||
Some(voice_states)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch presence data for known users.
|
||||
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
|
||||
user.online = Some(true);
|
||||
|
||||
// Fetch user data.
|
||||
let users = db
|
||||
@@ -148,69 +215,118 @@ impl State {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
// Copy data into local state cache.
|
||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
self.cache
|
||||
.users
|
||||
.insert(self.cache.user_id.clone(), user.clone());
|
||||
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
self.cache.channels = channels
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id().to_string(), x))
|
||||
.collect();
|
||||
self.cache.members = members
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id.server.clone(), x))
|
||||
.collect();
|
||||
|
||||
// Make all users appear from our perspective.
|
||||
let mut users: Vec<User> = users
|
||||
.into_iter()
|
||||
.map(|mut x| {
|
||||
x.online = Some(online_ids.contains(&x.id));
|
||||
x.with_relationship(&user)
|
||||
})
|
||||
// Fetch customisations.
|
||||
let emojis = if fields.emojis {
|
||||
Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|emoji| emoji.into())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch user settings
|
||||
let user_settings = if !fields.user_settings.is_empty() {
|
||||
Some(
|
||||
db.fetch_user_settings(&user.id, &fields.user_settings)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch channel unreads
|
||||
let channel_unreads = if fields.channel_unreads {
|
||||
Some(
|
||||
db.fetch_unreads(&user.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|unread| unread.into())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Copy data into local state cache.
|
||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
self.cache
|
||||
.users
|
||||
.insert(self.cache.user_id.clone(), user.clone());
|
||||
self.cache.channels = channels
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id().to_string(), x))
|
||||
.collect();
|
||||
|
||||
// Make all users appear from our perspective.
|
||||
let mut users: Vec<v0::User> = join_all(users.into_iter().map(|other_user| async {
|
||||
let is_online = online_ids.contains(&other_user.id);
|
||||
other_user.into_known(&user, is_online).await
|
||||
}))
|
||||
.await;
|
||||
|
||||
// Make sure we see our own user correctly.
|
||||
user.relationship = Some(RelationshipStatus::User);
|
||||
users.push(user.foreign());
|
||||
users.push(user.into_self(true).await);
|
||||
|
||||
// Set subscription state internally.
|
||||
self.reset_state();
|
||||
self.insert_subscription(self.private_topic.clone());
|
||||
self.reset_state().await;
|
||||
self.insert_subscription(self.private_topic.clone()).await;
|
||||
|
||||
for user in &users {
|
||||
self.insert_subscription(user.id.clone());
|
||||
self.insert_subscription(user.id.clone()).await;
|
||||
}
|
||||
|
||||
for server in &servers {
|
||||
self.insert_subscription(server.id.clone());
|
||||
self.insert_subscription(server.id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", server.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
for channel in &channels {
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
}
|
||||
|
||||
Ok(EventV1::Ready {
|
||||
users,
|
||||
servers,
|
||||
channels,
|
||||
members,
|
||||
users: if fields.users { Some(users) } else { None },
|
||||
servers: if fields.servers {
|
||||
Some(servers.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
channels: if fields.channels {
|
||||
Some(channels.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
members: if fields.members {
|
||||
Some(members.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
voice_states,
|
||||
|
||||
emojis,
|
||||
user_settings,
|
||||
channel_unreads,
|
||||
|
||||
policy_changes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -223,19 +339,14 @@ impl State {
|
||||
|
||||
let id = &id.to_string();
|
||||
for (channel_id, channel) in &self.cache.channels {
|
||||
match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
if server == id {
|
||||
channel_ids.insert(channel_id.clone());
|
||||
if channel.server() == Some(id) {
|
||||
channel_ids.insert(channel_id.clone());
|
||||
|
||||
if self.cache.can_view_channel(db, channel).await {
|
||||
added_channels.push(channel_id.clone());
|
||||
} else {
|
||||
removed_channels.push(channel_id.clone());
|
||||
}
|
||||
}
|
||||
if self.cache.can_view_channel(db, channel).await {
|
||||
added_channels.push(channel_id.clone());
|
||||
} else {
|
||||
removed_channels.push(channel_id.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,11 +355,11 @@ impl State {
|
||||
let mut bulk_events = vec![];
|
||||
|
||||
for id in added_channels {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
for id in removed_channels {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
self.cache.channels.remove(&id);
|
||||
|
||||
bulk_events.push(EventV1::ChannelDelete { id });
|
||||
@@ -271,8 +382,8 @@ impl State {
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
bulk_events.push(EventV1::ChannelCreate(channel));
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
bulk_events.push(EventV1::ChannelCreate(channel.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,6 +401,11 @@ impl State {
|
||||
|
||||
/// Push presence change to the user and all associated server topics
|
||||
pub async fn broadcast_presence_change(&self, target: bool) {
|
||||
let config = revolt_config::config().await;
|
||||
if config.disable_events_dont_use {
|
||||
return;
|
||||
}
|
||||
|
||||
if if let Some(status) = &self.cache.users.get(&self.cache.user_id).unwrap().status {
|
||||
status.presence != Some(Presence::Invisible)
|
||||
} else {
|
||||
@@ -297,7 +413,7 @@ impl State {
|
||||
} {
|
||||
let event = EventV1::UserUpdate {
|
||||
id: self.cache.user_id.clone(),
|
||||
data: PartialUser {
|
||||
data: v0::PartialUser {
|
||||
online: Some(target),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -344,8 +460,8 @@ impl State {
|
||||
match event {
|
||||
EventV1::ChannelCreate(channel) => {
|
||||
let id = channel.id().to_string();
|
||||
self.insert_subscription(id.clone());
|
||||
self.cache.channels.insert(id, channel.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
self.cache.channels.insert(id, channel.clone().into());
|
||||
}
|
||||
EventV1::ChannelUpdate {
|
||||
id, data, clear, ..
|
||||
@@ -353,15 +469,21 @@ impl State {
|
||||
let could_view: bool = if let Some(channel) = self.cache.channels.get(id) {
|
||||
self.cache.can_view_channel(db, channel).await
|
||||
} else {
|
||||
true
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(channel) = self.cache.channels.get_mut(id) {
|
||||
for field in clear {
|
||||
channel.remove(field);
|
||||
channel.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
channel.apply_options(data.clone());
|
||||
channel.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if !self.cache.channels.contains_key(id) {
|
||||
if let Ok(channel) = db.fetch_channel(id).await {
|
||||
self.cache.channels.insert(id.clone(), channel);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(channel) = self.cache.channels.get(id) {
|
||||
@@ -369,7 +491,7 @@ impl State {
|
||||
if could_view != can_view {
|
||||
if can_view {
|
||||
queue_add = Some(id.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone().into());
|
||||
} else {
|
||||
queue_remove = Some(id.clone());
|
||||
*event = EventV1::ChannelDelete { id: id.clone() };
|
||||
@@ -378,17 +500,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
EventV1::ChannelDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
self.cache.channels.remove(id);
|
||||
}
|
||||
EventV1::ChannelGroupJoin { user, .. } => {
|
||||
self.insert_subscription(user.clone());
|
||||
self.insert_subscription(user.clone()).await;
|
||||
}
|
||||
EventV1::ChannelGroupLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
} else if !self.cache.can_subscribe_to_user(user) {
|
||||
self.remove_subscription(user);
|
||||
self.remove_subscription(user).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,14 +518,29 @@ impl State {
|
||||
id,
|
||||
server,
|
||||
channels,
|
||||
emojis: _,
|
||||
voice_states: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone());
|
||||
self.cache.servers.insert(id.to_string(), server.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", id)).await;
|
||||
}
|
||||
|
||||
self.cache.servers.insert(id.clone(), server.clone().into());
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.clone(),
|
||||
user: self.cache.user_id.clone(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
self.cache.members.insert(id.clone(), member);
|
||||
|
||||
for channel in channels {
|
||||
self.cache
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
.insert(channel.id().to_string(), channel.clone().into());
|
||||
}
|
||||
|
||||
queue_server = Some(id.clone());
|
||||
@@ -413,10 +550,10 @@ impl State {
|
||||
} => {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
for field in clear {
|
||||
server.remove(field);
|
||||
server.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
server.apply_options(data.clone());
|
||||
server.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.default_permissions.is_some() {
|
||||
@@ -426,39 +563,41 @@ impl State {
|
||||
EventV1::ServerMemberJoin { .. } => {
|
||||
// We will always receive ServerCreate when joining a new server.
|
||||
}
|
||||
EventV1::ServerMemberLeave { id, user } => {
|
||||
EventV1::ServerMemberLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel);
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
self.cache.members.remove(id);
|
||||
}
|
||||
}
|
||||
EventV1::ServerDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel);
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
self.cache.members.remove(id);
|
||||
}
|
||||
EventV1::ServerMemberUpdate { id, data, clear } => {
|
||||
if id.user == self.cache.user_id {
|
||||
if let Some(member) = self.cache.members.get_mut(&id.server) {
|
||||
for field in &clear.clone() {
|
||||
member.remove(field);
|
||||
member.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
member.apply_options(data.clone());
|
||||
member.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.roles.is_some() || clear.contains(&FieldsMember::Roles) {
|
||||
if data.roles.is_some() || clear.contains(&v0::FieldsMember::Roles) {
|
||||
queue_server = Some(id.server.clone());
|
||||
}
|
||||
}
|
||||
@@ -473,10 +612,10 @@ impl State {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
if let Some(role) = server.roles.get_mut(role_id) {
|
||||
for field in &clear.clone() {
|
||||
role.remove(field);
|
||||
role.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
role.apply_options(data.clone());
|
||||
role.apply_options(data.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,12 +651,26 @@ impl State {
|
||||
*event_id = None;
|
||||
}
|
||||
EventV1::UserRelationship { id, user, .. } => {
|
||||
self.cache.users.insert(id.clone(), user.clone());
|
||||
self.cache.users.insert(id.clone(), user.clone().into());
|
||||
|
||||
if self.cache.can_subscribe_to_user(id) {
|
||||
self.insert_subscription(id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
} else {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::Message(message) => {
|
||||
// Since Message events are fanned out to many clients,
|
||||
// we must reconstruct the relationship value at this end.
|
||||
if let Some(user) = &mut message.user {
|
||||
user.relationship = self
|
||||
.cache
|
||||
.users
|
||||
.get(&self.cache.user_id)
|
||||
.expect("missing self?")
|
||||
.relationship_with(&message.author)
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,50 +684,13 @@ impl State {
|
||||
|
||||
// Sub / unsub accordingly.
|
||||
if let Some(id) = queue_add {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
if let Some(id) = queue_remove {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
/// Publish helper wrapper
|
||||
pub async fn p(self, channel: String) {
|
||||
#[cfg(not(debug_assertions))]
|
||||
redis_kiss::p(channel, self).await;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("Publishing event to {channel}: {self:?}");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
redis_kiss::publish(channel, self).await.unwrap();
|
||||
}
|
||||
|
||||
/// Publish user event
|
||||
pub async fn p_user(self, id: String, db: &Database) {
|
||||
self.clone().p(id.clone()).await;
|
||||
|
||||
// ! FIXME: this should be captured by member list in the future
|
||||
// ! and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().p(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish private event
|
||||
pub async fn private(self, id: String) {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
}
|
||||
}
|
||||
2
crates/bonfire/src/events/mod.rs
Normal file
2
crates/bonfire/src/events/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod r#impl;
|
||||
pub mod state;
|
||||
@@ -1,8 +1,13 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::models::{Channel, Member, Server, User};
|
||||
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
pub enum SubscriptionStateChange {
|
||||
@@ -31,6 +36,7 @@ pub enum SubscriptionStateChange {
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub user_id: String,
|
||||
pub is_bot: bool,
|
||||
|
||||
pub users: HashMap<String, User>,
|
||||
pub channels: HashMap<String, Channel>,
|
||||
@@ -44,6 +50,7 @@ impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Cache {
|
||||
user_id: Default::default(),
|
||||
is_bot: false,
|
||||
|
||||
users: Default::default(),
|
||||
channels: Default::default(),
|
||||
@@ -59,14 +66,17 @@ impl Default for Cache {
|
||||
pub struct State {
|
||||
pub cache: Cache,
|
||||
|
||||
pub session_id: String,
|
||||
pub private_topic: String,
|
||||
subscribed: HashSet<String>,
|
||||
state: SubscriptionStateChange,
|
||||
pub state: SubscriptionStateChange,
|
||||
|
||||
pub subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
pub active_servers: Arc<Mutex<LruTimeCache<String, ()>>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Create state from User
|
||||
pub fn from(user: User) -> State {
|
||||
pub fn from(user: User, session_id: String) -> State {
|
||||
let mut subscribed = HashSet::new();
|
||||
let private_topic = format!("{}!", user.id);
|
||||
subscribed.insert(private_topic.clone());
|
||||
@@ -81,22 +91,61 @@ impl State {
|
||||
|
||||
State {
|
||||
cache,
|
||||
subscribed,
|
||||
subscribed: Arc::new(RwLock::new(subscribed)),
|
||||
active_servers: Arc::new(Mutex::new(LruTimeCache::with_expiry_duration_and_capacity(
|
||||
Duration::from_secs(900),
|
||||
5,
|
||||
))),
|
||||
session_id,
|
||||
private_topic,
|
||||
state: SubscriptionStateChange::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply currently queued state
|
||||
pub fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
pub async fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
// Check if we need to change subscriptions to member event topics
|
||||
if !self.cache.is_bot {
|
||||
enum Server {
|
||||
Subscribe(String),
|
||||
Unsubscribe(String),
|
||||
}
|
||||
|
||||
let active_server_changes: Vec<Server> = {
|
||||
let mut active_servers = self.active_servers.lock().await;
|
||||
active_servers
|
||||
.notify_iter()
|
||||
.map(|e| match e {
|
||||
TimedEntry::Valid(k, _) => Server::Subscribe(format!("{}u", k)),
|
||||
TimedEntry::Expired(k, _) => Server::Unsubscribe(format!("{}u", k)),
|
||||
})
|
||||
.collect()
|
||||
// It is bad practice to open more than one Mutex at once and could
|
||||
// lead to a deadlock, so instead we choose to collect the changes.
|
||||
};
|
||||
|
||||
for entry in active_server_changes {
|
||||
match entry {
|
||||
Server::Subscribe(k) => {
|
||||
self.insert_subscription(k).await;
|
||||
}
|
||||
Server::Unsubscribe(k) => {
|
||||
self.remove_subscription(&k).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush changes to subscriptions
|
||||
let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None);
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if let SubscriptionStateChange::Change { add, remove } = &state {
|
||||
for id in add {
|
||||
self.subscribed.insert(id.clone());
|
||||
subscribed.insert(id.clone());
|
||||
}
|
||||
|
||||
for id in remove {
|
||||
self.subscribed.remove(id);
|
||||
subscribed.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,20 +157,16 @@ impl State {
|
||||
self.cache.users.get(&self.cache.user_id).unwrap().clone()
|
||||
}
|
||||
|
||||
/// Iterate through all subscriptions
|
||||
pub fn iter_subscriptions(&self) -> std::collections::hash_set::Iter<'_, std::string::String> {
|
||||
self.subscribed.iter()
|
||||
}
|
||||
|
||||
/// Reset the current state
|
||||
pub fn reset_state(&mut self) {
|
||||
pub async fn reset_state(&mut self) {
|
||||
self.state = SubscriptionStateChange::Reset;
|
||||
self.subscribed.clear();
|
||||
self.subscribed.write().await.clear();
|
||||
}
|
||||
|
||||
/// Add a new subscription
|
||||
pub fn insert_subscription(&mut self, subscription: String) {
|
||||
if self.subscribed.contains(&subscription) {
|
||||
pub async fn insert_subscription(&mut self, subscription: String) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if subscribed.contains(&subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,12 +183,13 @@ impl State {
|
||||
SubscriptionStateChange::Reset => {}
|
||||
}
|
||||
|
||||
self.subscribed.insert(subscription);
|
||||
subscribed.insert(subscription);
|
||||
}
|
||||
|
||||
/// Remove existing subscription
|
||||
pub fn remove_subscription(&mut self, subscription: &str) {
|
||||
if !self.subscribed.contains(&subscription.to_string()) {
|
||||
pub async fn remove_subscription(&mut self, subscription: &str) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if !subscribed.contains(&subscription.to_string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,6 +206,6 @@ impl State {
|
||||
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
|
||||
}
|
||||
|
||||
self.subscribed.remove(subscription);
|
||||
subscribed.remove(subscription);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use revolt_presence::clear_region;
|
||||
extern crate log;
|
||||
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
|
||||
mod database;
|
||||
mod websocket;
|
||||
@@ -14,21 +15,28 @@ mod websocket;
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
// Configure requirements for Bonfire.
|
||||
revolt_quark::configure!();
|
||||
revolt_config::configure!(events);
|
||||
database::connect().await;
|
||||
|
||||
// Clean up the current region information.
|
||||
clear_region(None).await;
|
||||
let no_clear_region = env::var("NO_CLEAR_PRESENCE").unwrap_or_else(|_| "0".into()) == "1";
|
||||
if !no_clear_region {
|
||||
clear_region(None).await;
|
||||
}
|
||||
|
||||
// Setup a TCP listener to accept WebSocket connections on.
|
||||
// By default, we bind to port 9000 on all interfaces.
|
||||
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:9000".into());
|
||||
// By default, we bind to port 14703 on all interfaces.
|
||||
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into());
|
||||
info!("Listening on host {bind}");
|
||||
let try_socket = TcpListener::bind(bind).await;
|
||||
let listener = try_socket.expect("Failed to bind");
|
||||
|
||||
// Start accepting new connections and spawn a client for each connection.
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
websocket::spawn_client(database::get_db(), stream, addr);
|
||||
async_std::task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
websocket::client(database::get_db(), stream, addr).await;
|
||||
info!("User disconnected from {addr:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +1,525 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
use revolt_quark::{
|
||||
events::{
|
||||
client::EventV1,
|
||||
server::ClientMessage,
|
||||
state::{State, SubscriptionStateChange},
|
||||
},
|
||||
models::{user::UserHint, User},
|
||||
redis_kiss, Database,
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::RedisErrorKind,
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
join, pin_mut, select,
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
||||
use revolt_config::report_internal_error;
|
||||
use revolt_database::{
|
||||
events::{client::EventV1, server::ClientMessage},
|
||||
iso8601_timestamp::Timestamp,
|
||||
Database, User, UserHint,
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
|
||||
use async_std::{net::TcpStream, sync::Mutex, task};
|
||||
use async_std::{
|
||||
net::TcpStream,
|
||||
sync::{Mutex, RwLock},
|
||||
task::spawn,
|
||||
};
|
||||
use revolt_result::create_error;
|
||||
use sentry::Level;
|
||||
|
||||
use crate::config::WebsocketHandshakeCallback;
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
use crate::events::state::{State, SubscriptionStateChange};
|
||||
|
||||
/// Spawn a new WebSocket client worker given access to the database,
|
||||
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
|
||||
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
|
||||
|
||||
/// Start a new WebSocket client worker given access to the database,
|
||||
/// the relevant TCP stream and the remote address of the client.
|
||||
pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
|
||||
// Spawn a new Async task to work on.
|
||||
task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
|
||||
// Upgrade the TCP connection to a WebSocket connection.
|
||||
// In this process, we also parse any additional parameters given.
|
||||
// e.g. wss://example.com?format=json&version=1
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
|
||||
stream,
|
||||
WebsocketHandshakeCallback::from(sender),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Upgrade the TCP connection to a WebSocket connection.
|
||||
// In this process, we also parse any additional parameters given.
|
||||
// e.g. wss://example.com?format=json&version=1
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
if let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
|
||||
stream,
|
||||
WebsocketHandshakeCallback::from(sender),
|
||||
None,
|
||||
)
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (mut write, mut read) = ws.split();
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
while let Ok(Some(message)) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
write
|
||||
.send(config.encode(&EventV1::Error {
|
||||
data: create_error!(InvalidSession),
|
||||
}))
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
|
||||
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
write
|
||||
.send(config.encode(&EventV1::Error { data: err }))
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
db.update_session_last_seen(&session_id, Timestamp::now_utc())
|
||||
.await
|
||||
{
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
if let Ok(mut config) = receiver.await {
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
.ok();
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (write, mut read) = ws.split();
|
||||
let write = Mutex::new(write);
|
||||
// Create local state.
|
||||
let mut state = State::from(user, session_id);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
'outer: while let Ok(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
config.set_session_token(token);
|
||||
break 'outer;
|
||||
}
|
||||
// Notify socket we have authenticated.
|
||||
if report_internal_error!(write.send(config.encode(&EventV1::Authenticated)).await).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
let ready_payload = match report_internal_error!(
|
||||
state
|
||||
.generate_ready_payload(db, config.get_ready_payload_fields())
|
||||
.await
|
||||
) {
|
||||
Ok(ready_payload) => ready_payload,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if report_internal_error!(write.send(config.encode(&ready_payload)).await).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
{
|
||||
// Setup channels and mutexes
|
||||
let write = Mutex::new(write);
|
||||
let subscribed = state.subscribed.clone();
|
||||
let active_servers = state.active_servers.clone();
|
||||
let (topic_signal_s, topic_signal_r) = async_channel::unbounded();
|
||||
|
||||
// TODO: this needs to be rewritten
|
||||
// Create channels through which the tasks can signal to each other they need to clean up
|
||||
let (kill_signal_1_s, kill_signal_1_r) = async_channel::bounded(1);
|
||||
let (kill_signal_2_s, kill_signal_2_r) = async_channel::bounded(1);
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener_with_kill_signal(
|
||||
db,
|
||||
&mut state,
|
||||
addr,
|
||||
&config,
|
||||
topic_signal_r,
|
||||
kill_signal_1_r,
|
||||
&write,
|
||||
kill_signal_2_s,
|
||||
);
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker_with_kill_signal(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id.clone(),
|
||||
&config,
|
||||
topic_signal_s,
|
||||
kill_signal_2_r,
|
||||
read,
|
||||
&write,
|
||||
kill_signal_1_s,
|
||||
);
|
||||
|
||||
join!(listener, worker);
|
||||
}
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
|
||||
// If this was the last session, notify other users that we just went offline.
|
||||
if last_session {
|
||||
state.broadcast_presence_change(false).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn listener_with_kill_signal(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
listener(
|
||||
db,
|
||||
state,
|
||||
addr,
|
||||
config,
|
||||
topic_signal_r,
|
||||
kill_signal_r,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
async fn listener(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let stoat_config = revolt_config::config().await;
|
||||
let url = stoat_config
|
||||
.database
|
||||
.redis_pubsub
|
||||
.unwrap_or(REDIS_URI.to_string());
|
||||
|
||||
let redis_config = RedisConfig::from_url(&url).unwrap();
|
||||
let subscriber = match report_internal_error!(
|
||||
fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
) {
|
||||
Ok(subscriber) => subscriber,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if report_internal_error!(subscriber.init().await).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Redis connection dropping
|
||||
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
||||
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
|
||||
subscriber.on_error(move |err| {
|
||||
if let RedisErrorKind::Canceled = err.kind() {
|
||||
let clean_up_s = clean_up_s.clone();
|
||||
spawn(async move {
|
||||
clean_up_s.lock().await.send(()).await.ok();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
'out: loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state().await {
|
||||
SubscriptionStateChange::Reset => {
|
||||
if report_internal_error!(subscriber.unsubscribe_all().await).is_err() {
|
||||
break 'out;
|
||||
}
|
||||
|
||||
let subscribed = state.subscribed.read().await;
|
||||
for id in subscribed.iter() {
|
||||
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
if let Some(token) = config.get_session_token().as_ref() {
|
||||
match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => {
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} has reset their subscriptions");
|
||||
}
|
||||
SubscriptionStateChange::Change { add, remove } => {
|
||||
for id in remove {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
if report_internal_error!(subscriber.unsubscribe(id).await).is_err() {
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
if let Ok(ready_payload) = state.generate_ready_payload(db).await {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&ready_payload))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = async {
|
||||
if let Ok(mut conn) = redis_kiss::open_pubsub_connection().await
|
||||
{
|
||||
loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state() {
|
||||
SubscriptionStateChange::Reset => {
|
||||
for id in state.iter_subscriptions() {
|
||||
conn.subscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} has reset their subscriptions");
|
||||
}
|
||||
SubscriptionStateChange::Change { add, remove } => {
|
||||
for id in remove {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
conn.unsubscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
conn.subscribe(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
// * Debug logging of current subscriptions.
|
||||
/*#[cfg(debug_assertions)]
|
||||
info!(
|
||||
"User {addr:?} is subscribed to {:?}",
|
||||
state
|
||||
.iter_subscriptions()
|
||||
.collect::<Vec<&String>>()
|
||||
);*/
|
||||
|
||||
// Handle incoming events.
|
||||
match conn.on_message().next().await.map(|item| {
|
||||
(
|
||||
item.get_channel_name().to_string(),
|
||||
redis_kiss::decode_payload::<EventV1>(&item),
|
||||
)
|
||||
}) {
|
||||
Some((channel, item)) => {
|
||||
if let Ok(mut event) = item {
|
||||
if state
|
||||
.handle_incoming_event_v1(
|
||||
db, &mut event,
|
||||
)
|
||||
.await
|
||||
&& write.lock().await
|
||||
.send(config.encode(&event))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
warn!("Failed to deserialise an event for {channel}!");
|
||||
}
|
||||
}
|
||||
// No more data, assume we disconnected or otherwise
|
||||
// something bad occurred, so disconnect user.
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker =
|
||||
async {
|
||||
while let Ok(Some(msg)) = read.try_next().await {
|
||||
if let Ok(payload) = config.decode(&msg) {
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(
|
||||
&EventV1::Pong { data },
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
|
||||
// Wait for either disconnect or for listener to die.
|
||||
select!(
|
||||
() = listener => {},
|
||||
() = worker => {}
|
||||
);
|
||||
|
||||
// * Combine the streams back once we are ready to disconnect.
|
||||
/* ws = read.reunite(write).unwrap(); */
|
||||
}
|
||||
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
|
||||
// If this was the last session, notify other users that we just went offline.
|
||||
if last_session {
|
||||
state.broadcast_presence_change(false).await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
write.lock().await.send(config.encode(&err)).await.ok();
|
||||
}
|
||||
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Disconnect the WebSocket if it isn't already.
|
||||
/*ws.close(Some(CloseFrame {
|
||||
code: CloseCode::Normal,
|
||||
reason: std::borrow::Cow::from(""),
|
||||
}))
|
||||
.await
|
||||
.unwrap();*/
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
info!("User disconnected from {addr:?}");
|
||||
});
|
||||
let t1 = message_rx.recv().fuse();
|
||||
let t2 = topic_signal_r.recv().fuse();
|
||||
let t3 = kill_signal_r.recv().fuse();
|
||||
let t4 = clean_up_r.recv().fuse();
|
||||
|
||||
pin_mut!(t1, t2, t3, t4);
|
||||
|
||||
select! {
|
||||
_ = t4 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t3 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t2 => {},
|
||||
message = t1 => {
|
||||
// Handle incoming events.
|
||||
let message = match report_internal_error!(message) {
|
||||
Ok(message) => message,
|
||||
Err(_) => break 'out
|
||||
};
|
||||
|
||||
let event = match *REDIS_PAYLOAD_TYPE {
|
||||
PayloadType::Json => message
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|s| report_internal_error!(serde_json::from_str::<EventV1>(s.as_ref())).ok()),
|
||||
PayloadType::Msgpack => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| report_internal_error!(rmp_serde::from_slice::<EventV1>(b)).ok()),
|
||||
PayloadType::Bincode => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| report_internal_error!(bincode::deserialize::<EventV1>(b)).ok()),
|
||||
};
|
||||
|
||||
let Some(mut event) = event else {
|
||||
let err = format!(
|
||||
"Failed to deserialise event for {}: `{:?}`",
|
||||
message.channel,
|
||||
message
|
||||
.value
|
||||
);
|
||||
|
||||
error!("{}", err);
|
||||
sentry::capture_message(&err, Level::Error);
|
||||
break 'out;
|
||||
};
|
||||
|
||||
if let EventV1::Auth(auth) = &event {
|
||||
if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
|
||||
if &state.session_id == session_id {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else if let AuthifierEvent::DeleteAllSessions {
|
||||
exclude_session_id, ..
|
||||
} = auth
|
||||
{
|
||||
if let Some(excluded) = exclude_session_id {
|
||||
if &state.session_id != excluded {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let result = write.lock().await.send(config.encode(&event)).await;
|
||||
if let Err(e) = result {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while sending an event to {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
break 'out;
|
||||
}
|
||||
|
||||
if let EventV1::Logout = event {
|
||||
info!("User {addr:?} received log out event!");
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
report_internal_error!(subscriber.quit().await).ok();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker_with_kill_signal(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
worker(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id,
|
||||
config,
|
||||
topic_signal_s,
|
||||
kill_signal_r,
|
||||
read,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let revolt_config = revolt_config::config().await;
|
||||
|
||||
loop {
|
||||
let t1 = read.try_next().fuse();
|
||||
let t2 = kill_signal_r.recv().fuse();
|
||||
|
||||
pin_mut!(t1, t2);
|
||||
|
||||
select! {
|
||||
_ = t2 => {
|
||||
return;
|
||||
},
|
||||
result = t1 => {
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => {
|
||||
warn!("Received a None message!");
|
||||
sentry::capture_message("Received a None message!", Level::Warning);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while reading an event from {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
if revolt_config.disable_events_dont_use {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if revolt_config.disable_events_dont_use {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Subscribe { server_id } => {
|
||||
let mut servers = active_servers.lock().await;
|
||||
let has_item = servers.contains_key(&server_id);
|
||||
servers.insert(server_id, ());
|
||||
|
||||
if !has_item {
|
||||
// Poke the listener to adjust subscriptions
|
||||
topic_signal_s.send(()).await.ok();
|
||||
}
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Pong { data }))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
crates/core/coalesced/Cargo.toml
Normal file
28
crates/core/coalesced/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "revolt-coalesced"
|
||||
version = "0.11.5"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
|
||||
description = "Revolt Backend: Coalescion service"
|
||||
repository = "https://github.com/stoatchat/stoatchat"
|
||||
|
||||
[features]
|
||||
tokio = ["dep:tokio"]
|
||||
queue = ["dep:indexmap"]
|
||||
cache = ["dep:lru"]
|
||||
|
||||
default = ["tokio"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.47.0", features = ["sync"], optional = true }
|
||||
indexmap = { version = "2.13.0", optional = true }
|
||||
lru = { version = "0.16.3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.47.0", features = [
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
] }
|
||||
9
crates/core/coalesced/LICENSE
Normal file
9
crates/core/coalesced/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
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.
|
||||
24
crates/core/coalesced/src/config.rs
Normal file
24
crates/core/coalesced/src/config.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
/// Config values for [`CoalescionService`].
|
||||
pub struct CoalescionServiceConfig {
|
||||
/// How many tasks are running at once
|
||||
pub max_concurrent: Option<usize>,
|
||||
/// Whether to queue tasks once `max_concurrent` is reached
|
||||
#[cfg(feature = "queue")]
|
||||
pub queue_requests: bool,
|
||||
/// Max amount of tasks in the buffer queue
|
||||
#[cfg(feature = "queue")]
|
||||
pub max_queue: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for CoalescionServiceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_concurrent: Some(100),
|
||||
#[cfg(feature = "queue")]
|
||||
queue_requests: true,
|
||||
#[cfg(feature = "queue")]
|
||||
max_queue: Some(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
crates/core/coalesced/src/error.rs
Normal file
27
crates/core/coalesced/src/error.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
/// Coalescion service error.
|
||||
pub enum Error {
|
||||
/// Failed to receive the actions return from the channel for unknown reason
|
||||
RecvError,
|
||||
/// Reached the `max_concurrent` amount of actions running at once and could not queue the action
|
||||
MaxConcurrent,
|
||||
/// Reached the `max_queue` amount of actions in the queue
|
||||
MaxQueue,
|
||||
/// Failed to downcast the type to the current type being returned, this will be most likely an ID collision
|
||||
DowncastError,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::RecvError => write!(f, "Unable to receive data from the channel"),
|
||||
Error::MaxConcurrent => write!(f, "Max number of tasks running at once"),
|
||||
Error::MaxQueue => write!(f, "Max number of tasks in queue"),
|
||||
Error::DowncastError => write!(f, "Failed to downcast type, possible key collision with different types")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
39
crates/core/coalesced/src/lib.rs
Normal file
39
crates/core/coalesced/src/lib.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! # Coalesced
|
||||
//!
|
||||
//! Coalescion service to group, caching and queue duplicate actions.
|
||||
//! useful for deduplicating web requests, database lookups and other similar resource
|
||||
//! intensive or rate-limited actions.
|
||||
//!
|
||||
//! ## Features
|
||||
//! - `tokio`: Uses tokio for the async backend, this is currently the only backend.
|
||||
//! - `queue`: Whether to support queueing requests to only allow X amount of actions running at once.
|
||||
//! - `cache`: Whether to cache the actions results for future actions with the same id, uses an LRU cache internally.
|
||||
//!
|
||||
//! [`CoalescionService`] uses both [`Arc`] and [`RwLock`] internally and can be cheaply cloned to
|
||||
//! use in your codebase.
|
||||
//!
|
||||
//! It is common practice to wrap the service and in your own which delegates the executions to ensure all ids are tracked in one location across your codebase.
|
||||
//!
|
||||
//! All values are stored using [`Any`] and must be [`'static`] + [`Send`] + [`Sync`], if there is an id mismatch
|
||||
//! and a type is wrong the library will return an error, values returned from the service are also
|
||||
//! wrapped in an [`Arc`] as they are shared to each duplicate action.
|
||||
//!
|
||||
//! ## Example:
|
||||
//! ```rs
|
||||
//! use revolt_coalesced::CoalescionService;
|
||||
//!
|
||||
//! let service = CoalescionService::new();
|
||||
//!
|
||||
//! let user_id = "my_user_id";
|
||||
//! let user = service.execute(user_id, || async move {
|
||||
//! database.fetch_user(user_id).await.unwrap()
|
||||
//! }).await;
|
||||
//! ```
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
mod service;
|
||||
|
||||
pub use config::CoalescionServiceConfig;
|
||||
pub use error::Error;
|
||||
pub use service::CoalescionService;
|
||||
208
crates/core/coalesced/src/service.rs
Normal file
208
crates/core/coalesced/src/service.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc};
|
||||
|
||||
use tokio::sync::{
|
||||
watch::{channel as watch_channel, Receiver},
|
||||
RwLock,
|
||||
};
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
use lru::LruCache;
|
||||
|
||||
#[cfg(feature = "queue")]
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::{CoalescionServiceConfig, Error};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// # Coalescion service
|
||||
///
|
||||
/// See module description for example usage.
|
||||
pub struct CoalescionService<Id: Hash + Clone + Eq> {
|
||||
config: Arc<CoalescionServiceConfig>,
|
||||
watchers: Arc<RwLock<HashMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
|
||||
#[cfg(feature = "queue")]
|
||||
queue: Arc<RwLock<IndexMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
|
||||
#[cfg(feature = "cache")]
|
||||
cache: Option<Arc<tokio::sync::Mutex<LruCache<Id, Arc<dyn Any + Send + Sync>>>>>,
|
||||
}
|
||||
|
||||
impl<Id: Hash + Clone + Eq> CoalescionService<Id> {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn from_config(config: CoalescionServiceConfig) -> Self {
|
||||
Self {
|
||||
config: Arc::new(config),
|
||||
watchers: Arc::new(RwLock::new(HashMap::new())),
|
||||
#[cfg(feature = "queue")]
|
||||
queue: Arc::new(RwLock::new(IndexMap::new())),
|
||||
#[cfg(feature = "cache")]
|
||||
cache: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
pub fn from_cache(
|
||||
config: CoalescionServiceConfig,
|
||||
cache: LruCache<Id, Arc<dyn Any + Send + Sync>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cache: Some(Arc::new(Mutex::new(cache))),
|
||||
..Self::from_config(config)
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for<Value: Any + Send + Sync>(
|
||||
&self,
|
||||
mut receiver: Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>,
|
||||
) -> Result<Arc<Value>, Error> {
|
||||
receiver
|
||||
.wait_for(|v| v.is_some())
|
||||
.await
|
||||
.map_err(|_| Error::RecvError)
|
||||
.and_then(|r| r.clone().unwrap())
|
||||
.and_then(|arc| Arc::downcast(arc).map_err(|_| Error::DowncastError))
|
||||
}
|
||||
|
||||
async fn insert_and_execute<
|
||||
Value: Send + Sync + 'static,
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = Value>,
|
||||
>(
|
||||
&self,
|
||||
id: Id,
|
||||
func: F,
|
||||
) -> Result<Arc<Value>, Error> {
|
||||
let (send, recv) = watch_channel(None);
|
||||
|
||||
self.watchers.write().await.insert(id.clone(), recv);
|
||||
|
||||
let value = Ok(Arc::new(func().await));
|
||||
|
||||
send.send_modify(|opt| {
|
||||
opt.replace(value.clone().map(|v| v as Arc<dyn Any + Send + Sync>));
|
||||
});
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
if let Some(cache) = self.cache.as_ref() {
|
||||
if let Ok(value) = &value {
|
||||
cache.lock().await.push(id.clone(), value.clone());
|
||||
}
|
||||
};
|
||||
|
||||
self.watchers.write().await.remove(&id);
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
/// Coalesces an function, the actual function may not run if one with the same id is already running,
|
||||
/// queued to be ran, or cached, the id should be globally unique for this specific action.
|
||||
pub async fn execute<
|
||||
Value: Send + Sync + 'static,
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = Value>,
|
||||
>(
|
||||
&self,
|
||||
id: Id,
|
||||
func: F,
|
||||
) -> Result<Arc<Value>, Error> {
|
||||
#[cfg(feature = "cache")]
|
||||
if let Some(cache) = self.cache.as_ref() {
|
||||
if let Some(value) = cache.lock().await.get(&id) {
|
||||
return Arc::downcast::<Value>(value.clone()).map_err(|_| Error::DowncastError);
|
||||
}
|
||||
};
|
||||
|
||||
let (receiver, length) = {
|
||||
let watchers = self.watchers.read().await;
|
||||
let length = watchers.len();
|
||||
|
||||
(watchers.get(&id).cloned(), length)
|
||||
};
|
||||
|
||||
if let Some(receiver) = receiver {
|
||||
self.wait_for(receiver).await
|
||||
} else {
|
||||
match self.config.max_concurrent {
|
||||
Some(max_concurrent) if length >= max_concurrent => {
|
||||
#[cfg(feature = "queue")]
|
||||
if self.config.queue_requests {
|
||||
let (receiver, length) = {
|
||||
let queue = self.queue.read().await;
|
||||
|
||||
(queue.get(&id).cloned(), queue.len())
|
||||
};
|
||||
|
||||
if let Some(receiver) = receiver {
|
||||
return self.wait_for(receiver).await;
|
||||
} else {
|
||||
if self
|
||||
.config
|
||||
.max_queue
|
||||
.is_some_and(|max_queue| max_queue >= length)
|
||||
{
|
||||
return Err(Error::MaxQueue);
|
||||
};
|
||||
|
||||
let (send, recv) = watch_channel(None);
|
||||
|
||||
self.queue.write().await.insert(id.clone(), recv);
|
||||
|
||||
loop {
|
||||
let length = self.watchers.read().await.len();
|
||||
|
||||
if length < max_concurrent {
|
||||
let first_key = {
|
||||
let queue = self.queue.read().await;
|
||||
queue.first().map(|v| v.0).cloned()
|
||||
};
|
||||
|
||||
if first_key == Some(id.clone()) {
|
||||
self.queue.write().await.shift_remove(&id);
|
||||
|
||||
let response = self.insert_and_execute(id, func).await;
|
||||
|
||||
send.send_modify(|opt| {
|
||||
opt.replace(
|
||||
response
|
||||
.clone()
|
||||
.map(|v| v as Arc<dyn Any + Send + Sync>),
|
||||
);
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(Error::MaxConcurrent)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "queue"))]
|
||||
Err(Error::MaxConcurrent)
|
||||
}
|
||||
_ => self.insert_and_execute(id, func).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the amount of currently running tasks
|
||||
pub async fn current_task_count(&self) -> usize {
|
||||
self.watchers.read().await.len()
|
||||
}
|
||||
|
||||
#[cfg(feature = "queue")]
|
||||
/// Fetches the current length of the queue
|
||||
pub async fn current_queue_len(&self) -> usize {
|
||||
self.queue.read().await.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Id: Hash + Clone + Eq> Default for CoalescionService<Id> {
|
||||
fn default() -> Self {
|
||||
Self::from_config(CoalescionServiceConfig::default())
|
||||
}
|
||||
}
|
||||
41
crates/core/config/Cargo.toml
Normal file
41
crates/core/config/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.11.5"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Configuration"
|
||||
repository = "https://github.com/stoatchat/stoatchat"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
anyhow = ["dep:sentry-anyhow"]
|
||||
report-macros = ["revolt-result"]
|
||||
sentry = ["dep:sentry"]
|
||||
test = ["async-std"]
|
||||
default = ["test", "sentry"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
config = "0.13.3"
|
||||
cached = "0.44.0"
|
||||
once_cell = "1.18.0"
|
||||
|
||||
# Serde
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
# Async
|
||||
futures-locks = "0.7.1"
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
|
||||
# Logging
|
||||
log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
# Sentry
|
||||
sentry = { version = "0.31.5", optional = true }
|
||||
sentry-anyhow = { version = "0.38.1", optional = true }
|
||||
|
||||
# Core
|
||||
revolt-result = { version = "0.11.5", path = "../result", optional = true }
|
||||
9
crates/core/config/LICENSE
Normal file
9
crates/core/config/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
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.
|
||||
12
crates/core/config/Revolt.test.toml
Normal file
12
crates/core/config/Revolt.test.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[database]
|
||||
mongodb = "mongodb://localhost?directConnection=true&replicaSet=rs0"
|
||||
redis = "redis://localhost/"
|
||||
|
||||
[rabbit]
|
||||
host = "127.0.0.1"
|
||||
port = 5672
|
||||
username = "rabbituser"
|
||||
password = "rabbitpass"
|
||||
|
||||
[features]
|
||||
webhooks_enabled = true
|
||||
325
crates/core/config/Revolt.toml
Normal file
325
crates/core/config/Revolt.toml
Normal file
@@ -0,0 +1,325 @@
|
||||
production = false
|
||||
disable_events_dont_use = false
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://database?directConnection=true&replicaSet=rs0"
|
||||
# Redis connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
redis = "redis://redis/"
|
||||
|
||||
[hosts]
|
||||
# Web locations of various services
|
||||
# Defaults assume all services are reverse-proxied
|
||||
# See https://github.com/stoatchat/self-hosted/blob/main/Caddyfile
|
||||
#
|
||||
# Remember to change these to https/wss where appropriate in production!
|
||||
app = "http://local.revolt.chat"
|
||||
api = "http://local.revolt.chat/api"
|
||||
events = "ws://local.revolt.chat/ws"
|
||||
autumn = "http://local.revolt.chat/autumn"
|
||||
january = "http://local.revolt.chat/january"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
[hosts.livekit]
|
||||
|
||||
[rabbit]
|
||||
host = "rabbit"
|
||||
port = 5672
|
||||
username = "rabbituser"
|
||||
password = "rabbitpass"
|
||||
|
||||
[api]
|
||||
|
||||
[api.registration]
|
||||
# Whether an invite should be required for registration
|
||||
# See https://github.com/stoatchat/self-hosted#making-your-instance-invite-only
|
||||
invite_only = false
|
||||
|
||||
[api.smtp]
|
||||
# Email server configuration for verification
|
||||
# Defaults to no email verification (host field is empty)
|
||||
host = ""
|
||||
username = ""
|
||||
password = ""
|
||||
from_address = "noreply@example.com"
|
||||
# reply_to = "noreply@example.com"
|
||||
# port = 587
|
||||
# use_tls = true
|
||||
|
||||
|
||||
[api.security]
|
||||
# Authifier Shield API key
|
||||
authifier_shield_key = ""
|
||||
# Legacy voice server management token
|
||||
voso_legacy_token = ""
|
||||
# Whether services are behind the Cloudflare network
|
||||
trust_cloudflare = false
|
||||
# easypwned endpoint
|
||||
easypwned = ""
|
||||
# Tenor API Key
|
||||
tenor_key = ""
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
hcaptcha_key = ""
|
||||
hcaptcha_sitekey = ""
|
||||
|
||||
[api.workers]
|
||||
# Maximum concurrent connections (to proxy server)
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[api.livekit]
|
||||
# How long to ring devices for when calling in dms/groups, in seconds
|
||||
call_ring_duration = 30
|
||||
|
||||
[api.livekit.nodes]
|
||||
|
||||
[api.users]
|
||||
|
||||
[pushd]
|
||||
# this changes the names of the queues to not overlap
|
||||
# prod/beta if they happen to be on the same exchange/instance.
|
||||
# Usually they have to be, so that messages sent from one or the other get sent to everyone
|
||||
production = true
|
||||
|
||||
# Changes how many users are processed in each chunk when resolving role/everyone mentions.
|
||||
# Increasing this will resolve mentions faster, but will consume more memory while resolving.
|
||||
mass_mention_chunk_size = 200
|
||||
|
||||
# How long pushd will cache resolved names for rendered message notifications.
|
||||
# Increasing this will result in lower database usage, but may result in a situation where a user/channel/role name changes
|
||||
# and the notifications still resolve to the old name.
|
||||
render_cache_time = 60
|
||||
|
||||
# none of these should need changing
|
||||
exchange = "revolt.notifications"
|
||||
message_queue = "notifications.origin.message"
|
||||
mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions
|
||||
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
|
||||
fr_received_queue = "notifications.ingest.fr_received" # friend request received
|
||||
dm_call_queue = "notifications.ingest.dm_call" # direct message voice call
|
||||
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
|
||||
ack_queue = "notifications.process.ack" # updates badges for apple devices
|
||||
|
||||
|
||||
[pushd.vapid]
|
||||
queue = "notifications.outbound.vapid"
|
||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
||||
|
||||
[pushd.fcm]
|
||||
queue = "notifications.outbound.fcm"
|
||||
key_type = ""
|
||||
project_id = ""
|
||||
private_key_id = ""
|
||||
private_key = ""
|
||||
client_email = ""
|
||||
client_id = ""
|
||||
auth_uri = ""
|
||||
token_uri = ""
|
||||
auth_provider_x509_cert_url = ""
|
||||
client_x509_cert_url = ""
|
||||
|
||||
[pushd.apn]
|
||||
sandbox = false
|
||||
queue = "notifications.outbound.apn"
|
||||
pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
|
||||
[files]
|
||||
# Encryption key for stored files
|
||||
# Generate your own key using `openssl rand -base64 32`
|
||||
encryption_key = "qcuMA+ssxhMyKaNAKBGFfryfFtUH8NDlamQyDwGW6fU="
|
||||
# Quality used for lossy WebP previews (set to 100 for lossless)
|
||||
webp_quality = 80.0
|
||||
# Mime types that cannot be uploaded or served
|
||||
#
|
||||
# Example for Windows executables and Android installation files:
|
||||
# ["application/vnd.microsoft.portable-executable", "application/vnd.android.package-archive"]
|
||||
blocked_mime_types = []
|
||||
# ClamAV service
|
||||
# hostname:port
|
||||
clamd_host = ""
|
||||
# Mime types that should be virus scanned
|
||||
#
|
||||
# Leave empty to scan all file types
|
||||
scan_mime_types = [
|
||||
"application/vnd.microsoft.portable-executable",
|
||||
"application/vnd.android.package-archive",
|
||||
"application/zip",
|
||||
]
|
||||
|
||||
[files.limit]
|
||||
# Minimum file size (in bytes)
|
||||
min_file_size = 1
|
||||
# Minimum image resolution
|
||||
min_resolution = [1, 1]
|
||||
# Maximum MP of images
|
||||
max_mega_pixels = 40
|
||||
# Maximum pixel side of an image
|
||||
max_pixel_side = 10_000
|
||||
|
||||
[files.preview]
|
||||
# Maximum image resolution
|
||||
attachments = [1280, 1280]
|
||||
avatars = [128, 128]
|
||||
backgrounds = [1280, 720]
|
||||
icons = [128, 128]
|
||||
banners = [480, 480]
|
||||
emojis = [128, 128]
|
||||
|
||||
[files.s3]
|
||||
# Configuration for S3
|
||||
# Defaults included for MinIO + self-hosted setup
|
||||
#
|
||||
# Backblaze B2:
|
||||
# - endpoint is listed on the "Buckets" page
|
||||
# - path_style_buckets is set to true
|
||||
# - region is `eu-central-003` string from endpoint URL
|
||||
# - access_key_id is keyID generated on the "Application Keys" page
|
||||
# - secret_access_key is token generated on the "Application Keys" page
|
||||
# - default_bucket matches the name of the bucket you've created
|
||||
|
||||
# S3 protocol endpoint
|
||||
endpoint = "http://minio:9000"
|
||||
# Whether to use path-style buckets
|
||||
# Generally true, except for MinIO
|
||||
path_style_buckets = false
|
||||
# S3 region name
|
||||
region = "minio"
|
||||
# S3 protocol key ID
|
||||
access_key_id = "minioautumn"
|
||||
# S3 protocol access key
|
||||
secret_access_key = "minioautumn"
|
||||
default_bucket = "revolt-uploads"
|
||||
|
||||
|
||||
[features]
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
# Enable push notifications for mass pings (everyone, online, roles)
|
||||
# When false this will still ping in-client but will not send notifications from pushd
|
||||
mass_mentions_send_notifications = true
|
||||
# Can role/everyone pings be used at all
|
||||
mass_mentions_enabled = true
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.global]
|
||||
group_size = 100
|
||||
message_embeds = 5
|
||||
message_replies = 5
|
||||
message_reactions = 20
|
||||
server_emoji = 100
|
||||
server_roles = 200
|
||||
server_channels = 200
|
||||
|
||||
# How many hours since creation a user is considered new
|
||||
new_user_hours = 72
|
||||
|
||||
# Maximum permissible body size in bytes for uploads
|
||||
# (should be greater than any one file upload limit)
|
||||
body_limit_size = 20_000_000
|
||||
|
||||
# If any userids are entered here, only those users will be able to create servers.
|
||||
# Leave empty to allow all users to create servers
|
||||
restrict_server_creation = []
|
||||
|
||||
[features.limits.new_user]
|
||||
# Limits imposed on new users
|
||||
|
||||
# Number of outgoing friend requests permitted at any time
|
||||
outgoing_friend_requests = 5
|
||||
|
||||
# Maximum number of owned bots
|
||||
bots = 2
|
||||
|
||||
# Message content length
|
||||
message_length = 2000
|
||||
|
||||
# Number of attachments that can be included
|
||||
message_attachments = 5
|
||||
|
||||
# Maximum number of servers the user can create/join
|
||||
servers = 50
|
||||
|
||||
# Maximum audio frequency (Hz) in voice calls
|
||||
voice_quality = 16000
|
||||
|
||||
# Whether the user can use video streams in voice calls
|
||||
video = true
|
||||
|
||||
# Maximum resolution (width, height) of video streams in voice calls
|
||||
video_resolution = [1080, 720]
|
||||
|
||||
# Minimum and maximum aspect ratio of video streams in voice calls
|
||||
video_aspect_ratio = [0.3, 2.5]
|
||||
|
||||
[features.limits.new_user.file_upload_size_limit]
|
||||
# Maximum file size limits (in bytes)
|
||||
attachments = 20_000_000
|
||||
avatars = 4_000_000
|
||||
backgrounds = 6_000_000
|
||||
icons = 2_500_000
|
||||
banners = 6_000_000
|
||||
emojis = 500_000
|
||||
|
||||
[features.limits.default]
|
||||
# Limits imposed on users by default
|
||||
|
||||
# Number of outgoing friend requests permitted at any time
|
||||
outgoing_friend_requests = 10
|
||||
|
||||
# Maximum number of owned bots
|
||||
bots = 5
|
||||
|
||||
# Message content length
|
||||
message_length = 2000
|
||||
|
||||
# Number of attachments that can be included
|
||||
message_attachments = 5
|
||||
|
||||
# Maximum number of servers the user can create/join
|
||||
servers = 100
|
||||
|
||||
# Maximum audio frequency (Hz) in voice calls
|
||||
voice_quality = 16000
|
||||
|
||||
# Whether the user can use video streams in voice calls
|
||||
video = true
|
||||
|
||||
# Maximum resolution (width, height) of video streams in voice calls
|
||||
video_resolution = [1280, 720]
|
||||
|
||||
# Minimum and maximum aspect ratio of video streams in voice calls
|
||||
video_aspect_ratio = [0.3, 2.5]
|
||||
|
||||
[features.limits.default.file_upload_size_limit]
|
||||
# Maximum file size limits (in bytes)
|
||||
attachments = 20_000_000
|
||||
avatars = 4_000_000
|
||||
backgrounds = 6_000_000
|
||||
icons = 2_500_000
|
||||
banners = 6_000_000
|
||||
emojis = 500_000
|
||||
|
||||
[features.advanced]
|
||||
# The max amount of messages the rabbitmq provider/db mention adder job will delay for before forcing handling of a channel.
|
||||
# default: 5
|
||||
process_message_delay_limit = 5
|
||||
|
||||
[sentry]
|
||||
# Configuration for Sentry error reporting
|
||||
api = ""
|
||||
events = ""
|
||||
voice_ingress = ""
|
||||
files = ""
|
||||
proxy = ""
|
||||
pushd = ""
|
||||
crond = ""
|
||||
gifbox = ""
|
||||
521
crates/core/config/src/lib.rs
Normal file
521
crates/core/config/src/lib.rs
Normal file
@@ -0,0 +1,521 @@
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
use config::{Config, Environment, File, FileFormat};
|
||||
use futures_locks::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
pub use sentry::{capture_error, capture_message, Level};
|
||||
#[cfg(feature = "anyhow")]
|
||||
pub use sentry_anyhow::capture_anyhow;
|
||||
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! report_error {
|
||||
( $expr: expr, $error: ident $( $tt:tt )? ) => {
|
||||
$expr
|
||||
.inspect_err(|err| {
|
||||
$crate::capture_message(
|
||||
&format!("{err:?} ({}:{}:{})", file!(), line!(), column!()),
|
||||
$crate::Level::Error,
|
||||
);
|
||||
})
|
||||
.map_err(|_| ::revolt_result::create_error!($error))
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! capture_internal_error {
|
||||
( $expr: expr ) => {
|
||||
$crate::capture_message(
|
||||
&format!("{:?} ({}:{}:{})", $expr, file!(), line!(), column!()),
|
||||
$crate::Level::Error,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! report_internal_error {
|
||||
( $expr: expr ) => {
|
||||
$expr
|
||||
.inspect_err(|err| {
|
||||
$crate::capture_message(
|
||||
&format!("{err:?} ({}:{}:{})", file!(), line!(), column!()),
|
||||
$crate::Level::Error,
|
||||
);
|
||||
})
|
||||
.map_err(|_| ::revolt_result::create_error!(InternalError))
|
||||
};
|
||||
}
|
||||
|
||||
/// Paths to search for configuration
|
||||
static CONFIG_SEARCH_PATHS: [&str; 3] = [
|
||||
// current working directory
|
||||
"Revolt.toml",
|
||||
// current working directory - overrides file
|
||||
"Revolt.overrides.toml",
|
||||
// root directory, for Docker containers
|
||||
"/Revolt.toml",
|
||||
];
|
||||
|
||||
/// Path to search for test overrides
|
||||
static TEST_OVERRIDE_PATH: &str = "Revolt.test-overrides.toml";
|
||||
|
||||
/// Configuration builder
|
||||
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
RwLock::new({
|
||||
let mut builder = Config::builder().add_source(File::from_str(
|
||||
include_str!("../Revolt.toml"),
|
||||
FileFormat::Toml,
|
||||
));
|
||||
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
builder = builder.add_source(File::from_str(
|
||||
include_str!("../Revolt.test.toml"),
|
||||
FileFormat::Toml,
|
||||
));
|
||||
|
||||
// recursively search upwards for an overrides file (if there is one)
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
let mut path = Some(cwd.as_path());
|
||||
while let Some(current_path) = path {
|
||||
let target_path = current_path.join(TEST_OVERRIDE_PATH);
|
||||
if target_path.exists() {
|
||||
builder = builder
|
||||
.add_source(File::new(target_path.to_str().unwrap(), FileFormat::Toml));
|
||||
}
|
||||
|
||||
path = current_path.parent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let mut cwd: Option<&Path> = Some(&cwd);
|
||||
|
||||
while let Some(path) = cwd {
|
||||
for config_path in CONFIG_SEARCH_PATHS {
|
||||
let config_path = path.join(config_path);
|
||||
if config_path.exists() {
|
||||
builder = builder
|
||||
.add_source(File::new(config_path.to_str().unwrap(), FileFormat::Toml));
|
||||
}
|
||||
}
|
||||
|
||||
cwd = path.parent();
|
||||
}
|
||||
|
||||
builder = builder.add_source(Environment::with_prefix("REVOLT").separator("__"));
|
||||
|
||||
builder.build().unwrap()
|
||||
})
|
||||
});
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Database {
|
||||
pub mongodb: String,
|
||||
pub redis: String,
|
||||
pub redis_pubsub: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Rabbit {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Hosts {
|
||||
pub app: String,
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
pub autumn: String,
|
||||
pub january: String,
|
||||
pub livekit: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiRegistration {
|
||||
pub invite_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSmtp {
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub from_address: String,
|
||||
pub reply_to: Option<String>,
|
||||
pub port: Option<i32>,
|
||||
pub use_tls: Option<bool>,
|
||||
pub use_starttls: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct PushVapid {
|
||||
pub queue: String,
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct PushFcm {
|
||||
pub queue: String,
|
||||
pub key_type: String,
|
||||
pub project_id: String,
|
||||
pub private_key_id: String,
|
||||
pub private_key: String,
|
||||
pub client_email: String,
|
||||
pub client_id: String,
|
||||
pub auth_uri: String,
|
||||
pub token_uri: String,
|
||||
pub auth_provider_x509_cert_url: String,
|
||||
pub client_x509_cert_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct PushApn {
|
||||
pub queue: String,
|
||||
pub sandbox: bool,
|
||||
pub pkcs8: String,
|
||||
pub key_id: String,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurityCaptcha {
|
||||
pub hcaptcha_key: String,
|
||||
pub hcaptcha_sitekey: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurity {
|
||||
pub authifier_shield_key: String,
|
||||
pub voso_legacy_token: String,
|
||||
pub captcha: ApiSecurityCaptcha,
|
||||
pub trust_cloudflare: bool,
|
||||
pub easypwned: String,
|
||||
pub tenor_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiWorkers {
|
||||
pub max_concurrent_connections: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiLiveKit {
|
||||
pub call_ring_duration: usize,
|
||||
pub nodes: HashMap<String, LiveKitNode>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct LiveKitNode {
|
||||
pub url: String,
|
||||
pub lat: f64,
|
||||
pub lon: f64,
|
||||
pub key: String,
|
||||
pub secret: String,
|
||||
|
||||
// whether to hide the node in the nodes list
|
||||
#[serde(default)]
|
||||
pub private: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiUsers {
|
||||
pub early_adopter_cutoff: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Api {
|
||||
pub registration: ApiRegistration,
|
||||
pub smtp: ApiSmtp,
|
||||
pub security: ApiSecurity,
|
||||
pub workers: ApiWorkers,
|
||||
pub livekit: ApiLiveKit,
|
||||
pub users: ApiUsers,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Pushd {
|
||||
pub production: bool,
|
||||
pub exchange: String,
|
||||
pub mass_mention_chunk_size: usize,
|
||||
pub render_cache_time: usize,
|
||||
|
||||
// Queues
|
||||
pub message_queue: String,
|
||||
pub mass_mention_queue: String,
|
||||
pub dm_call_queue: String,
|
||||
pub fr_accepted_queue: String,
|
||||
pub fr_received_queue: String,
|
||||
pub generic_queue: String,
|
||||
pub ack_queue: String,
|
||||
|
||||
pub vapid: PushVapid,
|
||||
pub fcm: PushFcm,
|
||||
pub apn: PushApn,
|
||||
}
|
||||
|
||||
impl Pushd {
|
||||
fn get_routing_key(&self, key: String) -> String {
|
||||
match self.production {
|
||||
true => key + "-prd",
|
||||
false => key + "-tst",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ack_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.ack_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_message_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.message_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_mass_mention_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.mass_mention_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_dm_call_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.dm_call_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_fr_accepted_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.fr_accepted_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_fr_received_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.fr_received_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_generic_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.generic_queue.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FilesLimit {
|
||||
pub min_file_size: usize,
|
||||
pub min_resolution: [usize; 2],
|
||||
pub max_mega_pixels: usize,
|
||||
pub max_pixel_side: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FilesS3 {
|
||||
pub endpoint: String,
|
||||
pub path_style_buckets: bool,
|
||||
pub region: String,
|
||||
pub access_key_id: String,
|
||||
pub secret_access_key: String,
|
||||
pub default_bucket: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Files {
|
||||
pub encryption_key: String,
|
||||
pub webp_quality: f32,
|
||||
pub blocked_mime_types: Vec<String>,
|
||||
pub clamd_host: String,
|
||||
pub scan_mime_types: Vec<String>,
|
||||
|
||||
pub limit: FilesLimit,
|
||||
pub preview: HashMap<String, [usize; 2]>,
|
||||
pub s3: FilesS3,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct GlobalLimits {
|
||||
pub group_size: usize,
|
||||
pub message_embeds: usize,
|
||||
pub message_replies: usize,
|
||||
pub message_reactions: usize,
|
||||
pub server_emoji: usize,
|
||||
pub server_roles: usize,
|
||||
pub server_channels: usize,
|
||||
|
||||
pub new_user_hours: usize,
|
||||
|
||||
pub body_limit_size: usize,
|
||||
|
||||
pub restrict_server_creation: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimits {
|
||||
pub outgoing_friend_requests: usize,
|
||||
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
pub voice_quality: u32,
|
||||
pub video: bool,
|
||||
pub video_resolution: [u32; 2],
|
||||
pub video_aspect_ratio: [f32; 2],
|
||||
|
||||
pub file_upload_size_limit: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimitsCollection {
|
||||
pub global: GlobalLimits,
|
||||
|
||||
pub new_user: FeaturesLimits,
|
||||
pub default: FeaturesLimits,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub roles: HashMap<String, FeaturesLimits>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesAdvanced {
|
||||
#[serde(default)]
|
||||
pub process_message_delay_limit: u16,
|
||||
}
|
||||
|
||||
impl Default for FeaturesAdvanced {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
process_message_delay_limit: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Features {
|
||||
pub limits: FeaturesLimitsCollection,
|
||||
pub webhooks_enabled: bool,
|
||||
pub mass_mentions_send_notifications: bool,
|
||||
pub mass_mentions_enabled: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub advanced: FeaturesAdvanced,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Sentry {
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
pub voice_ingress: String,
|
||||
pub files: String,
|
||||
pub proxy: String,
|
||||
pub pushd: String,
|
||||
pub crond: String,
|
||||
pub gifbox: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Settings {
|
||||
pub database: Database,
|
||||
pub rabbit: Rabbit,
|
||||
pub hosts: Hosts,
|
||||
pub api: Api,
|
||||
pub pushd: Pushd,
|
||||
pub files: Files,
|
||||
pub features: Features,
|
||||
pub sentry: Sentry,
|
||||
pub production: bool,
|
||||
pub disable_events_dont_use: bool,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn preflight_checks(&self) {
|
||||
if self.api.smtp.host.is_empty() {
|
||||
log::warn!("No SMTP settings specified! Remember to configure email.");
|
||||
}
|
||||
|
||||
if self.api.security.captcha.hcaptcha_key.is_empty() {
|
||||
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init() {
|
||||
println!(
|
||||
":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m",
|
||||
config().await
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn read() -> Config {
|
||||
CONFIG_BUILDER.read().await.clone()
|
||||
}
|
||||
|
||||
#[cached(time = 30)]
|
||||
pub async fn config() -> Settings {
|
||||
let mut config = read().await.try_deserialize::<Settings>().unwrap();
|
||||
|
||||
// inject REDIS_URI for redis-kiss library
|
||||
if std::env::var("REDIS_URI").is_err() {
|
||||
std::env::set_var("REDIS_URI", config.database.redis.clone());
|
||||
}
|
||||
|
||||
// auto-detect production nodes
|
||||
if config.hosts.api.contains("https")
|
||||
&& (config.hosts.api.contains("revolt.chat") || config.hosts.api.contains("stoat.chat"))
|
||||
{
|
||||
config.production = true;
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Configure logging and common Rust variables
|
||||
#[cfg(feature = "sentry")]
|
||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
if std::env::var("ROCKET_ADDRESS").is_err() {
|
||||
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
|
||||
}
|
||||
|
||||
pretty_env_logger::init();
|
||||
log::info!("Starting {release}");
|
||||
|
||||
if dsn.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(sentry::init((
|
||||
dsn,
|
||||
sentry::ClientOptions {
|
||||
release: Some(release.into()),
|
||||
..Default::default()
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
#[macro_export]
|
||||
macro_rules! configure {
|
||||
($application: ident) => {
|
||||
let config = $crate::config().await;
|
||||
let _sentry = $crate::setup_logging(
|
||||
concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")),
|
||||
config.sentry.$application,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::init;
|
||||
|
||||
#[async_std::test]
|
||||
async fn it_works() {
|
||||
init().await;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,66 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.6.0"
|
||||
version = "0.11.5"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Database Implementation"
|
||||
repository = "https://github.com/stoatchat/stoatchat"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
# Databases
|
||||
mongodb = [ "dep:mongodb", "bson" ]
|
||||
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
|
||||
|
||||
# ... Other
|
||||
async-std-runtime = [ "async-std" ]
|
||||
rocket-impl = [ "rocket", "schemars" ]
|
||||
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
|
||||
tasks = ["isahc", "linkify", "url-escape"]
|
||||
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
|
||||
rocket-impl = [
|
||||
"rocket",
|
||||
"schemars",
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi",
|
||||
"authifier/rocket_impl",
|
||||
]
|
||||
axum-impl = ["axum", "revolt-result/axum"]
|
||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||
voice = ["livekit-api", "livekit-protocol", "livekit-runtime"]
|
||||
|
||||
# Default Features
|
||||
default = [ "mongodb", "async-std-runtime" ]
|
||||
default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.6.0", path = "../result" }
|
||||
revolt-models = { version = "0.6.0", path = "../models" }
|
||||
revolt-presence = { version = "0.6.0", path = "../presence" }
|
||||
revolt-permissions = { version = "0.6.0", path = "../permissions", features = [ "serde", "bson" ] }
|
||||
revolt-config = { version = "0.11.5", path = "../config", features = [
|
||||
"report-macros",
|
||||
] }
|
||||
revolt-result = { version = "0.11.5", path = "../result" }
|
||||
revolt-models = { version = "0.11.5", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.11.5", path = "../presence" }
|
||||
revolt-permissions = { version = "0.11.5", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
revolt-parser = { version = "0.11.5", path = "../parser" }
|
||||
|
||||
# Utility
|
||||
log = "0.4"
|
||||
lru = "0.11.0"
|
||||
rand = "0.8.5"
|
||||
ulid = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
base64 = "0.21.3"
|
||||
once_cell = "1.17"
|
||||
indexmap = "1.9.1"
|
||||
decancer = "1.6.2"
|
||||
deadqueue = "0.2.4"
|
||||
linkify = { optional = true, version = "0.8.1" }
|
||||
url-escape = { optional = true, version = "0.1.1" }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
isahc = { optional = true, version = "1.7", features = ["json"] }
|
||||
|
||||
# Serialisation
|
||||
serde_json = "1"
|
||||
@@ -45,7 +73,7 @@ redis-kiss = { version = "0.1.4" }
|
||||
|
||||
# Database
|
||||
bson = { optional = true, version = "2.1.0" }
|
||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
||||
mongodb = { optional = true, version = "3.1.0" }
|
||||
|
||||
# Database Migration
|
||||
unicode-segmentation = "1.10.1"
|
||||
@@ -53,15 +81,31 @@ regex = "1"
|
||||
|
||||
# Async Language Features
|
||||
futures = "0.3.19"
|
||||
async-lock = "2.8.0"
|
||||
async-trait = "0.1.51"
|
||||
async-recursion = "1.0.4"
|
||||
|
||||
# Async
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
|
||||
# Axum Impl
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
|
||||
# Rocket Impl
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"], optional = true }
|
||||
rocket = { version = "0.5.1", default-features = false, features = [
|
||||
"json",
|
||||
], optional = true }
|
||||
revolt_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.10.0", optional = true }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0" }
|
||||
authifier = { version = "1.0.16" }
|
||||
|
||||
# RabbitMQ
|
||||
amqprs = { version = "1.7.0" }
|
||||
|
||||
# Voice
|
||||
livekit-api = { version = "0.4.4", optional = true }
|
||||
livekit-protocol = { version = "0.4.0", optional = true }
|
||||
livekit-runtime = { version = "0.3.1", features = ["tokio"], optional = true }
|
||||
|
||||
34
crates/core/database/fixtures/group_with_members.json
Normal file
34
crates/core/database/fixtures/group_with_members.json
Normal file
@@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Member",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "Member",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0002"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "Group",
|
||||
"name": "My Group",
|
||||
"owner": "__ID:0__",
|
||||
"recipients": [
|
||||
"__ID:0__",
|
||||
"__ID:1__"
|
||||
]
|
||||
}
|
||||
]
|
||||
100
crates/core/database/fixtures/server_with_roles.json
Normal file
100
crates/core/database/fixtures/server_with_roles.json
Normal file
@@ -0,0 +1,100 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Moderator",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "User",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "TextChannel",
|
||||
"name": "General",
|
||||
"server": "__ID:4__",
|
||||
"default_permissions": {
|
||||
"a": 0,
|
||||
"d": 1048576
|
||||
},
|
||||
"role_permissions": {
|
||||
"__ID:5__": {
|
||||
"a": 1048576,
|
||||
"d": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_object_type": "Server",
|
||||
"_id": "__ID:4__",
|
||||
"owner": "__ID:0__",
|
||||
"name": "Server",
|
||||
"channels": [
|
||||
"__ID:3__"
|
||||
],
|
||||
"roles": {
|
||||
"__ID:5__": {
|
||||
"_id": "__ID:5__",
|
||||
"name": "Moderator",
|
||||
"permissions": {
|
||||
"a": 545270208,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 1
|
||||
},
|
||||
"__ID:6__": {
|
||||
"_id": "__ID:6__",
|
||||
"name": "Owner",
|
||||
"permissions": {
|
||||
"a": 0,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 0
|
||||
}
|
||||
},
|
||||
"default_permissions": 4000322560
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:0__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": [
|
||||
"__ID:6__"
|
||||
],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:1__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": [
|
||||
"__ID:5__"
|
||||
],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:2__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"joined_at": 1698318340195
|
||||
}
|
||||
]
|
||||
319
crates/core/database/src/amqp/amqp.rs
Normal file
319
crates/core/database/src/amqp/amqp.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::events::rabbit::*;
|
||||
use crate::User;
|
||||
use amqprs::channel::{BasicPublishArguments, ExchangeDeclareArguments};
|
||||
use amqprs::connection::OpenConnectionArguments;
|
||||
use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError};
|
||||
use amqprs::{BasicProperties, FieldTable};
|
||||
use revolt_models::v0::PushNotification;
|
||||
use revolt_presence::filter_online;
|
||||
|
||||
use serde_json::to_string;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AMQP {
|
||||
#[allow(unused)]
|
||||
connection: Connection,
|
||||
channel: Channel,
|
||||
}
|
||||
|
||||
impl AMQP {
|
||||
pub fn new(connection: Connection, channel: Channel) -> AMQP {
|
||||
AMQP {
|
||||
connection,
|
||||
channel,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_auto() -> AMQP {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let connection = Connection::open(&OpenConnectionArguments::new(
|
||||
&config.rabbit.host,
|
||||
config.rabbit.port,
|
||||
&config.rabbit.username,
|
||||
&config.rabbit.password,
|
||||
))
|
||||
.await
|
||||
.expect("Failed to connect to RabbitMQ");
|
||||
|
||||
let channel = connection
|
||||
.open_channel(None)
|
||||
.await
|
||||
.expect("Failed to open RabbitMQ channel");
|
||||
|
||||
channel
|
||||
.exchange_declare(
|
||||
ExchangeDeclareArguments::new(&config.pushd.exchange, "direct")
|
||||
.durable(true)
|
||||
.finish(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to declare exchange");
|
||||
|
||||
AMQP::new(connection, channel)
|
||||
}
|
||||
|
||||
pub async fn friend_request_accepted(
|
||||
&self,
|
||||
accepted_request_user: &User,
|
||||
sent_request_user: &User,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
let payload = FRAcceptedPayload {
|
||||
accepted_user: accepted_request_user.to_owned(),
|
||||
user: sent_request_user.id.clone(),
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending friend request accept payload on channel {}: {}",
|
||||
config.pushd.get_fr_accepted_routing_key(),
|
||||
payload
|
||||
);
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_fr_accepted_routing_key(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn friend_request_received(
|
||||
&self,
|
||||
received_request_user: &User,
|
||||
sent_request_user: &User,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
let payload = FRReceivedPayload {
|
||||
from_user: sent_request_user.to_owned(),
|
||||
user: received_request_user.id.clone(),
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending friend request received payload on channel {}: {}",
|
||||
config.pushd.get_fr_received_routing_key(),
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_fr_received_routing_key(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn generic_message(
|
||||
&self,
|
||||
user: &User,
|
||||
title: String,
|
||||
body: String,
|
||||
icon: Option<String>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
let payload = GenericPayload {
|
||||
title,
|
||||
body,
|
||||
icon,
|
||||
user: user.to_owned(),
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending generic payload on channel {}: {}",
|
||||
config.pushd.get_generic_routing_key(),
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_generic_routing_key(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn message_sent(
|
||||
&self,
|
||||
recipients: Vec<String>,
|
||||
payload: PushNotification,
|
||||
) -> Result<(), AMQPError> {
|
||||
if recipients.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let online_ids = filter_online(&recipients).await;
|
||||
let recipients = (&recipients.into_iter().collect::<HashSet<String>>() - &online_ids)
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let payload = MessageSentPayload {
|
||||
notification: payload,
|
||||
users: recipients,
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending message payload on channel {}: {}",
|
||||
config.pushd.get_message_routing_key(),
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_message_routing_key(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn mass_mention_message_sent(
|
||||
&self,
|
||||
server_id: String,
|
||||
payload: Vec<PushNotification>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = MassMessageSentPayload {
|
||||
notifications: payload,
|
||||
server_id,
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
let routing_key = config.pushd.get_mass_mention_routing_key();
|
||||
|
||||
debug!(
|
||||
"Sending mass mention payload on channel {}: {}",
|
||||
routing_key, payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(&config.pushd.exchange, routing_key.as_str()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn ack_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
channel_id: String,
|
||||
message_id: String,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = AckPayload {
|
||||
user_id: user_id.clone(),
|
||||
channel_id: channel_id.clone(),
|
||||
message_id,
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
info!(
|
||||
"Sending ack payload on channel {}: {}",
|
||||
config.pushd.ack_queue, payload
|
||||
);
|
||||
|
||||
let mut headers = FieldTable::new();
|
||||
headers.insert(
|
||||
"x-deduplication-header".try_into().unwrap(),
|
||||
format!("{}-{}", &user_id, &channel_id).into(),
|
||||
);
|
||||
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
//.with_headers(headers)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(&config.pushd.exchange, &config.pushd.ack_queue),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// # DM Call Update
|
||||
/// Used to send an update about a DM call, eg. start or end of a call.
|
||||
/// Recipients can be used to narrow the scope of recipients, otherwise all recipients will be notified.
|
||||
/// `ended` refers to the ringing period, not necessarily the call itself.
|
||||
pub async fn dm_call_updated(
|
||||
&self,
|
||||
initiator_id: &str,
|
||||
channel_id: &str,
|
||||
started_at: Option<&str>,
|
||||
ended: bool,
|
||||
recipients: Option<Vec<String>>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = InternalDmCallPayload {
|
||||
payload: DmCallPayload {
|
||||
initiator_id: initiator_id.to_string(),
|
||||
channel_id: channel_id.to_string(),
|
||||
started_at: started_at.map(|f| f.to_string()),
|
||||
ended,
|
||||
},
|
||||
recipients,
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending dm call update payload on channel {}: {}",
|
||||
config.pushd.get_dm_call_routing_key(),
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_dm_call_routing_key(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
2
crates/core/database/src/amqp/mod.rs
Normal file
2
crates/core/database/src/amqp/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod amqp;
|
||||
@@ -1,6 +1,21 @@
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use authifier::config::Captcha;
|
||||
use authifier::config::EmailVerificationConfig;
|
||||
use authifier::config::PasswordScanning;
|
||||
use authifier::config::ResolveIp;
|
||||
use authifier::config::SMTPSettings;
|
||||
use authifier::config::Shield;
|
||||
use authifier::config::Template;
|
||||
use authifier::config::Templates;
|
||||
use authifier::config::EmailExpiryConfig;
|
||||
use authifier::Authifier;
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use self::mongodb::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
@@ -13,17 +28,20 @@ pub enum DatabaseInfo {
|
||||
/// Use the mock database
|
||||
Reference,
|
||||
/// Connect to MongoDB
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDb { uri: String, database_name: String },
|
||||
/// Use existing MongoDB connection
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDbFromClient(::mongodb::Client, String),
|
||||
}
|
||||
|
||||
/// Database
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Database {
|
||||
/// Mock database
|
||||
Reference(ReferenceDb),
|
||||
/// MongoDB database
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDb(MongoDb),
|
||||
}
|
||||
|
||||
@@ -31,37 +49,206 @@ impl DatabaseInfo {
|
||||
/// Create a database client from the given database information
|
||||
#[async_recursion]
|
||||
pub async fn connect(self) -> Result<Database, String> {
|
||||
Ok(match self {
|
||||
let config = config().await;
|
||||
|
||||
match self {
|
||||
DatabaseInfo::Auto => {
|
||||
if let Ok(uri) = std::env::var("MONGODB") {
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
DatabaseInfo::Test(format!(
|
||||
"revolt_test_{}",
|
||||
rand::thread_rng().gen_range(1_000_000..10_000_000)
|
||||
))
|
||||
.connect()
|
||||
.await
|
||||
} else if !config.database.mongodb.is_empty() {
|
||||
#[cfg(feature = "mongodb")]
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri,
|
||||
uri: config.database.mongodb,
|
||||
database_name: "revolt".to_string(),
|
||||
}
|
||||
.connect()
|
||||
.await;
|
||||
}
|
||||
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
#[cfg(not(feature = "mongodb"))]
|
||||
return Err("MongoDB not enabled.".to_string());
|
||||
} else {
|
||||
DatabaseInfo::Reference.connect().await
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Test(database_name) => {
|
||||
if let Ok(uri) = std::env::var("MONGODB") {
|
||||
return DatabaseInfo::MongoDb { uri, database_name }.connect().await;
|
||||
}
|
||||
match std::env::var("TEST_DB")
|
||||
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
|
||||
.as_str()
|
||||
{
|
||||
"REFERENCE" => DatabaseInfo::Reference.connect().await,
|
||||
"MONGODB" => {
|
||||
#[cfg(feature = "mongodb")]
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name,
|
||||
}
|
||||
.connect()
|
||||
.await;
|
||||
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
#[cfg(not(feature = "mongodb"))]
|
||||
return Err("MongoDB not enabled.".to_string());
|
||||
}
|
||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Reference => Database::Reference(Default::default()),
|
||||
DatabaseInfo::Reference => Ok(Database::Reference(Default::default())),
|
||||
#[cfg(feature = "mongodb")]
|
||||
DatabaseInfo::MongoDb { uri, database_name } => {
|
||||
let client = ::mongodb::Client::with_uri_str(uri)
|
||||
.await
|
||||
.map_err(|_| "Failed to init db connection.".to_string())?;
|
||||
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
Ok(Database::MongoDb(MongoDb(client, database_name)))
|
||||
}
|
||||
#[cfg(feature = "mongodb")]
|
||||
DatabaseInfo::MongoDbFromClient(client, database_name) => {
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
Ok(Database::MongoDb(MongoDb(client, database_name)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Create an Authifier reference
|
||||
pub async fn to_authifier(self) -> Authifier {
|
||||
let config = config().await;
|
||||
|
||||
let mut auth_config = authifier::Config {
|
||||
password_scanning: if config.api.security.easypwned.is_empty() {
|
||||
Default::default()
|
||||
} else {
|
||||
PasswordScanning::EasyPwned {
|
||||
endpoint: config.api.security.easypwned,
|
||||
}
|
||||
},
|
||||
email_verification: if !config.api.smtp.host.is_empty() {
|
||||
EmailVerificationConfig::Enabled {
|
||||
smtp: SMTPSettings {
|
||||
from: config.api.smtp.from_address,
|
||||
host: config.api.smtp.host,
|
||||
username: config.api.smtp.username,
|
||||
password: config.api.smtp.password,
|
||||
reply_to: Some(
|
||||
config
|
||||
.api
|
||||
.smtp
|
||||
.reply_to
|
||||
.unwrap_or("support@stoat.chat".into()),
|
||||
),
|
||||
port: config.api.smtp.port,
|
||||
use_tls: config.api.smtp.use_tls,
|
||||
use_starttls: config.api.smtp.use_starttls,
|
||||
},
|
||||
expiry: EmailExpiryConfig {
|
||||
expire_verification: 3600 * 24 * 7,
|
||||
expire_password_reset: 3600 * 24,
|
||||
expire_account_deletion: 3600 * 24,
|
||||
},
|
||||
templates: if config.production {
|
||||
Templates {
|
||||
verify: Template {
|
||||
title: "Verify your Stoat account.".into(),
|
||||
text: include_str!("../../templates/verify.txt").into(),
|
||||
url: format!("{}/login/verify/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/verify.html").into()),
|
||||
},
|
||||
reset: Template {
|
||||
title: "Reset your Stoat password.".into(),
|
||||
text: include_str!("../../templates/reset.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/reset.html").into()),
|
||||
},
|
||||
reset_existing: Template {
|
||||
title: "You already have a Stoat account, reset your password."
|
||||
.into(),
|
||||
text: include_str!("../../templates/reset-existing.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: Some(
|
||||
include_str!("../../templates/reset-existing.html").into(),
|
||||
),
|
||||
},
|
||||
deletion: Template {
|
||||
title: "Confirm account deletion.".into(),
|
||||
text: include_str!("../../templates/deletion.txt").into(),
|
||||
url: format!("{}/delete/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/deletion.html").into()),
|
||||
},
|
||||
welcome: None,
|
||||
}
|
||||
} else {
|
||||
Templates {
|
||||
verify: Template {
|
||||
title: "Verify your account.".into(),
|
||||
text: include_str!("../../templates/verify.whitelabel.txt").into(),
|
||||
url: format!("{}/login/verify/", config.hosts.app),
|
||||
html: None,
|
||||
},
|
||||
reset: Template {
|
||||
title: "Reset your password.".into(),
|
||||
text: include_str!("../../templates/reset.whitelabel.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: None,
|
||||
},
|
||||
reset_existing: Template {
|
||||
title: "Reset your password.".into(),
|
||||
text: include_str!("../../templates/reset.whitelabel.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: None,
|
||||
},
|
||||
deletion: Template {
|
||||
title: "Confirm account deletion.".into(),
|
||||
text: include_str!("../../templates/deletion.whitelabel.txt")
|
||||
.into(),
|
||||
url: format!("{}/delete/", config.hosts.app),
|
||||
html: None,
|
||||
},
|
||||
welcome: None,
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
EmailVerificationConfig::Disabled
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
auth_config.invite_only = config.api.registration.invite_only;
|
||||
|
||||
if !config.api.security.captcha.hcaptcha_key.is_empty() {
|
||||
auth_config.captcha = Captcha::HCaptcha {
|
||||
secret: config.api.security.captcha.hcaptcha_key,
|
||||
};
|
||||
}
|
||||
|
||||
if !config.api.security.authifier_shield_key.is_empty() {
|
||||
auth_config.shield = Shield::Enabled {
|
||||
api_key: config.api.security.authifier_shield_key,
|
||||
strict: false,
|
||||
};
|
||||
}
|
||||
|
||||
if config.api.security.trust_cloudflare {
|
||||
auth_config.resolve_ip = ResolveIp::Cloudflare;
|
||||
}
|
||||
|
||||
Authifier {
|
||||
database: match self {
|
||||
Database::Reference(_) => Default::default(),
|
||||
#[cfg(feature = "mongodb")]
|
||||
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: auth_config,
|
||||
#[cfg(feature = "tasks")]
|
||||
event_channel: Some(crate::tasks::authifier_relay::sender()),
|
||||
#[cfg(not(feature = "tasks"))]
|
||||
event_channel: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
database_derived!(
|
||||
#[cfg(feature = "mongodb")]
|
||||
/// MongoDB implementation
|
||||
#[derive(Debug)]
|
||||
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
||||
);
|
||||
|
||||
@@ -31,17 +31,17 @@ impl MongoDb {
|
||||
}
|
||||
|
||||
/// Get a collection by its name
|
||||
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
|
||||
pub fn col<T: Send + Sync>(&self, collection: &str) -> mongodb::Collection<T> {
|
||||
self.db().collection(collection)
|
||||
}
|
||||
|
||||
/// Insert one document into a collection
|
||||
pub async fn insert_one<T: Serialize>(
|
||||
pub async fn insert_one<T: Serialize + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
document: T,
|
||||
) -> Result<InsertOneResult> {
|
||||
self.col::<T>(collection).insert_one(document, None).await
|
||||
self.col::<T>(collection).insert_one(document).await
|
||||
}
|
||||
|
||||
/// Count documents by projection
|
||||
@@ -51,7 +51,7 @@ impl MongoDb {
|
||||
projection: Document,
|
||||
) -> Result<u64> {
|
||||
self.col::<Document>(collection)
|
||||
.count_documents(projection, None)
|
||||
.count_documents(projection)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ impl MongoDb {
|
||||
{
|
||||
Ok(self
|
||||
.col::<T>(collection)
|
||||
.find(projection, options)
|
||||
.find(projection)
|
||||
.with_options(options)
|
||||
.await?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
@@ -101,7 +102,8 @@ impl MongoDb {
|
||||
O: Into<Option<FindOneOptions>>,
|
||||
{
|
||||
self.col::<T>(collection)
|
||||
.find_one(projection, options)
|
||||
.find_one(projection)
|
||||
.with_options(options)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -165,7 +167,7 @@ impl MongoDb {
|
||||
};
|
||||
|
||||
self.col::<Document>(collection)
|
||||
.update_one(projection, query, None)
|
||||
.update_one(projection, query)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -200,7 +202,7 @@ impl MongoDb {
|
||||
projection: Document,
|
||||
) -> Result<DeleteResult> {
|
||||
self.col::<Document>(collection)
|
||||
.delete_one(projection, None)
|
||||
.delete_one(projection)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -238,6 +240,6 @@ pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_j
|
||||
let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap();
|
||||
v.into_iter()
|
||||
.filter(|(_k, v)| !v.is_null())
|
||||
.map(|(k, v)| (prefix.to_owned() + &k, v))
|
||||
.map(|(k, v)| (format!("{}{}", prefix.to_owned(), k), v))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -3,29 +3,32 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
AccountStrike, Bot, Channel, File, Member, MemberCompositeKey, Server, User, UserSettings,
|
||||
Webhook,
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
|
||||
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
|
||||
User, UserSettings, Webhook,
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ReferenceDb {
|
||||
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
|
||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
|
||||
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
|
||||
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
|
||||
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
|
||||
pub file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
|
||||
pub files: Arc<Mutex<HashMap<String, File>>>,
|
||||
pub messages: Arc<Mutex<HashMap<String, Message>>>,
|
||||
pub policy_changes: Arc<Mutex<HashMap<String, PolicyChange>>>,
|
||||
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
|
||||
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
||||
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
|
||||
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
|
||||
pub servers: Arc<Mutex<HashMap<String, Server>>>,
|
||||
pub files: Arc<Mutex<HashMap<String, File>>>,
|
||||
pub server_bans: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub emoji: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub messages: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
use authifier::AuthifierEvent;
|
||||
use revolt_result::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
Channel, FieldsChannel, FieldsWebhook, PartialChannel, PartialWebhook, Webhook,
|
||||
AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook
|
||||
};
|
||||
use revolt_result::Error;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
/// WebSocket Client Errors
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "error")]
|
||||
pub enum WebSocketError {
|
||||
LabelMe,
|
||||
InternalError { at: String },
|
||||
InvalidSession,
|
||||
OnboardingNotFinished,
|
||||
AlreadyAuthenticated,
|
||||
MalformedData { msg: String },
|
||||
}
|
||||
|
||||
/// Ping Packet
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(untagged)]
|
||||
@@ -28,12 +16,34 @@ pub enum Ping {
|
||||
Number(usize),
|
||||
}
|
||||
|
||||
/// Untagged Error
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ErrorEvent {
|
||||
Error(WebSocketError),
|
||||
APIError(Error),
|
||||
/// Fields provided in Ready payload
|
||||
#[derive(PartialEq, Debug, Clone, Deserialize)]
|
||||
pub struct ReadyPayloadFields {
|
||||
pub users: bool,
|
||||
pub servers: bool,
|
||||
pub channels: bool,
|
||||
pub members: bool,
|
||||
pub emojis: bool,
|
||||
pub voice_states: bool,
|
||||
pub user_settings: Vec<String>,
|
||||
pub channel_unreads: bool,
|
||||
pub policy_changes: bool,
|
||||
}
|
||||
|
||||
impl Default for ReadyPayloadFields {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
users: true,
|
||||
servers: true,
|
||||
channels: true,
|
||||
members: true,
|
||||
emojis: true,
|
||||
voice_states: true,
|
||||
user_settings: Vec::new(),
|
||||
channel_unreads: false,
|
||||
policy_changes: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol Events
|
||||
@@ -42,21 +52,39 @@ pub enum ErrorEvent {
|
||||
pub enum EventV1 {
|
||||
/// Multiple events
|
||||
Bulk { v: Vec<EventV1> },
|
||||
/// Error event
|
||||
Error { data: Error },
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
/* /// Basic data to cache
|
||||
/// Logged out
|
||||
Logout,
|
||||
/// Basic data to cache
|
||||
Ready {
|
||||
users: Vec<User>,
|
||||
servers: Vec<Server>,
|
||||
channels: Vec<Channel>,
|
||||
members: Vec<Member>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
users: Option<Vec<User>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
servers: Option<Vec<Server>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channels: Option<Vec<Channel>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
members: Option<Vec<Member>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voice_states: Option<Vec<ChannelVoiceState>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_settings: Option<UserSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
policy_changes: Option<Vec<PolicyChange>>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping },
|
||||
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
@@ -65,6 +93,8 @@ pub enum EventV1 {
|
||||
id: String,
|
||||
channel: String,
|
||||
data: PartialMessage,
|
||||
#[serde(default)]
|
||||
clear: Vec<FieldsMessage>,
|
||||
},
|
||||
|
||||
/// Append information to existing message
|
||||
@@ -108,12 +138,15 @@ pub enum EventV1 {
|
||||
id: String,
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
emojis: Vec<Emoji>,
|
||||
voice_states: Vec<ChannelVoiceState>
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
ServerUpdate {
|
||||
id: String,
|
||||
data: PartialServer,
|
||||
#[serde(default)]
|
||||
clear: Vec<FieldsServer>,
|
||||
},
|
||||
|
||||
@@ -124,42 +157,52 @@ pub enum EventV1 {
|
||||
ServerMemberUpdate {
|
||||
id: MemberCompositeKey,
|
||||
data: PartialMember,
|
||||
#[serde(default)]
|
||||
clear: Vec<FieldsMember>,
|
||||
},
|
||||
|
||||
/// User joins server
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
ServerMemberJoin {
|
||||
id: String,
|
||||
// Deprecated: use member.id.user
|
||||
#[deprecated = "Use member.id.user instead"]
|
||||
user: String,
|
||||
member: Member,
|
||||
},
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave { id: String, user: String },
|
||||
ServerMemberLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
reason: RemovalIntention,
|
||||
},
|
||||
|
||||
/// Server role created or updated
|
||||
ServerRoleUpdate {
|
||||
id: String,
|
||||
role_id: String,
|
||||
data: PartialRole,
|
||||
#[serde(default)]
|
||||
clear: Vec<FieldsRole>,
|
||||
},
|
||||
|
||||
/// Server role deleted
|
||||
ServerRoleDelete { id: String, role_id: String },
|
||||
|
||||
/// Server roles ranks updated
|
||||
ServerRoleRanksUpdate { id: String, ranks: Vec<String> },
|
||||
|
||||
/// Update existing user
|
||||
UserUpdate {
|
||||
id: String,
|
||||
data: PartialUser,
|
||||
#[serde(default)]
|
||||
clear: Vec<FieldsUser>,
|
||||
event_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Relationship with another user changed
|
||||
UserRelationship {
|
||||
id: String,
|
||||
user: User,
|
||||
// ! this field can be deprecated
|
||||
status: RelationshipStatus,
|
||||
},
|
||||
|
||||
UserRelationship { id: String, user: User },
|
||||
/// Settings updated remotely
|
||||
UserSettingsUpdate { id: String, update: UserSettings },
|
||||
|
||||
@@ -173,7 +216,6 @@ pub enum EventV1 {
|
||||
///
|
||||
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
||||
UserPlatformWipe { user_id: String, flags: i32 },
|
||||
|
||||
/// New emoji
|
||||
EmojiCreate(Emoji),
|
||||
|
||||
@@ -181,7 +223,7 @@ pub enum EventV1 {
|
||||
EmojiDelete { id: String },
|
||||
|
||||
/// New report
|
||||
ReportCreate(Report), */
|
||||
ReportCreate(Report),
|
||||
/// New channel
|
||||
ChannelCreate(Channel),
|
||||
|
||||
@@ -189,6 +231,7 @@ pub enum EventV1 {
|
||||
ChannelUpdate {
|
||||
id: String,
|
||||
data: PartialChannel,
|
||||
#[serde(default)]
|
||||
clear: Vec<FieldsChannel>,
|
||||
},
|
||||
|
||||
@@ -229,6 +272,33 @@ pub enum EventV1 {
|
||||
|
||||
/// Auth events
|
||||
Auth(AuthifierEvent),
|
||||
|
||||
/// Voice events
|
||||
VoiceChannelJoin {
|
||||
id: String,
|
||||
state: UserVoiceState,
|
||||
},
|
||||
VoiceChannelLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
},
|
||||
VoiceChannelMove {
|
||||
user: String,
|
||||
from: String,
|
||||
to: String,
|
||||
state: UserVoiceState
|
||||
},
|
||||
UserVoiceStateUpdate {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
data: PartialUserVoiceState,
|
||||
},
|
||||
UserMoveVoiceChannel {
|
||||
node: String,
|
||||
from: String,
|
||||
to: String,
|
||||
token: String,
|
||||
}
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
@@ -248,11 +318,10 @@ impl EventV1 {
|
||||
pub async fn p_user(self, id: String, db: &Database) {
|
||||
self.clone().p(id.clone()).await;
|
||||
|
||||
// ! FIXME: this should be captured by member list in the future
|
||||
// ! and not immediately fanned out to users
|
||||
// TODO: this should be captured by member list in the future and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().p(member.id.server).await;
|
||||
self.clone().server(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,6 +331,11 @@ impl EventV1 {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish server member event
|
||||
pub async fn server(self, id: String) {
|
||||
self.p(format!("{id}u")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
pub mod client;
|
||||
pub mod rabbit;
|
||||
pub mod server;
|
||||
|
||||
80
crates/core/database/src/events/rabbit.rs
Normal file
80
crates/core/database/src/events/rabbit.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use revolt_models::v0::PushNotification;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::User;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MessageSentPayload {
|
||||
pub notification: PushNotification,
|
||||
pub users: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MassMessageSentPayload {
|
||||
pub notifications: Vec<PushNotification>,
|
||||
pub server_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct FRAcceptedPayload {
|
||||
pub accepted_user: User,
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct FRReceivedPayload {
|
||||
pub from_user: User,
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct GenericPayload {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub icon: Option<String>,
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DmCallPayload {
|
||||
pub initiator_id: String,
|
||||
pub channel_id: String,
|
||||
pub started_at: Option<String>,
|
||||
pub ended: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct InternalDmCallPayload {
|
||||
pub payload: DmCallPayload,
|
||||
pub recipients: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum PayloadKind {
|
||||
MessageNotification(PushNotification),
|
||||
FRAccepted(FRAcceptedPayload),
|
||||
FRReceived(FRReceivedPayload),
|
||||
BadgeUpdate(usize),
|
||||
Generic(GenericPayload),
|
||||
DmCallStartEnd(DmCallPayload),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PayloadToService {
|
||||
pub notification: PayloadKind,
|
||||
pub user_id: String,
|
||||
pub session_id: String,
|
||||
pub token: String,
|
||||
pub extras: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AckPayload {
|
||||
pub user_id: String,
|
||||
pub channel_id: String,
|
||||
pub message_id: String,
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
use serde::Deserialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use super::client::Ping;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClientMessage {
|
||||
Authenticate { token: String },
|
||||
BeginTyping { channel: String },
|
||||
EndTyping { channel: String },
|
||||
Subscribe { server_id: String },
|
||||
Ping { data: Ping, responded: Option<()> },
|
||||
}
|
||||
@@ -16,6 +16,8 @@ extern crate revolt_optional_struct;
|
||||
#[macro_use]
|
||||
extern crate revolt_result;
|
||||
|
||||
pub use iso8601_timestamp;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use mongodb;
|
||||
|
||||
@@ -23,6 +25,29 @@ pub use mongodb;
|
||||
#[macro_use]
|
||||
extern crate bson;
|
||||
|
||||
#[cfg(not(feature = "async-std-runtime"))]
|
||||
compile_error!("async-std-runtime feature must be enabled.");
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! query {
|
||||
( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => {
|
||||
Ok($self.$type($collection, $($rest),+).await.unwrap())
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(not(debug_assertions))]
|
||||
macro_rules! query {
|
||||
( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => {
|
||||
$self.$type($collection, $($rest),+).await
|
||||
.map_err(|err| {
|
||||
revolt_config::capture_internal_error!(err);
|
||||
create_database_error!(stringify!($type), $collection)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! database_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
@@ -43,7 +68,7 @@ macro_rules! auto_derived {
|
||||
|
||||
macro_rules! auto_derived_partial {
|
||||
( $item:item, $name:expr ) => {
|
||||
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[optional_name = $name]
|
||||
#[opt_skip_serializing_none]
|
||||
@@ -81,8 +106,22 @@ pub mod util;
|
||||
pub use models::*;
|
||||
|
||||
pub mod events;
|
||||
#[cfg(feature = "tasks")]
|
||||
pub mod tasks;
|
||||
|
||||
mod amqp;
|
||||
pub use amqp::amqp::AMQP;
|
||||
|
||||
#[cfg(feature = "voice")]
|
||||
pub mod voice;
|
||||
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
}
|
||||
|
||||
/// Utility function to check if an option doesnt contain true
|
||||
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||
t != &Some(true)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ mod tests {
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Migrate the existing database
|
||||
db.migrate_database().await.unwrap()
|
||||
db.migrate_database().await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ impl AbstractMigrations for MongoDb {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self) {
|
||||
self.db().drop(None).await.ok();
|
||||
self.db().drop().await.ok();
|
||||
}
|
||||
|
||||
/// Migrate the database
|
||||
@@ -18,7 +18,7 @@ impl AbstractMigrations for MongoDb {
|
||||
info!("Migrating the database.");
|
||||
|
||||
let list = self
|
||||
.list_database_names(None, None)
|
||||
.list_database_names()
|
||||
.await
|
||||
.expect("Failed to fetch database names.");
|
||||
|
||||
|
||||
@@ -8,206 +8,260 @@ pub async fn create_database(db: &MongoDb) {
|
||||
info!("Creating database.");
|
||||
let db = db.db();
|
||||
|
||||
db.create_collection("accounts", None)
|
||||
db.create_collection("accounts")
|
||||
.await
|
||||
.expect("Failed to create accounts collection.");
|
||||
|
||||
db.create_collection("users", None)
|
||||
db.create_collection("users")
|
||||
.await
|
||||
.expect("Failed to create users collection.");
|
||||
|
||||
db.create_collection("channels", None)
|
||||
db.create_collection("channels")
|
||||
.await
|
||||
.expect("Failed to create channels collection.");
|
||||
|
||||
db.create_collection("messages", None)
|
||||
db.create_collection("messages")
|
||||
.await
|
||||
.expect("Failed to create messages collection.");
|
||||
|
||||
db.create_collection("servers", None)
|
||||
db.create_collection("servers")
|
||||
.await
|
||||
.expect("Failed to create servers collection.");
|
||||
|
||||
db.create_collection("server_members", None)
|
||||
db.create_collection("server_members")
|
||||
.await
|
||||
.expect("Failed to create server_members collection.");
|
||||
|
||||
db.create_collection("server_bans", None)
|
||||
db.create_collection("server_bans")
|
||||
.await
|
||||
.expect("Failed to create server_bans collection.");
|
||||
|
||||
db.create_collection("channel_invites", None)
|
||||
db.create_collection("channel_invites")
|
||||
.await
|
||||
.expect("Failed to create channel_invites collection.");
|
||||
|
||||
db.create_collection("channel_unreads", None)
|
||||
db.create_collection("channel_unreads")
|
||||
.await
|
||||
.expect("Failed to create channel_unreads collection.");
|
||||
|
||||
db.create_collection("channel_webhooks", None)
|
||||
db.create_collection("channel_webhooks")
|
||||
.await
|
||||
.expect("Failed to create channel_webhooks collection.");
|
||||
|
||||
db.create_collection("migrations", None)
|
||||
db.create_collection("migrations")
|
||||
.await
|
||||
.expect("Failed to create migrations collection.");
|
||||
|
||||
db.create_collection("attachments", None)
|
||||
db.create_collection("attachments")
|
||||
.await
|
||||
.expect("Failed to create attachments collection.");
|
||||
|
||||
db.create_collection("user_settings", None)
|
||||
db.create_collection("attachment_hashes")
|
||||
.await
|
||||
.expect("Failed to create attachment_hashes collection.");
|
||||
|
||||
db.create_collection("user_settings")
|
||||
.await
|
||||
.expect("Failed to create user_settings collection.");
|
||||
|
||||
db.create_collection("safety_reports", None)
|
||||
db.create_collection("policy_changes")
|
||||
.await
|
||||
.expect("Failed to create policy_changes collection.");
|
||||
|
||||
db.create_collection("safety_reports")
|
||||
.await
|
||||
.expect("Failed to create safety_reports collection.");
|
||||
|
||||
db.create_collection("safety_snapshots", None)
|
||||
db.create_collection("safety_snapshots")
|
||||
.await
|
||||
.expect("Failed to create safety_snapshots collection.");
|
||||
|
||||
db.create_collection("safety_strikes", None)
|
||||
db.create_collection("safety_strikes")
|
||||
.await
|
||||
.expect("Failed to create safety_strikes collection.");
|
||||
|
||||
db.create_collection("bots", None)
|
||||
db.create_collection("bots")
|
||||
.await
|
||||
.expect("Failed to create bots collection.");
|
||||
|
||||
db.create_collection(
|
||||
"pubsub",
|
||||
CreateCollectionOptions::builder()
|
||||
.capped(true)
|
||||
.size(1_000_000)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create pubsub collection.");
|
||||
db.create_collection("ratelimit_events")
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events collection.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "users",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32
|
||||
},
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
db.create_collection("pubsub")
|
||||
.with_options(
|
||||
CreateCollectionOptions::builder()
|
||||
.capped(true)
|
||||
.size(1_000_000)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create pubsub collection.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "users",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create username index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "messages",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"content": "text"
|
||||
},
|
||||
"name": "content"
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "messages",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"content": "text"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"_id": 1_i32
|
||||
},
|
||||
"name": "channel_id_compound"
|
||||
"name": "content"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"_id": 1_i32
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
"name": "channel_id_compound"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
"name": "channel_pinned_compound"
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create message index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "channel_unreads",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.channel": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "compound_id"
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "channel_unreads",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.channel": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
"name": "compound_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create channel_unreads index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "server_members",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.server": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "compound_id"
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "server_members",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.server": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
"name": "compound_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create server_members index.");
|
||||
|
||||
db.collection("migrations")
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": 0_i32,
|
||||
"revision": LATEST_REVISION
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "attachments",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"hash": 1_i32
|
||||
},
|
||||
"name": "hash"
|
||||
},
|
||||
None,
|
||||
)
|
||||
{
|
||||
"key": {
|
||||
"used_for.id": 1_i32
|
||||
},
|
||||
"name": "used_for_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create attachments index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "attachment_hashes",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"processed_hash": 1_i32
|
||||
},
|
||||
"name": "processed_hash"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create attachment_hashes index.");
|
||||
|
||||
db.collection("migrations")
|
||||
.insert_one(doc! {
|
||||
"_id": 0_i32,
|
||||
"revision": LATEST_REVISION
|
||||
})
|
||||
.await
|
||||
.expect("Failed to save migration info.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "ratelimit_events",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id": 1_i32,
|
||||
"target_id": 1_i32,
|
||||
"event_type": 1_i32,
|
||||
},
|
||||
"name": "compound_key"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
|
||||
info!("Created database.");
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::Database;
|
||||
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Bot
|
||||
@@ -49,8 +50,74 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Bot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Default::default(),
|
||||
owner: Default::default(),
|
||||
token: Default::default(),
|
||||
public: Default::default(),
|
||||
analytics: Default::default(),
|
||||
discoverable: Default::default(),
|
||||
interactions_url: Default::default(),
|
||||
terms_of_service_url: Default::default(),
|
||||
privacy_policy_url: Default::default(),
|
||||
flags: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Bot {
|
||||
/// Create a new bot
|
||||
pub async fn create<D>(
|
||||
db: &Database,
|
||||
username: String,
|
||||
owner: &User,
|
||||
data: D,
|
||||
) -> Result<(Bot, User)>
|
||||
where
|
||||
D: Into<Option<PartialBot>>,
|
||||
{
|
||||
if owner.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
|
||||
return Err(create_error!(ReachedMaximumBots));
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
|
||||
let user = User::create(
|
||||
db,
|
||||
username,
|
||||
Some(id.to_string()),
|
||||
Some(PartialUser {
|
||||
bot: Some(BotInformation {
|
||||
owner: owner.id.to_string(),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut bot = Bot {
|
||||
id,
|
||||
owner: owner.id.to_string(),
|
||||
token: nanoid::nanoid!(64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(data) = data.into() {
|
||||
bot.apply_options(data);
|
||||
}
|
||||
|
||||
db.insert_bot(&bot).await?;
|
||||
Ok((bot, user))
|
||||
}
|
||||
|
||||
/// Remove a field from this object
|
||||
pub fn remove_field(&mut self, field: &FieldsBot) {
|
||||
match field {
|
||||
@@ -78,6 +145,10 @@ impl Bot {
|
||||
|
||||
db.update_bot(&self.id, &partial, remove).await?;
|
||||
|
||||
if partial.token.is_some() {
|
||||
EventV1::Logout.private(self.id.clone()).await;
|
||||
}
|
||||
|
||||
self.apply_options(partial);
|
||||
Ok(())
|
||||
}
|
||||
@@ -96,27 +167,24 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let bot_id = "bot";
|
||||
let user_id = "user";
|
||||
let token = "my_token";
|
||||
let owner = User::create(&db, "Owner".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = User {
|
||||
id: bot_id.to_string(),
|
||||
username: "Bot Name".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let (bot, _) = Bot::create(
|
||||
&db,
|
||||
"Bot Name".to_string(),
|
||||
&owner,
|
||||
PartialBot {
|
||||
token: Some("my token".to_string()),
|
||||
interactions_url: Some("some url".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.insert_user(&user).await.unwrap();
|
||||
|
||||
let bot = Bot {
|
||||
id: bot_id.to_string(),
|
||||
owner: user_id.to_string(),
|
||||
token: token.to_string(),
|
||||
interactions_url: "some url".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_bot(&bot).await.unwrap();
|
||||
assert!(!bot.interactions_url.is_empty());
|
||||
|
||||
let mut updated_bot = bot.clone();
|
||||
updated_bot
|
||||
@@ -131,9 +199,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched_bot1 = db.fetch_bot(bot_id).await.unwrap();
|
||||
let fetched_bot1 = db.fetch_bot(&bot.id).await.unwrap();
|
||||
let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap();
|
||||
let fetched_bots = db.fetch_bots_by_user(user_id).await.unwrap();
|
||||
let fetched_bots = db.fetch_bots_by_user(&owner.id).await.unwrap();
|
||||
|
||||
assert!(!bot.public);
|
||||
assert!(fetched_bot1.public);
|
||||
@@ -143,12 +211,12 @@ mod tests {
|
||||
assert_eq!(updated_bot, fetched_bot1);
|
||||
assert_eq!(fetched_bot1, fetched_bot2);
|
||||
assert_eq!(fetched_bot1, fetched_bots[0]);
|
||||
assert_eq!(1, db.get_number_of_bots_by_user(user_id).await.unwrap());
|
||||
assert_eq!(1, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
|
||||
|
||||
bot.delete(&db).await.unwrap();
|
||||
assert!(db.fetch_bot(bot_id).await.is_err());
|
||||
assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap());
|
||||
assert_eq!(db.fetch_user(bot_id).await.unwrap().flags, Some(2))
|
||||
assert!(db.fetch_bot(&bot.id).await.is_err());
|
||||
assert_eq!(0, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
|
||||
assert_eq!(db.fetch_user(&bot.id).await.unwrap().flags, Some(2))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
use nanoid::nanoid;
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use crate::{
|
||||
models::{Channel, Invite, User},
|
||||
Database, Error, Result,
|
||||
};
|
||||
use crate::{Channel, Database, User};
|
||||
|
||||
static ALPHABET: [char; 54] = [
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
||||
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
|
||||
'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
|
||||
'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
|
||||
auto_derived!(
|
||||
/// Invite
|
||||
#[serde(tag = "type")]
|
||||
pub enum Invite {
|
||||
/// Invite to a specific server channel
|
||||
Server {
|
||||
/// Invite code
|
||||
#[serde(rename = "_id")]
|
||||
code: String,
|
||||
/// Id of the server this invite points to
|
||||
server: String,
|
||||
/// Id of user who created this invite
|
||||
creator: String,
|
||||
/// Id of the server channel this invite points to
|
||||
channel: String,
|
||||
},
|
||||
/// Invite to a group channel
|
||||
Group {
|
||||
/// Invite code
|
||||
#[serde(rename = "_id")]
|
||||
code: String,
|
||||
/// Id of user who created this invite
|
||||
creator: String,
|
||||
/// Id of the group channel this invite points to
|
||||
channel: String,
|
||||
}, /* User {
|
||||
code: String,
|
||||
user: String
|
||||
} */
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Invite {
|
||||
/// Get the invite code for this invite
|
||||
pub fn code(&'_ self) -> &'_ str {
|
||||
@@ -27,15 +57,19 @@ impl Invite {
|
||||
}
|
||||
|
||||
/// Create a new invite from given information
|
||||
pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result<Invite> {
|
||||
let code = nanoid!(8, &ALPHABET);
|
||||
let invite = match &target {
|
||||
pub async fn create_channel_invite(
|
||||
db: &Database,
|
||||
creator: &User,
|
||||
channel: &Channel,
|
||||
) -> Result<Invite> {
|
||||
let code = nanoid::nanoid!(8, &ALPHABET);
|
||||
let invite = match &channel {
|
||||
Channel::Group { id, .. } => Ok(Invite::Group {
|
||||
code,
|
||||
creator: creator.id.clone(),
|
||||
channel: id.clone(),
|
||||
}),
|
||||
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
|
||||
Channel::TextChannel { id, server, .. } => {
|
||||
Ok(Invite::Server {
|
||||
code,
|
||||
creator: creator.id.clone(),
|
||||
@@ -43,7 +77,7 @@ impl Invite {
|
||||
channel: id.clone(),
|
||||
})
|
||||
}
|
||||
_ => Err(Error::InvalidOperation),
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}?;
|
||||
|
||||
db.insert_invite(&invite).await?;
|
||||
@@ -67,6 +101,6 @@ impl Invite {
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::NotFound)
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
use crate::models::Invite;
|
||||
use crate::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelInvite: Sync + Send {
|
||||
/// Fetch an invite by its id
|
||||
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
|
||||
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
|
||||
|
||||
/// Delete an invite by its id
|
||||
async fn delete_invite(&self, code: &str) -> Result<()>;
|
||||
|
||||
/// Fetch all invites for a server
|
||||
async fn fetch_invites_for_server(&self, server: &str) -> Result<Vec<Invite>>;
|
||||
}
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelInvites: Sync + Send {
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
|
||||
|
||||
/// Fetch an invite by its id
|
||||
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
|
||||
|
||||
/// Fetch all invites for a server
|
||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>>;
|
||||
|
||||
/// Delete an invite by its id
|
||||
async fn delete_invite(&self, code: &str) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractChannelInvites;
|
||||
|
||||
static COL: &str = "channel_invites";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelInvites for MongoDb {
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
||||
query!(self, insert_one, COL, &invite).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch an invite by the code
|
||||
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
|
||||
query!(self, find_one_by_id, COL, code)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all invites for a server
|
||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
|
||||
Ok(self
|
||||
.col::<Invite>(COL)
|
||||
.find(doc! {
|
||||
"server": server_id,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Delete an invite by its code
|
||||
async fn delete_invite(&self, code: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, code).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractChannelInvites;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelInvites for ReferenceDb {
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
||||
let mut invites = self.channel_invites.lock().await;
|
||||
if invites.contains_key(invite.code()) {
|
||||
Err(create_database_error!("insert", "invite"))
|
||||
} else {
|
||||
invites.insert(invite.code().to_string(), invite.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch an invite by the code
|
||||
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
|
||||
let invites = self.channel_invites.lock().await;
|
||||
invites
|
||||
.get(code)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all invites for a server
|
||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
|
||||
let invites = self.channel_invites.lock().await;
|
||||
Ok(invites
|
||||
.values()
|
||||
.filter(|invite| match invite {
|
||||
Invite::Server { server, .. } => server == server_id,
|
||||
_ => false,
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Delete an invite by its code
|
||||
async fn delete_invite(&self, code: &str) -> Result<()> {
|
||||
let mut invites = self.channel_invites.lock().await;
|
||||
if invites.remove(code).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/channel_unreads/mod.rs
Normal file
5
crates/core/database/src/models/channel_unreads/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
24
crates/core/database/src/models/channel_unreads/model.rs
Normal file
24
crates/core/database/src/models/channel_unreads/model.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
auto_derived!(
|
||||
/// Channel Unread
|
||||
pub struct ChannelUnread {
|
||||
/// Composite key pointing to a user's view of a channel
|
||||
#[serde(rename = "_id")]
|
||||
pub id: ChannelCompositeKey,
|
||||
|
||||
/// Id of the last message read in this channel by a user
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_id: Option<String>,
|
||||
/// Array of message ids that mention the user
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentions: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Composite primary key consisting of channel and user id
|
||||
#[derive(Hash)]
|
||||
pub struct ChannelCompositeKey {
|
||||
/// Channel Id
|
||||
pub channel: String,
|
||||
/// User Id
|
||||
pub user: String,
|
||||
}
|
||||
);
|
||||
46
crates/core/database/src/models/channel_unreads/ops.rs
Normal file
46
crates/core/database/src/models/channel_unreads/ops.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ChannelUnread;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelUnreads: Sync + Send {
|
||||
/// Acknowledge a message, and returns updated channel unread.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<Option<ChannelUnread>>;
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
|
||||
|
||||
/// Add a mention.
|
||||
async fn add_mention_to_unread<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_ids: &[String],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Add a mention.
|
||||
async fn add_mention_to_many_unreads<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_ids: &[String],
|
||||
message_ids: &[String],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Fetch all unreads with mentions for a user.
|
||||
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>>;
|
||||
}
|
||||
177
crates/core/database/src/models/channel_unreads/ops/mongodb.rs
Normal file
177
crates/core/database/src/models/channel_unreads/ops/mongodb.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use bson::Document;
|
||||
use mongodb::options::FindOneAndUpdateOptions;
|
||||
use mongodb::options::ReturnDocument;
|
||||
use mongodb::options::UpdateOptions;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::ChannelUnread;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractChannelUnreads;
|
||||
|
||||
static COL: &str = "channel_unreads";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelUnreads for MongoDb {
|
||||
/// Acknowledge a message, and returns updated channel unread.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<Option<ChannelUnread>> {
|
||||
self.col::<ChannelUnread>(COL)
|
||||
.find_one_and_update(
|
||||
doc! {
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": user_id,
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"mentions": {
|
||||
"$lte": message_id
|
||||
}
|
||||
},
|
||||
"$set": {
|
||||
"last_id": message_id
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_options(
|
||||
FindOneAndUpdateOptions::builder()
|
||||
.upsert(true)
|
||||
.return_document(ReturnDocument::After)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
|
||||
let current_time = Ulid::new().to_string();
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(doc! {
|
||||
"_id.channel": {
|
||||
"$in": channel_ids
|
||||
},
|
||||
"_id.user": user_id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", COL))?;
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.insert_many(
|
||||
channel_ids
|
||||
.iter()
|
||||
.map(|channel_id| {
|
||||
doc! {
|
||||
"_id": {
|
||||
"channel": channel_id,
|
||||
"user": user_id
|
||||
},
|
||||
"last_id": ¤t_time
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Document>>(),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
}
|
||||
|
||||
/// Add a mention.
|
||||
async fn add_mention_to_unread<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_ids: &[String],
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": user_id,
|
||||
},
|
||||
doc! {
|
||||
"$push": {
|
||||
"mentions": {
|
||||
"$each": message_ids
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_options(UpdateOptions::builder().upsert(true).build())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Add a mention to multiple users.
|
||||
async fn add_mention_to_many_unreads<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_ids: &[String],
|
||||
message_ids: &[String],
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
doc! {
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": {
|
||||
"$in": user_ids
|
||||
},
|
||||
},
|
||||
doc! {
|
||||
"$push": {
|
||||
"mentions": {
|
||||
"$each": message_ids
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_options(UpdateOptions::builder().upsert(true).build())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
}
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.user": user_id
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
||||
query! {
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.user": user_id,
|
||||
"mentions": {"$ne": null}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.user": user_id,
|
||||
"_id.channel": channel_id
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
143
crates/core/database/src/models/channel_unreads/ops/reference.rs
Normal file
143
crates/core/database/src/models/channel_unreads/ops/reference.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{ChannelCompositeKey, ChannelUnread, ReferenceDb};
|
||||
|
||||
use super::AbstractChannelUnreads;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelUnreads for ReferenceDb {
|
||||
/// Acknowledge a message.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<Option<ChannelUnread>> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
};
|
||||
|
||||
if let Some(unread) = unreads.get_mut(&key) {
|
||||
unread.mentions = None;
|
||||
unread.last_id.replace(message_id.to_string());
|
||||
} else {
|
||||
unreads.insert(
|
||||
key.clone(),
|
||||
ChannelUnread {
|
||||
id: key.clone(),
|
||||
last_id: Some(message_id.to_string()),
|
||||
mentions: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(unreads.get(&key).cloned())
|
||||
}
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
|
||||
let current_time = Ulid::new().to_string();
|
||||
for channel_id in channel_ids {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
self.acknowledge_message(channel_id, user_id, ¤t_time)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a mention.
|
||||
async fn add_mention_to_unread<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_ids: &[String],
|
||||
) -> Result<()> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
};
|
||||
|
||||
if let Some(unread) = unreads.get_mut(&key) {
|
||||
unread.mentions.replace(message_ids.to_vec());
|
||||
} else {
|
||||
unreads.insert(
|
||||
key.clone(),
|
||||
ChannelUnread {
|
||||
id: key,
|
||||
last_id: None,
|
||||
mentions: Some(message_ids.to_vec()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a mention to multiple users.
|
||||
async fn add_mention_to_many_unreads<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_ids: &[String],
|
||||
message_ids: &[String],
|
||||
) -> Result<()> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
|
||||
for user_id in user_ids {
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
};
|
||||
|
||||
if let Some(unread) = unreads.get_mut(&key) {
|
||||
unread.mentions.replace(message_ids.to_vec());
|
||||
} else {
|
||||
unreads.insert(
|
||||
key.clone(),
|
||||
ChannelUnread {
|
||||
id: key,
|
||||
last_id: None,
|
||||
mentions: Some(message_ids.to_vec()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
||||
let unreads = self.channel_unreads.lock().await;
|
||||
Ok(unreads
|
||||
.values()
|
||||
.filter(|unread| unread.id.user == user_id && unread.mentions.is_some())
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
||||
let unreads = self.channel_unreads.lock().await;
|
||||
Ok(unreads
|
||||
.values()
|
||||
.filter(|unread| unread.id.user == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||
let unreads = self.channel_unreads.lock().await;
|
||||
|
||||
Ok(unreads
|
||||
.get(&ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
})
|
||||
.cloned())
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,15 @@ auto_derived_partial!(
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<File>,
|
||||
|
||||
/// User that created this webhook
|
||||
pub creator_id: String,
|
||||
|
||||
/// The channel this webhook belongs to
|
||||
pub channel_id: String,
|
||||
|
||||
/// The permissions of the webhook
|
||||
pub permissions: u64,
|
||||
|
||||
/// The private token for the webhook
|
||||
pub token: Option<String>,
|
||||
},
|
||||
@@ -33,6 +39,21 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Webhook {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Default::default(),
|
||||
name: Default::default(),
|
||||
avatar: None,
|
||||
creator_id: Default::default(),
|
||||
channel_id: Default::default(),
|
||||
permissions: Default::default(),
|
||||
token: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Webhook {
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
@@ -53,7 +74,7 @@ impl Webhook {
|
||||
if self.token.as_deref() == Some(token) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(InvalidCredentials))
|
||||
Err(create_error!(NotAuthenticated))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,11 +138,11 @@ mod tests {
|
||||
id: webhook_id.to_string(),
|
||||
name: "Webhook Name".to_string(),
|
||||
channel_id: channel_id.to_string(),
|
||||
avatar: Some(Default::default()),
|
||||
avatar: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_webhook(&webhook).await.unwrap();
|
||||
webhook.create(&db).await.unwrap();
|
||||
|
||||
let mut updated_webhook = webhook.clone();
|
||||
updated_webhook
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -24,12 +24,9 @@ impl AbstractWebhooks for MongoDb {
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
||||
Ok(self
|
||||
.col::<Webhook>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"channel_id": channel_id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"channel_id": channel_id,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
use std::collections::HashMap;
|
||||
#![allow(deprecated)]
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::{self, MessageAuthor};
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{events::client::EventV1, Database, File, IntoDocumentPath};
|
||||
use crate::{
|
||||
events::client::EventV1, Database, File, PartialServer, Server, SystemMessage, User, AMQP,
|
||||
};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
use crate::IntoDocumentPath;
|
||||
|
||||
auto_derived!(
|
||||
#[serde(tag = "channel_type")]
|
||||
pub enum Channel {
|
||||
/// Personal "Saved Notes" channel which allows users to save messages
|
||||
SavedMessages {
|
||||
@@ -96,38 +106,22 @@ auto_derived!(
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
|
||||
/// Voice Information for when this channel is also a voice channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voice: Option<VoiceInformation>,
|
||||
|
||||
/// The channel's slowmode delay in seconds
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
slowmode: Option<u64>,
|
||||
},
|
||||
/// Voice channel belonging to a server
|
||||
VoiceChannel {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
}
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Channel description
|
||||
description: Option<String>,
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
#[derive(Default)]
|
||||
pub struct VoiceInformation {
|
||||
/// Maximium amount of users allowed in the voice channel at once
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_users: Option<usize>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -154,6 +148,10 @@ auto_derived!(
|
||||
pub default_permissions: Option<OverrideField>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_message_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub voice: Option<VoiceInformation>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slowmode: Option<u64>,
|
||||
}
|
||||
|
||||
/// Optional fields on channel object
|
||||
@@ -161,55 +159,239 @@ auto_derived!(
|
||||
Description,
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
Voice,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Channel {
|
||||
/// Create a channel
|
||||
/* /// Create a channel
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_channel(self).await?;
|
||||
|
||||
let event = EventV1::ChannelCreate(self.clone().into());
|
||||
match self {
|
||||
Self::SavedMessages { user, .. } => event.private(user.clone()).await,
|
||||
Self::DirectMessage { recipients, .. } | Self::Group { recipients, .. } => {
|
||||
for recipient in recipients {
|
||||
event.clone().private(recipient.clone()).await;
|
||||
}
|
||||
}
|
||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => {
|
||||
event.p(server.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}*/
|
||||
|
||||
/// Create a new server channel
|
||||
pub async fn create_server_channel(
|
||||
db: &Database,
|
||||
server: &mut Server,
|
||||
data: v0::DataCreateServerChannel,
|
||||
update_server: bool,
|
||||
) -> Result<Channel> {
|
||||
let config = config().await;
|
||||
if server.channels.len() > config.features.limits.global.server_channels {
|
||||
return Err(create_error!(TooManyChannels {
|
||||
max: config.features.limits.global.server_channels,
|
||||
}));
|
||||
};
|
||||
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
let channel = match data.channel_type {
|
||||
v0::LegacyServerChannelType::Text => Channel::TextChannel {
|
||||
id: id.clone(),
|
||||
server: server.id.to_owned(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
icon: None,
|
||||
last_message_id: None,
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
voice: data.voice.map(|voice| voice.into()),
|
||||
slowmode: None
|
||||
},
|
||||
v0::LegacyServerChannelType::Voice => Channel::TextChannel {
|
||||
id: id.clone(),
|
||||
server: server.id.to_owned(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
icon: None,
|
||||
last_message_id: None,
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
voice: Some(data.voice.unwrap_or_default().into()),
|
||||
slowmode: None
|
||||
},
|
||||
};
|
||||
|
||||
db.insert_channel(&channel).await?;
|
||||
|
||||
if update_server {
|
||||
server
|
||||
.update(
|
||||
db,
|
||||
PartialServer {
|
||||
channels: Some([server.channels.clone(), [id].into()].concat()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
EventV1::ChannelCreate(channel.clone().into())
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
/// Create a group
|
||||
pub async fn create_group(
|
||||
db: &Database,
|
||||
mut data: v0::DataCreateGroup,
|
||||
owner_id: String,
|
||||
) -> Result<Channel> {
|
||||
data.users.insert(owner_id.to_string());
|
||||
|
||||
let config = config().await;
|
||||
if data.users.len() > config.features.limits.global.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.global.group_size,
|
||||
}));
|
||||
}
|
||||
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
|
||||
let icon = if let Some(icon_id) = data.icon {
|
||||
Some(File::use_channel_icon(db, &icon_id, &id, &owner_id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let recipients = data.users.into_iter().collect::<Vec<String>>();
|
||||
let channel = Channel::Group {
|
||||
id,
|
||||
|
||||
name: data.name,
|
||||
owner: owner_id,
|
||||
description: data.description,
|
||||
recipients: recipients.clone(),
|
||||
|
||||
icon,
|
||||
last_message_id: None,
|
||||
|
||||
permissions: None,
|
||||
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
};
|
||||
|
||||
db.insert_channel(&channel).await?;
|
||||
|
||||
let event = EventV1::ChannelCreate(channel.clone().into());
|
||||
for recipient in recipients {
|
||||
event.clone().private(recipient).await;
|
||||
}
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
/// Create a DM (or return the existing one / saved messages)
|
||||
pub async fn create_dm(db: &Database, user_a: &User, user_b: &User) -> Result<Channel> {
|
||||
// Try to find existing channel
|
||||
if let Ok(channel) = db.find_direct_message_channel(&user_a.id, &user_b.id).await {
|
||||
Ok(channel)
|
||||
} else {
|
||||
let channel = if user_a.id == user_b.id {
|
||||
// Create a new saved messages channel
|
||||
Channel::SavedMessages {
|
||||
id: Ulid::new().to_string(),
|
||||
user: user_a.id.to_string(),
|
||||
}
|
||||
} else {
|
||||
// Create a new DM channel
|
||||
Channel::DirectMessage {
|
||||
id: Ulid::new().to_string(),
|
||||
active: true, // show by default
|
||||
recipients: vec![user_a.id.clone(), user_b.id.clone()],
|
||||
last_message_id: None,
|
||||
}
|
||||
};
|
||||
|
||||
db.insert_channel(&channel).await?;
|
||||
|
||||
if let Channel::DirectMessage { .. } = &channel {
|
||||
let event = EventV1::ChannelCreate(channel.clone().into());
|
||||
event.clone().private(user_a.id.clone()).await;
|
||||
event.private(user_b.id.clone()).await;
|
||||
};
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add user to a group
|
||||
pub async fn add_user_to_group(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
user_id: &str,
|
||||
_by_id: &str,
|
||||
amqp: &AMQP,
|
||||
user: &User,
|
||||
by_id: &str,
|
||||
) -> Result<()> {
|
||||
if let Channel::Group { recipients, .. } = self {
|
||||
if recipients.contains(&String::from(user_id)) {
|
||||
if recipients.contains(&String::from(&user.id)) {
|
||||
return Err(create_error!(AlreadyInGroup));
|
||||
}
|
||||
|
||||
recipients.push(String::from(user_id));
|
||||
let config = config().await;
|
||||
if recipients.len() >= config.features.limits.global.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.global.group_size
|
||||
}));
|
||||
}
|
||||
|
||||
recipients.push(String::from(&user.id));
|
||||
}
|
||||
|
||||
match &self {
|
||||
Channel::Group { id, .. } => {
|
||||
db.add_user_to_group(id, user_id).await?;
|
||||
db.add_user_to_group(id, &user.id).await?;
|
||||
|
||||
EventV1::ChannelGroupJoin {
|
||||
id: id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user_id.to_string())
|
||||
.await;
|
||||
|
||||
/* TODO: SystemMessage::UserAdded {
|
||||
id: user.to_string(),
|
||||
by: by.to_string(),
|
||||
SystemMessage::UserAdded {
|
||||
id: user.id.to_string(),
|
||||
by: by_id.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok(); */
|
||||
.ok();
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user.id.to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -238,14 +420,34 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to this channel's id
|
||||
pub fn id(&self) -> String {
|
||||
/// Clone this channel's id
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Channel::DirectMessage { id, .. }
|
||||
| Channel::Group { id, .. }
|
||||
| Channel::SavedMessages { id, .. }
|
||||
| Channel::TextChannel { id, .. }
|
||||
| Channel::VoiceChannel { id, .. } => id.clone(),
|
||||
| Channel::TextChannel { id, .. } => id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone this channel's server id
|
||||
pub fn server(&self) -> Option<&str> {
|
||||
match self {
|
||||
Channel::TextChannel { server, .. } => Some(server),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets this channel's voice information
|
||||
pub fn voice(&self) -> Option<Cow<VoiceInformation>> {
|
||||
match self {
|
||||
Self::DirectMessage { .. } | Self::Group { .. } => {
|
||||
Some(Cow::Owned(VoiceInformation::default()))
|
||||
}
|
||||
Self::TextChannel {
|
||||
voice: Some(voice), ..
|
||||
} => Some(Cow::Borrowed(voice)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,12 +464,6 @@ impl Channel {
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
db.set_channel_role_permission(id, role_id, permissions)
|
||||
.await?;
|
||||
@@ -293,7 +489,7 @@ impl Channel {
|
||||
}
|
||||
|
||||
/// Update channel data
|
||||
pub async fn update<'a>(
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialChannel,
|
||||
@@ -305,8 +501,19 @@ impl Channel {
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_channel(&self.id(), &partial, remove.clone())
|
||||
.await?;
|
||||
let id = self.id().to_string();
|
||||
db.update_channel(&id, &partial, remove.clone()).await?;
|
||||
|
||||
EventV1::ChannelUpdate {
|
||||
id: id.clone(),
|
||||
data: partial.into(),
|
||||
clear: remove.into_iter().map(|v| v.into()).collect(),
|
||||
}
|
||||
.p(match self {
|
||||
Self::TextChannel { server, .. } => server.clone(),
|
||||
_ => id,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -315,17 +522,13 @@ impl Channel {
|
||||
pub fn remove_field(&mut self, field: &FieldsChannel) {
|
||||
match field {
|
||||
FieldsChannel::Description => match self {
|
||||
Self::Group { description, .. }
|
||||
| Self::TextChannel { description, .. }
|
||||
| Self::VoiceChannel { description, .. } => {
|
||||
Self::Group { description, .. } | Self::TextChannel { description, .. } => {
|
||||
description.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Icon => match self {
|
||||
Self::Group { icon, .. }
|
||||
| Self::TextChannel { icon, .. }
|
||||
| Self::VoiceChannel { icon, .. } => {
|
||||
Self::Group { icon, .. } | Self::TextChannel { icon, .. } => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
@@ -334,15 +537,17 @@ impl Channel {
|
||||
Self::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
} => {
|
||||
default_permissions.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Voice => match self {
|
||||
Self::TextChannel { voice, .. } => {
|
||||
voice.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +559,7 @@ impl Channel {
|
||||
}
|
||||
|
||||
/// Apply partial channel to channel
|
||||
#[allow(deprecated)]
|
||||
pub fn apply_options(&mut self, partial: PartialChannel) {
|
||||
match self {
|
||||
Self::SavedMessages { .. } => {}
|
||||
@@ -402,15 +608,7 @@ impl Channel {
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
voice,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
@@ -436,27 +634,56 @@ impl Channel {
|
||||
if let Some(v) = partial.default_permissions {
|
||||
default_permissions.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.voice {
|
||||
voice.replace(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledge a message
|
||||
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
|
||||
EventV1::ChannelAck {
|
||||
id: self.id().to_string(),
|
||||
user: user.to_string(),
|
||||
message_id: message.to_string(),
|
||||
}
|
||||
.private(user.to_string())
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "tasks")]
|
||||
crate::tasks::ack::queue_ack(
|
||||
self.id().to_string(),
|
||||
user.to_string(),
|
||||
crate::tasks::ack::AckEvent::AckMessage {
|
||||
id: message.to_string(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove user from a group
|
||||
pub async fn remove_user_from_group(
|
||||
&self,
|
||||
db: &Database,
|
||||
user_id: &str,
|
||||
_by_id: Option<&str>,
|
||||
amqp: &AMQP,
|
||||
user: &User,
|
||||
by_id: Option<&str>,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
match &self {
|
||||
Channel::Group {
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
recipients,
|
||||
..
|
||||
} => {
|
||||
if user_id == owner {
|
||||
if let Some(new_owner) = recipients.iter().find(|x| *x != user_id) {
|
||||
if &user.id == owner {
|
||||
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
|
||||
db.update_channel(
|
||||
id,
|
||||
&PartialChannel {
|
||||
@@ -467,42 +694,65 @@ impl Channel {
|
||||
)
|
||||
.await?;
|
||||
|
||||
/* TODO: SystemMessage::ChannelOwnershipChanged {
|
||||
SystemMessage::ChannelOwnershipChanged {
|
||||
from: owner.to_string(),
|
||||
to: new_owner.into(),
|
||||
to: new_owner.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
MessageAuthor::System {
|
||||
username: name,
|
||||
avatar: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok(); */
|
||||
.ok();
|
||||
} else {
|
||||
db.delete_channel(self).await?;
|
||||
return Ok(());
|
||||
return self.delete(db).await;
|
||||
}
|
||||
}
|
||||
|
||||
db.remove_user_from_group(id, &user.id).await?;
|
||||
|
||||
EventV1::ChannelGroupLeave {
|
||||
id: id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
if !silent {
|
||||
/* TODO: if let Some(_by) = by_id {
|
||||
if let Some(by) = by_id {
|
||||
SystemMessage::UserRemove {
|
||||
id: user_id.to_string(),
|
||||
id: user.id.to_string(),
|
||||
by: by.to_string(),
|
||||
}
|
||||
} else {
|
||||
SystemMessage::UserLeft {
|
||||
id: user_id.to_string(),
|
||||
id: user.id.to_string(),
|
||||
}
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.send(
|
||||
db,
|
||||
Some(amqp),
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok(); */
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -514,16 +764,82 @@ impl Channel {
|
||||
|
||||
/// Delete a channel
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
let id = self.id().to_string();
|
||||
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
|
||||
// TODO: missing functionality:
|
||||
// - group invites
|
||||
// - channels list / categories list on server
|
||||
db.delete_channel(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
impl IntoDocumentPath for FieldsChannel {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsChannel::Description => "description",
|
||||
FieldsChannel::Icon => "icon",
|
||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
||||
FieldsChannel::Voice => "voice",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
|
||||
use crate::{fixture, util::permissions::DatabasePermissionQuery};
|
||||
|
||||
#[async_std::test]
|
||||
async fn permissions_group_channel() {
|
||||
database_test!(|db| async move {
|
||||
fixture!(db, "group_with_members",
|
||||
owner user 0
|
||||
member1 user 1
|
||||
member2 user 2
|
||||
channel channel 3);
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
|
||||
assert!(calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &member1).channel(&channel);
|
||||
assert!(calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &member2).channel(&channel);
|
||||
assert!(!calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
});
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn permissions_text_channel() {
|
||||
database_test!(|db| async move {
|
||||
fixture!(db, "server_with_roles",
|
||||
owner user 0
|
||||
moderator user 1
|
||||
user user 2
|
||||
channel channel 3);
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
|
||||
assert!(calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &moderator).channel(&channel);
|
||||
assert!(calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &user).channel(&channel);
|
||||
assert!(!calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user