forked from jmug/stoatchat
Compare commits
729 Commits
0.5.3-alph
...
20241002-4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
26afbeed84 | ||
|
|
9975de01bc | ||
|
|
ac525466b8 | ||
|
|
c7a04e4559 | ||
|
|
5bbe30edbc | ||
|
|
31c7dc0577 | ||
|
|
aba5c7d8af | ||
|
|
2f4ea4cabb | ||
|
|
7e801a31bd | ||
|
|
eb4870c9d5 | ||
|
|
b1fdd7fc74 | ||
|
|
bf3e027067 | ||
|
|
a7a7e49e18 | ||
|
|
3c834d06d4 | ||
|
|
f5e6086d2a | ||
|
|
3365a8e482 | ||
|
|
268c2d1355 | ||
|
|
e9d6b963df | ||
|
|
f9d9447203 | ||
|
|
a0002d0b43 | ||
|
|
23188032ca | ||
|
|
f9f5a30e2c | ||
|
|
e393e17b59 | ||
|
|
a29d8f6aab | ||
|
|
1dbbc3ed8d | ||
|
|
410acb5b5c | ||
|
|
53c9160cd6 | ||
|
|
28b1170633 | ||
|
|
4abd4070f7 | ||
|
|
bca17b11a7 | ||
|
|
2a6d532852 | ||
|
|
a9a5af8cc8 | ||
|
|
edfa8e5256 | ||
|
|
2ebcfdc770 | ||
|
|
fc47df786b | ||
|
|
f6aa405607 | ||
|
|
101e8feaa2 | ||
|
|
df8f588e15 | ||
|
|
3ef549537e | ||
|
|
d81d08f1ce | ||
|
|
a77b7717b8 | ||
|
|
d5f903781d | ||
|
|
8b12546b56 | ||
|
|
5bb91a7113 | ||
|
|
a04c2512b4 | ||
|
|
c3f0df6567 | ||
|
|
8b8aec4dc1 | ||
|
|
1d228ff76e | ||
|
|
b5863c0104 | ||
|
|
08ccc43f8d | ||
|
|
e33ae17061 | ||
|
|
60ebdb64d7 | ||
|
|
c27d0ad5bb | ||
|
|
c1b9d941f3 | ||
|
|
175866ce00 | ||
|
|
8c5de18991 | ||
|
|
1afb27a508 | ||
|
|
6a8481f7df | ||
|
|
7f201565c0 | ||
|
|
ae9474b95d | ||
|
|
050f3abc89 | ||
|
|
888c22cb54 | ||
|
|
f633fccbca | ||
|
|
39fec310f9 | ||
|
|
d5d922d830 | ||
|
|
f8c8407af3 | ||
|
|
e6d0d44c5a | ||
|
|
1933c9ea3d | ||
|
|
8a695b4bb5 | ||
|
|
bf71b45fbb | ||
|
|
9124c9f1e3 | ||
|
|
bcc2d05625 | ||
|
|
22bfd720b5 | ||
|
|
63f56aec0c | ||
|
|
dd3d7e9c49 | ||
|
|
dbb66edd9f | ||
|
|
8bfb48dff3 | ||
|
|
12d963d2bd | ||
|
|
c817c2dd40 | ||
|
|
8a230ba989 | ||
|
|
2a9cc3190c | ||
|
|
e84d55a697 | ||
|
|
403a94f70c | ||
|
|
633eb78630 | ||
|
|
bbe1f4936c | ||
|
|
11a87263be | ||
|
|
8b30dddc06 | ||
|
|
b93dd90caf | ||
|
|
0054019f82 | ||
|
|
736220a94e | ||
|
|
a9c82791b3 | ||
|
|
eacf4decab | ||
|
|
b8cda2ec74 | ||
|
|
69ab7e031b | ||
|
|
ace6431cb8 | ||
|
|
40790de909 | ||
|
|
750037b5d2 | ||
|
|
6b10385c0d | ||
|
|
56ead0f894 | ||
|
|
d633cba630 | ||
|
|
e43833c0ea | ||
|
|
7f86337cb2 | ||
|
|
f2bb388b3b | ||
|
|
43e45aef3f | ||
|
|
c5880596a4 | ||
|
|
59a644891d | ||
|
|
a326cdc736 | ||
|
|
03a28dbb3e | ||
|
|
32542a822e | ||
|
|
1df90ff53b | ||
|
|
57f00a283b | ||
|
|
312e1dd41f | ||
|
|
487b979f0d | ||
|
|
b9d813d8f0 | ||
|
|
36dd128459 | ||
|
|
426d836a09 | ||
|
|
2fad66d24a | ||
|
|
144f0d39c6 | ||
|
|
83429f9f65 | ||
|
|
a60ae78176 | ||
|
|
7b39f4e9d9 | ||
|
|
32a294a64a | ||
|
|
b7b70346b4 | ||
|
|
59832e6ba2 | ||
|
|
f309218573 | ||
|
|
0321eff62b | ||
|
|
01577fd798 | ||
|
|
4c8ea31d98 | ||
|
|
92ac86a6bd | ||
|
|
13ed69c82a | ||
|
|
b83f6da648 | ||
|
|
89f1167239 | ||
|
|
82d868751f | ||
|
|
31b9f18921 | ||
|
|
fd80823910 | ||
|
|
6bd8221eda | ||
|
|
ce77e926a5 | ||
|
|
2710edb76b | ||
|
|
8f1ff9e774 | ||
|
|
c09244039e | ||
|
|
bf9108408e | ||
|
|
fca91e1560 | ||
|
|
304336d905 | ||
|
|
42b4906594 | ||
|
|
3c881a40a6 | ||
|
|
3038fb230f | ||
|
|
73af105feb | ||
|
|
2de37b23c1 | ||
|
|
0c75d8c8b3 | ||
|
|
be793b1570 | ||
|
|
5a3a97df34 | ||
|
|
0c072b01d7 | ||
|
|
056c0380b2 | ||
|
|
49598daf70 | ||
|
|
0445181d5b | ||
|
|
e780178cc0 | ||
|
|
c4b9884825 | ||
|
|
fdab997d17 | ||
|
|
8e0addc3e0 | ||
|
|
0da4baf777 | ||
|
|
c03fcda85b | ||
|
|
c7849a82bf | ||
|
|
781d51df6f | ||
|
|
0afbcc065f | ||
|
|
a406e5504d | ||
|
|
e8c233f60b | ||
|
|
758b1e43e7 | ||
|
|
5cc8231c36 | ||
|
|
72595f30d4 | ||
|
|
39e345af74 | ||
|
|
7b447faefa | ||
|
|
a017256e95 | ||
|
|
1050891d87 | ||
|
|
e0b918771d | ||
|
|
7a6bd70dcd | ||
|
|
c0ef3d295a | ||
|
|
f5c95f9d12 | ||
|
|
fed1da72fd | ||
|
|
cf9c0bc5e1 | ||
|
|
cc2054838a | ||
|
|
a246cff7f0 | ||
|
|
d9d2735efa | ||
|
|
94b0472934 | ||
|
|
4a7917e9ea | ||
|
|
ce6b454874 | ||
|
|
fc32996b51 | ||
|
|
d74104fae4 | ||
|
|
32d3683013 | ||
|
|
0cebb69e02 | ||
|
|
117958d8cd | ||
|
|
c5b823ad13 | ||
|
|
27b4699299 | ||
|
|
c218db5047 | ||
|
|
c7f2f3614f | ||
|
|
c826d8cf07 | ||
|
|
5cb2320760 | ||
|
|
ee1c4cc2d5 | ||
|
|
0e0daf86f7 | ||
|
|
572e08c733 | ||
|
|
9011939344 | ||
|
|
f4dcd091cc | ||
|
|
86ad72b426 | ||
|
|
044d82d566 | ||
|
|
deffe663cb | ||
|
|
e7f6433d77 | ||
|
|
345e4d5f13 | ||
|
|
81fc3e7760 | ||
|
|
ca8d2e6118 | ||
|
|
c1227e8e33 | ||
|
|
0e0c6f2c3a | ||
|
|
fa82be74b5 | ||
|
|
8491ced13d | ||
|
|
229d4e2e1d | ||
|
|
7bdd2d69d6 | ||
|
|
ab2af9b5e5 | ||
|
|
5aebda2d16 | ||
|
|
a7273d0556 | ||
|
|
72aed64e5b | ||
|
|
00ad4d54e7 | ||
|
|
cba8c4ef3b | ||
|
|
b6e395fe0d | ||
|
|
8d07457c58 | ||
|
|
f1171e5358 | ||
|
|
d96c9f62c4 | ||
|
|
4f73e43a03 | ||
|
|
741b8ee8fd | ||
|
|
692081b7f0 | ||
|
|
943b1f08f0 | ||
|
|
0e5c8ed59b | ||
|
|
a7e0c42ee4 | ||
|
|
386f027a5a | ||
|
|
30c7f553d8 | ||
|
|
e291eef364 | ||
|
|
73443ce883 | ||
|
|
47297e243f | ||
|
|
4188b6d2f2 | ||
|
|
165380c7b4 | ||
|
|
49ca24ac9f | ||
|
|
35f956ce7d | ||
|
|
4baab5d5d5 | ||
|
|
7fc4fb2df7 | ||
|
|
ef757aa2fb | ||
|
|
0585dd0c20 | ||
|
|
f96541efab | ||
|
|
c6414338b6 | ||
|
|
4c4eb60cdb | ||
|
|
11d89b3bf0 | ||
|
|
64bb171cc8 | ||
|
|
6de5ad15c5 | ||
|
|
b5ab16d66f | ||
|
|
b9aad6d38c | ||
|
|
ed5c8159e9 | ||
|
|
2e6e1fdd44 | ||
|
|
cb2e6fb2f7 | ||
|
|
d069acd5a5 | ||
|
|
f8b5c25c17 | ||
|
|
80af18761f | ||
|
|
98e10c3478 | ||
|
|
2051c8ce45 | ||
|
|
0abc12e5fa | ||
|
|
aaceb5bdee | ||
|
|
3de7ee4c7b | ||
|
|
d660127c14 | ||
|
|
7390b3c087 | ||
|
|
020f2a1b1a | ||
|
|
3e55ff3577 | ||
|
|
bb70294546 | ||
|
|
9c93017b8a | ||
|
|
57592c0862 | ||
|
|
f09981695e | ||
|
|
ad76e85c8d | ||
|
|
a100b7aefa | ||
|
|
a06ef2ba1a | ||
|
|
6336b254c5 | ||
|
|
f1dd7d2c78 | ||
|
|
ae3ed7c124 | ||
|
|
5237ecbb90 | ||
|
|
2ce610e1e7 | ||
|
|
5d8432e267 | ||
|
|
44311c87be | ||
|
|
ee9622251a | ||
|
|
d13573eb45 | ||
|
|
20e93b9838 | ||
|
|
dc6ce96aa2 | ||
|
|
de5428348c | ||
|
|
d7527d9131 | ||
|
|
c59c4146c4 | ||
|
|
bd455b59fb | ||
|
|
d03cb44071 | ||
|
|
ac4cedd014 | ||
|
|
09e918d8e0 | ||
|
|
97fef56bad | ||
|
|
408743476b | ||
|
|
fad53ee5f0 | ||
|
|
be9a5dba5b | ||
|
|
c33d387486 | ||
|
|
0651a501f6 | ||
|
|
278d795d50 | ||
|
|
6281418ecb | ||
|
|
ae0e5cd7bf | ||
|
|
45a3b4ba15 | ||
|
|
8b72e2fa0e | ||
|
|
e5af59d307 | ||
|
|
6a7e7e9501 | ||
|
|
247c059f29 | ||
|
|
621dd86845 | ||
|
|
2b016c34f7 | ||
|
|
e1169c5335 | ||
|
|
fad540a87a | ||
|
|
00cb132d29 | ||
|
|
7757769f79 | ||
|
|
2d10180f08 | ||
|
|
180ac67056 | ||
|
|
350bd9c0be | ||
|
|
367c4e3260 | ||
|
|
02ba8c7978 | ||
|
|
d7839f1790 | ||
|
|
9c71f09a84 | ||
|
|
b32503d29f | ||
|
|
535665ca1b | ||
|
|
da08a7d722 | ||
|
|
5ce0c133e8 | ||
|
|
3adbf29060 | ||
|
|
3d98f5dfbb | ||
|
|
54b6cf67ab | ||
|
|
b4a2e8c366 | ||
|
|
3e135a1a9c | ||
|
|
b351ef0332 | ||
|
|
87391a49a5 | ||
|
|
d2f0fb3414 | ||
|
|
7ea5146e58 | ||
|
|
3d4c40dac0 | ||
|
|
c3a04d02c5 | ||
|
|
1dbd66a45c | ||
|
|
10645638a2 | ||
|
|
24def22613 | ||
|
|
a2911e42c6 | ||
|
|
558e4b86f1 | ||
|
|
e245b272fa | ||
|
|
b4d825b9a1 | ||
|
|
aee6c751c6 | ||
|
|
0232d721db | ||
|
|
8dd9acdb59 | ||
|
|
43c475eabb | ||
|
|
25a52cbbb1 | ||
|
|
14e17b10c0 | ||
|
|
a0926eea32 | ||
|
|
3ae23af275 | ||
|
|
927556041b | ||
|
|
bbb368217c | ||
|
|
f568838ff9 | ||
|
|
3c5e494216 | ||
|
|
465d766254 | ||
|
|
8d8bcff31f | ||
|
|
3df869c21d | ||
|
|
a495a8dfd7 | ||
|
|
0553ce5587 | ||
|
|
fcbaab7bcb | ||
|
|
437a620274 | ||
|
|
30af92bee4 | ||
|
|
0ecce98056 | ||
|
|
04c2f8f293 | ||
|
|
d45b32bce6 | ||
|
|
23ec914033 | ||
|
|
71c01e0817 | ||
|
|
7b1a3dc7dd | ||
|
|
64d1126af9 | ||
|
|
4c40c6c2cc | ||
|
|
7befd77c5b | ||
|
|
aab85fa143 | ||
|
|
d7be1db755 | ||
|
|
2580cec272 | ||
|
|
489c2bdd87 | ||
|
|
061af1f2c3 | ||
|
|
50d9f50c8f | ||
|
|
28db08ba88 | ||
|
|
9da51b2d45 | ||
|
|
6a871fa944 | ||
|
|
4801b51cef | ||
|
|
7a9adb45f7 | ||
|
|
f0eb228b19 | ||
|
|
5aee381fd6 | ||
|
|
249b1161f9 | ||
|
|
1988a9501b | ||
|
|
6d230e57f5 | ||
|
|
9f1eda4af0 | ||
|
|
734dc2bb59 | ||
|
|
e22d991995 | ||
|
|
37ffde23ce | ||
|
|
f676071641 | ||
|
|
a8d5fcec9e | ||
|
|
d4aff072a8 | ||
|
|
57d114fee9 | ||
|
|
a4cbbfea51 | ||
|
|
a6cec559b1 | ||
|
|
8b4541feb0 | ||
|
|
88273021a2 | ||
|
|
e367c42dca | ||
|
|
88f811761b | ||
|
|
b58755dbeb | ||
|
|
780b88eb2f | ||
|
|
1aafa51033 | ||
|
|
63064cc9bb | ||
|
|
1d5800a3b8 | ||
|
|
c1cd4bfa66 | ||
|
|
c39a9917df | ||
|
|
0fdb749199 | ||
|
|
ab4c3f913e | ||
|
|
8be51dc4f0 | ||
|
|
96fd18f679 | ||
|
|
2db54cf8de | ||
|
|
c3f092aaef | ||
|
|
ce68f7eeed | ||
|
|
139e704746 | ||
|
|
345c4c627c | ||
|
|
03ca611553 | ||
|
|
0fe63e3955 | ||
|
|
4474061103 | ||
|
|
788580a64f |
2
.config/nextest.toml
Normal file
2
.config/nextest.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "3s", terminate-after = 2 }
|
||||
@@ -1,3 +1,4 @@
|
||||
Dockerfile
|
||||
target
|
||||
.mongo
|
||||
.env
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
ko_fi: insertish
|
||||
custom: https://insrt.uk/donate
|
||||
37
.github/workflows/book.yml
vendored
Normal file
37
.github/workflows/book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Build documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pages: write # To push to a GitHub Pages site
|
||||
id-token: write # To update the deployment status
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install latest mdbook
|
||||
run: |
|
||||
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
|
||||
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
mkdir mdbook
|
||||
curl -sSL $url | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Build Book
|
||||
run: |
|
||||
cd doc
|
||||
mdbook build
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "doc/book"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
133
.github/workflows/docker.yaml
vendored
Normal file
133
.github/workflows/docker.yaml
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
name: Docker Test & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
# branches:
|
||||
# - "main"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
base:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build base image
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- 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 }}
|
||||
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
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
|
||||
if: github.event_name != 'pull_request'
|
||||
strategy:
|
||||
matrix:
|
||||
project: [delta, bonfire, autumn, january]
|
||||
name: Build ${{ matrix.project }} image
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
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:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Resolve the correct project
|
||||
- uses: kanga333/variable-mapper@master
|
||||
id: export
|
||||
with:
|
||||
key: "${{ matrix.project }}"
|
||||
map: |
|
||||
{
|
||||
"delta": {
|
||||
"path": "crates/delta",
|
||||
"tag": "${{ github.repository_owner }}/server"
|
||||
},
|
||||
"bonfire": {
|
||||
"path": "crates/bonfire",
|
||||
"tag": "${{ github.repository_owner }}/bonfire"
|
||||
},
|
||||
"autumn": {
|
||||
"path": "crates/services/autumn",
|
||||
"tag": "${{ github.repository_owner }}/autumn"
|
||||
},
|
||||
"january": {
|
||||
"path": "crates/services/january",
|
||||
"tag": "${{ github.repository_owner }}/january"
|
||||
}
|
||||
}
|
||||
export_to: output
|
||||
|
||||
# Configure metadata
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/${{ steps.export.outputs.tag }}
|
||||
ghcr.io/${{ steps.export.outputs.tag }}
|
||||
|
||||
# Build crate image
|
||||
- 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 }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
103
.github/workflows/docker.yml
vendored
103
.github/workflows/docker.yml
vendored
@@ -1,103 +0,0 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- ".gitignore"
|
||||
- ".gitlab-ci.yml"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [linux/amd64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.architecture }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
|
||||
publish_amd64:
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache amd64 Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache/linux/amd64
|
||||
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: revoltchat/server, ghcr.io/revoltchat/server
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and publish
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
79
.github/workflows/rust.yaml
vendored
79
.github/workflows/rust.yaml
vendored
@@ -1,10 +1,17 @@
|
||||
name: Rust build and test
|
||||
name: Rust build, test, and generate specification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/rust.yaml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -15,19 +22,71 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install latest nightly
|
||||
- name: Install latest stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Install cargo-nextest
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
with:
|
||||
crate: cargo-nextest
|
||||
locked: true
|
||||
|
||||
- name: Run cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
||||
|
||||
- name: Run services in background
|
||||
run: |
|
||||
docker compose -f compose.yml up -d
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo nextest run
|
||||
|
||||
- name: Run cargo test (with MongoDB)
|
||||
env:
|
||||
TEST_DB: MONGODB
|
||||
MONGODB: mongodb://localhost
|
||||
run: |
|
||||
cargo nextest run
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
|
||||
|
||||
- name: Wait for API to go up
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: nev7n/wait_for_response@v1
|
||||
with:
|
||||
command: test
|
||||
url: "http://localhost:8000/"
|
||||
|
||||
- name: Checkout API repository
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: revoltchat/api
|
||||
path: api
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
with:
|
||||
cwd: "api"
|
||||
add: "*.json"
|
||||
author_name: Revolt CI
|
||||
author_email: revolt-ci@users.noreply.github.com
|
||||
message: "chore: generate OpenAPI specification"
|
||||
|
||||
27
.github/workflows/triage_issue.yml
vendored
27
.github/workflows/triage_issue.yml
vendored
@@ -15,22 +15,27 @@ jobs:
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
projectV2(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||
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:
|
||||
@@ -39,11 +44,11 @@ jobs:
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||
projectNextItem {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
39
.github/workflows/triage_pr.yml
vendored
39
.github/workflows/triage_pr.yml
vendored
@@ -2,7 +2,7 @@ name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
@@ -15,22 +15,27 @@ jobs:
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
projectV2(number: 5) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
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=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
@@ -39,13 +44,13 @@ jobs:
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||
projectNextItem {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
@@ -59,14 +64,16 @@ jobs:
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectNextItemField(input: {
|
||||
set_status: updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: $status_value
|
||||
value: {
|
||||
singleSelectOptionId: $status_value
|
||||
}
|
||||
}) {
|
||||
projectNextItem {
|
||||
projectV2Item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -1,7 +1,9 @@
|
||||
Rocket.toml
|
||||
/target
|
||||
/target_backup
|
||||
**/*.rs.bk
|
||||
.mongo
|
||||
Revolt.*.toml
|
||||
|
||||
target
|
||||
.data
|
||||
.env
|
||||
avatar.png
|
||||
|
||||
.vercel
|
||||
.DS_Store
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
stages:
|
||||
- build
|
||||
|
||||
# Rocket does not currently compile on stable Rust.
|
||||
# Once it does, we can uncomment this, and instead
|
||||
# put `allow-failure: true` on the nightly build.
|
||||
#
|
||||
# rust-latest:
|
||||
# stage: build
|
||||
# image: rust:latest
|
||||
# script:
|
||||
# - cargo build --verbose
|
||||
# - cargo test --verbose
|
||||
|
||||
rust-nightly:
|
||||
stage: build
|
||||
image: rustlang/rust:nightly
|
||||
script:
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"rust-analyzer.diagnostics.disabled": [
|
||||
"unresolved-macro-call"
|
||||
]
|
||||
}
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"nixEnvSelector.suggestion": false
|
||||
}
|
||||
|
||||
7551
Cargo.lock
generated
7551
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
79
Cargo.toml
79
Cargo.toml
@@ -1,65 +1,16 @@
|
||||
[package]
|
||||
name = "revolt"
|
||||
# To help optimise CI and Docker builds.
|
||||
# Version here is left as 0.0.0, please
|
||||
# adjust and run ./set_version.sh instead.
|
||||
version = "0.0.0"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/delta",
|
||||
"crates/bonfire",
|
||||
"crates/core/*",
|
||||
"crates/services/*",
|
||||
"crates/bindings/*",
|
||||
]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
lru = "0.7.0"
|
||||
url = "2.2.2"
|
||||
log = "0.4.11"
|
||||
dotenv = "0.15.0"
|
||||
linkify = "0.6.0"
|
||||
once_cell = "1.4.1"
|
||||
env_logger = "0.7.1"
|
||||
lazy_static = "1.4.0"
|
||||
ctrlc = { version = "3.0", features = ["termination"] }
|
||||
|
||||
# Lang. Utilities
|
||||
regex = "1"
|
||||
num_enum = "0.5.1"
|
||||
impl_ops = "0.1.1"
|
||||
bitfield = "0.13.2"
|
||||
phf = { version = "0.9.0", features = ["macros"] }
|
||||
|
||||
# ID / key generation
|
||||
ulid = "0.4.1"
|
||||
nanoid = "0.4.0"
|
||||
base64 = "0.13.0"
|
||||
|
||||
# serde
|
||||
serde_json = "1.0.57"
|
||||
serde = { version = "1.0.115", features = ["derive"] }
|
||||
validator = { version = "0.11", features = ["derive"] }
|
||||
rmp-serde = { git = "https://github.com/3Hren/msgpack-rust", rev = "5bf2c24203ad422233cf35b7b7bfad9f7e811814" }
|
||||
|
||||
# async
|
||||
futures = "0.3.8"
|
||||
chrono = "0.4.15"
|
||||
async-channel = "1.6.1"
|
||||
reqwest = { version = "0.11.4", features = ["json"] }
|
||||
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
|
||||
|
||||
# internal util
|
||||
web-push = "0.7.2"
|
||||
many-to-many = "0.1.2"
|
||||
lettre = "0.10.0-alpha.4"
|
||||
rauth = { git = "https://github.com/insertish/rauth", rev = "775a6f3ffb9ffc0e382af32fac2924e9a707e890" }
|
||||
hive_pubsub = { git = "https://gitlab.insrt.uk/insert/hive", rev = "b0f3db9d33990530d7640d4bbb309c8d9eb5c0cf", features = ["redis-backend"] }
|
||||
|
||||
# redis
|
||||
redis = { version = "0.21.2", features = ["async-std-comp"] }
|
||||
mobc = { version = "0.7.3" }
|
||||
mobc-redis = { version = "0.7.0", default-features = false, features = ["async-std-comp"] }
|
||||
|
||||
# web
|
||||
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
|
||||
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] }
|
||||
mongodb = { version = "1.2.2", features = ["tokio-runtime"], default-features = false }
|
||||
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }
|
||||
[patch.crates-io]
|
||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||
redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
|
||||
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
|
||||
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
|
||||
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
|
||||
|
||||
49
Dockerfile
49
Dockerfile
@@ -1,25 +1,36 @@
|
||||
# Build Stage
|
||||
FROM rustlang/rust:nightly-slim AS builder
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.77.2-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
RUN USER=root cargo new --bin revolt
|
||||
WORKDIR /home/rust/src/revolt
|
||||
ARG TARGETARCH
|
||||
|
||||
# Install build requirements
|
||||
RUN dpkg --add-architecture "${TARGETARCH}"
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
make \
|
||||
pkg-config \
|
||||
libssl-dev:"${TARGETARCH}"
|
||||
COPY scripts/build-image-layer.sh /tmp/
|
||||
RUN sh /tmp/build-image-layer.sh tools
|
||||
|
||||
# Build all dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src/bin/dummy.rs ./src/bin/dummy.rs
|
||||
RUN apt-get update && apt-get install -y libssl-dev pkg-config && cargo build --release --bin dummy
|
||||
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
|
||||
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/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/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
COPY assets/templates ./assets/templates
|
||||
COPY src ./src
|
||||
RUN cargo install --locked --path .
|
||||
|
||||
# Bundle Stage
|
||||
FROM debian:buster-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
COPY --from=builder /usr/local/cargo/bin/revolt ./
|
||||
COPY assets ./assets
|
||||
EXPOSE 8000
|
||||
EXPOSE 9000
|
||||
ENV ROCKET_ADDRESS 0.0.0.0
|
||||
ENV ROCKET_PORT 8000
|
||||
CMD ["./revolt"]
|
||||
# Build all apps
|
||||
COPY crates ./crates
|
||||
RUN sh /tmp/build-image-layer.sh apps
|
||||
|
||||
32
Dockerfile.useCurrentArch
Normal file
32
Dockerfile.useCurrentArch
Normal file
@@ -0,0 +1,32 @@
|
||||
# Build Stage
|
||||
FROM rust:1.77.2-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/bindings/node/Cargo.toml ./crates/bindings/node/
|
||||
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/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/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
COPY crates ./crates
|
||||
RUN sh /tmp/build-image-layer.sh apps
|
||||
15
LICENSE
15
LICENSE
@@ -1,7 +1,10 @@
|
||||
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. <https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
@@ -629,8 +632,8 @@ to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Server
|
||||
Copyright (C) 2021 REVOLT
|
||||
Revolt Project
|
||||
Copyright (C) 2022 Pawel Makles
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
@@ -643,7 +646,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
@@ -658,4 +661,4 @@ specific requirements.
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
192
README.md
192
README.md
@@ -1,45 +1,179 @@
|
||||
# Delta
|
||||
<div align="center">
|
||||
<h1>
|
||||
Revolt Backend
|
||||
|
||||
[](https://github.com/revoltchat/backend/stargazers)
|
||||
[](https://github.com/revoltchat/backend/network/members)
|
||||
[](https://github.com/revoltchat/backend/pulls)
|
||||
[](https://github.com/revoltchat/backend/issues)
|
||||
[](https://github.com/revoltchat/backend/graphs/contributors)
|
||||
[](https://github.com/revoltchat/backend/blob/main/LICENSE)
|
||||
</h1>
|
||||
The services and libraries that power the Revolt service.<br/>
|
||||
<br/>
|
||||
|
||||
## Description
|
||||
| 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 |     |
|
||||
| `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/autumn` | [crates/services/autumn](crates/services/autumn) | File server |  |
|
||||
| `bindings/node` | [crates/bindings/node](crates/bindings/node) | Node.js bindings for the Revolt software |  |
|
||||
|
||||
Delta is a blazing fast API server built with Rust for Revolt.
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
**Features:**
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
- Robust and efficient API routes for running a chat platform.
|
||||
- Distributed notification system, allowing any node to be seamlessly connected.
|
||||
- Simple deployment, based mostly on pure Rust code and libraries.
|
||||
- Hooks up to a MongoDB deployment, provide URI and no extra work needed.
|
||||
Rust 1.76 or higher.
|
||||
|
||||
## Stack
|
||||
> [!CAUTION]
|
||||
> The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341).
|
||||
|
||||
- [Rocket](https://rocket.rs/) (REST)
|
||||
- [Async Tungstenite](https://github.com/sdroege/async-tungstenite) (WebSockets)
|
||||
- [MongoDB](https://mongodb.com/)
|
||||
## Development Guide
|
||||
|
||||
## Resources
|
||||
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
|
||||
|
||||
### Revolt
|
||||
Before getting started, you'll want to install:
|
||||
|
||||
- [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)
|
||||
- Rust toolchain (rustup recommended)
|
||||
- Docker
|
||||
- Git
|
||||
- mold (optional, faster compilation)
|
||||
|
||||
## CLI Commands
|
||||
> A **default.nix** is available for Nix users!
|
||||
> Just run `nix-shell` and continue.
|
||||
|
||||
| Command | Description |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------- |
|
||||
| `./publish.sh` | Publish a Docker Image. |
|
||||
| `./set_version.sh` | Update the version. **Not intended for PR use.** |
|
||||
| `cargo build` | Build/compile Delta. |
|
||||
| `cargo run` | Run Delta. |
|
||||
| `cargo fmt` | Format Delta. Not intended for PR use to avoid accidentally formatting unformatted files. |
|
||||
As a heads-up, the development environment uses the following ports:
|
||||
|
||||
## Contributing
|
||||
| Service | Port |
|
||||
| ------------------------- | :------------: |
|
||||
| MongoDB | 14017 |
|
||||
| Redis | 14079 |
|
||||
| MinIO | 14009 |
|
||||
| Maildev | 14025<br>14080 |
|
||||
| Revolt Web App | 14701 |
|
||||
| `crates/delta` | 14702 |
|
||||
| `crates/bonfire` | 14703 |
|
||||
| `crates/services/autumn` | 14704 |
|
||||
| `crates/services/january` | 14705 |
|
||||
|
||||
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.
|
||||
Now you can clone and build the project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/revoltchat/backend revolt-backend
|
||||
cd revolt-backend
|
||||
cargo 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"
|
||||
> ```
|
||||
|
||||
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 th proxy server
|
||||
cargo run --bin revolt-january
|
||||
|
||||
# hint:
|
||||
# mold -run <cargo build, cargo run, etc...>
|
||||
```
|
||||
|
||||
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/revoltchat/revite
|
||||
cd revite
|
||||
yarn
|
||||
yarn build:deps
|
||||
echo "VITE_API_URL=http://local.revolt.chat:14702" > .env.local
|
||||
yarn dev --port 14701
|
||||
```
|
||||
|
||||
Then go to http://local.revolt.chat:14701 to create an account/login.
|
||||
|
||||
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/revoltchat/backend/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
|
||||
|
||||
Delta is licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/delta/blob/master/LICENSE).
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
|
||||
|
||||
**Individual crates may supply their own licenses!**
|
||||
|
||||
49
Revolt.toml
Normal file
49
Revolt.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
# ⚠️ This configuration is intended for development environment.
|
||||
# If you'd like to override anything, create a Revolt.override.toml
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://127.0.0.1:14017"
|
||||
# Redis connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
redis = "redis://127.0.0.1:14079/"
|
||||
|
||||
[hosts]
|
||||
# Web locations of various services
|
||||
# Defaults assume all services are reverse-proxied
|
||||
# See https://github.com/revoltchat/self-hosted/blob/master/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 = ""
|
||||
|
||||
[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
|
||||
|
||||
[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"
|
||||
73
STYLE_GUIDE.md
Normal file
73
STYLE_GUIDE.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
||||
## Writing Style
|
||||
|
||||
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
|
||||
|
||||
## `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.
|
||||
@@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-GB">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Reset your password.</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
|
||||
<style type="text/css">
|
||||
a[x-apple-data-detectors] {color: inherit !important;}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px 0 30px 0;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#ff4654">
|
||||
<img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="color: #153643; font-family: Arial, sans-serif;">
|
||||
<h1 style="font-size: 24px; margin: 0;">Reset your password!</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
|
||||
<p>
|
||||
You requested a password reset, if you didn't perform this action you can safely ignore this email.
|
||||
</p>
|
||||
<p style="margin: 0;">
|
||||
Reset your password by navigating to <a href="{{url}}">{{url}}</a>.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ff4654" style="padding: 30px 30px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
|
||||
<p style="margin: 0;">Sent by Revolt. · Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
|
||||
<p>Revolt is a user-first chat platform built with modern web technologies.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,14 +0,0 @@
|
||||
<h2>Reset your password.</h2>
|
||||
<p>
|
||||
You requested a password reset, if you did not perform this action you can safely ignore this email.
|
||||
</p>
|
||||
<p>
|
||||
Reset your password here: <a href="{{url}}">{{url}}</a>
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
|
||||
</p>
|
||||
<p>
|
||||
Revolt is a user-first chat platform built with modern web technologies.
|
||||
</p>
|
||||
@@ -1,63 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-GB">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Verify your account.</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
|
||||
<style type="text/css">
|
||||
a[x-apple-data-detectors] {color: inherit !important;}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px 0 30px 0;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#ff4654">
|
||||
<img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="color: #153643; font-family: Arial, sans-serif;">
|
||||
<h1 style="font-size: 24px; margin: 0;">You're almost there!</h1>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
|
||||
<p>
|
||||
Verify your account to be able to log into the platform. If you didn't perform this action you can safely ignore this email.
|
||||
</p>
|
||||
<p style="margin: 0;">
|
||||
Please verify your account by navigating to <a href="{{url}}">{{url}}</a>.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ff4654" style="padding: 30px 30px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
|
||||
<p style="margin: 0;">Sent by Revolt. · Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
|
||||
<p>Revolt is a user-first chat platform built with modern web technologies.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
You're almost there!
|
||||
If you did not perform this action you can safely ignore this email.
|
||||
|
||||
Please verify your account here: {{url}}
|
||||
|
||||
Sent by Revolt.
|
||||
@@ -1,15 +0,0 @@
|
||||
<h2>You're almost there!</h2>
|
||||
<p>
|
||||
Verify your account to be able to log into the platform.<br/>
|
||||
If you did not perform this action you can safely ignore this email.
|
||||
</p>
|
||||
<p>
|
||||
Please verify your account here: <a href="{{url}}">{{url}}</a>
|
||||
</p>
|
||||
<br/>
|
||||
<p>
|
||||
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
|
||||
</p>
|
||||
<p>
|
||||
Revolt is a user-first chat platform built with modern web technologies.
|
||||
</p>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
28
clippy.toml
Normal file
28
clippy.toml
Normal file
@@ -0,0 +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::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",
|
||||
|
||||
# Prefer to use Object::delete(&self)
|
||||
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
||||
]
|
||||
56
compose.yml
Normal file
56
compose.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
# Redis
|
||||
redis:
|
||||
image: eqalpha/keydb
|
||||
ports:
|
||||
- "14079:6379"
|
||||
|
||||
# MongoDB
|
||||
database:
|
||||
image: mongo
|
||||
ports:
|
||||
- "14017:27017"
|
||||
volumes:
|
||||
- ./.data/db:/data/db
|
||||
|
||||
# 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
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioautumn
|
||||
MINIO_ROOT_PASSWORD: minioautumn
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
/usr/bin/mc config host add minio http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD;
|
||||
while ! /usr/bin/mc ready minio; do echo 'Waiting minio...' && sleep 1; done;
|
||||
/usr/bin/mc mb minio/revolt-uploads;
|
||||
exit 0;
|
||||
"
|
||||
|
||||
# Mock SMTP server
|
||||
maildev:
|
||||
image: soulteary/maildev
|
||||
ports:
|
||||
- "14025:25"
|
||||
- "14080:8080"
|
||||
environment:
|
||||
MAILDEV_SMTP_PORT: 25
|
||||
MAILDEV_WEB_PORT: 8080
|
||||
MAILDEV_INCOMING_USER: smtp
|
||||
MAILDEV_INCOMING_PASS: smtp
|
||||
7
crates/bindings/node/.gitignore
vendored
Normal file
7
crates/bindings/node/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
target
|
||||
index.node
|
||||
**/node_modules
|
||||
**/.DS_Store
|
||||
npm-debug.log*
|
||||
cargo.log
|
||||
cross.log
|
||||
25
crates/bindings/node/Cargo.toml
Normal file
25
crates/bindings/node/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "revolt-nodejs-bindings"
|
||||
version = "0.7.18"
|
||||
description = "Node.js bindings for the Revolt software"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
exclude = ["index.node"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
neon = "1.0.0"
|
||||
neon-serde4 = "1.0.0"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
async-std = "1.12.0"
|
||||
|
||||
revolt-config = { version = "0.7.18", path = "../../core/config" }
|
||||
revolt-result = { version = "0.7.18", path = "../../core/result" }
|
||||
revolt-database = { version = "0.7.18", path = "../../core/database" }
|
||||
92
crates/bindings/node/README.md
Normal file
92
crates/bindings/node/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# revolt.node
|
||||
|
||||
**revolt.node:** Node.js bindings for the Revolt software
|
||||
|
||||
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
|
||||
|
||||
## Building revolt.node
|
||||
|
||||
Building revolt.node requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
|
||||
|
||||
To run the build, run:
|
||||
|
||||
```sh
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`.
|
||||
|
||||
## Exploring revolt.node
|
||||
|
||||
After building revolt.node, you can explore its exports at the Node console:
|
||||
|
||||
```sh
|
||||
$ npm i
|
||||
$ npm run build
|
||||
$ node
|
||||
> require('.').hello()
|
||||
'hello node'
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
#### `npm install`
|
||||
|
||||
Installs the project, including running `npm run build`.
|
||||
|
||||
#### `npm run build`
|
||||
|
||||
Builds the Node addon (`index.node`) from source, generating a release build with `cargo --release`.
|
||||
|
||||
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm run build` and similar commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
|
||||
|
||||
```
|
||||
npm run build -- --feature=beetle
|
||||
```
|
||||
|
||||
#### `npm run debug`
|
||||
|
||||
Similar to `npm run build` but generates a debug build with `cargo`.
|
||||
|
||||
#### `npm run cross`
|
||||
|
||||
Similar to `npm run build` but uses [cross-rs](https://github.com/cross-rs/cross) to cross-compile for another platform. Use the [`CARGO_BUILD_TARGET`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget) environment variable to select the build target.
|
||||
|
||||
#### `npm test`
|
||||
|
||||
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
|
||||
|
||||
## Project Layout
|
||||
|
||||
The directory structure of this project is:
|
||||
|
||||
```
|
||||
revolt.node/
|
||||
├── Cargo.toml
|
||||
├── README.md
|
||||
├── src/
|
||||
| └── lib.rs
|
||||
├── index.node
|
||||
├── package.json
|
||||
└── target/
|
||||
```
|
||||
|
||||
| Entry | Purpose |
|
||||
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
|
||||
| `README.md` | This file. |
|
||||
| `src/` | The directory tree containing the Rust source code for the project. |
|
||||
| `lib.rs` | Entry point for the Rust source code. |
|
||||
| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. |
|
||||
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
|
||||
| `target/` | Binary artifacts generated by the Rust build. |
|
||||
|
||||
## Learn More
|
||||
|
||||
Learn more about:
|
||||
|
||||
- [Neon](https://neon-bindings.com).
|
||||
- [Rust](https://www.rust-lang.org).
|
||||
- [Node](https://nodejs.org).
|
||||
84
crates/bindings/node/index.d.ts
vendored
Normal file
84
crates/bindings/node/index.d.ts
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Channel, User } from "revolt-api";
|
||||
|
||||
/**
|
||||
* Opaque type for Revolt database
|
||||
*/
|
||||
export declare interface Database {}
|
||||
|
||||
/**
|
||||
* Opaque type for Revolt database
|
||||
*/
|
||||
export declare interface OpaqueUser {}
|
||||
|
||||
/**
|
||||
* Error type from Revolt backend
|
||||
*/
|
||||
export declare interface Err {
|
||||
type: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises background tasks and logging, must be called before anything else!
|
||||
* Can be called multiple times!
|
||||
*/
|
||||
export declare function init();
|
||||
|
||||
/**
|
||||
* Gets a new handle to the Revolt database
|
||||
* @returns {Database} Handle
|
||||
*/
|
||||
export declare function database(): Database;
|
||||
|
||||
/**
|
||||
* Fetch user from database
|
||||
* @param {string} userId User's ID
|
||||
* @this {Database}
|
||||
*/
|
||||
export declare function database_fetch_user(userId: string): OpaqueUser;
|
||||
|
||||
/**
|
||||
* Fetch user from database
|
||||
* @param {string} username Username
|
||||
* @param {string} discriminator Discriminator
|
||||
* @this {Database}
|
||||
*/
|
||||
export declare function database_fetch_user_by_username(
|
||||
username: string,
|
||||
discriminator: string
|
||||
): OpaqueUser;
|
||||
|
||||
/**
|
||||
* Gets model data as JSON
|
||||
* @this {OpaqueUser}
|
||||
*/
|
||||
export declare function model_data(): User;
|
||||
|
||||
/**
|
||||
* Gets error if the model failed to fetch
|
||||
* @this {OpaqueUser}
|
||||
*/
|
||||
export declare function model_error(): Err;
|
||||
|
||||
/**
|
||||
* Open a direct message channel between two users
|
||||
* @param {string} userA User A ID
|
||||
* @param {string} userB User B ID
|
||||
* @returns Existing or newly created channel
|
||||
*/
|
||||
export declare function proc_channels_create_dm(
|
||||
userA: string,
|
||||
userB: string
|
||||
): Promise<Channel & { error: Err }>;
|
||||
|
||||
/**
|
||||
* Suspend a user
|
||||
* @param {string} user User
|
||||
* @param {number} duration Duration (in days), set to 0 for indefinite
|
||||
* @param {string} reason Pipe-separated list of reasons (e.g. reason1|reason2|reason3)
|
||||
*/
|
||||
export declare function proc_users_suspend(
|
||||
user: OpaqueUser,
|
||||
duration: number,
|
||||
reason: string
|
||||
): Promise<{ error: Err }>;
|
||||
36
crates/bindings/node/package.json
Normal file
36
crates/bindings/node/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "revolt-nodejs-bindings",
|
||||
"version": "0.7.15-rev0.0.3",
|
||||
"description": "Node.js bindings for the Revolt software",
|
||||
"main": "index.node",
|
||||
"scripts": {
|
||||
"test": "cargo test",
|
||||
"cargo-build": "cargo build --message-format=json > cargo.log",
|
||||
"cross-build": "cross build --message-format=json > cross.log",
|
||||
"postcargo-build": "neon dist < cargo.log",
|
||||
"postcross-build": "neon dist -m /target < cross.log",
|
||||
"debug": "npm run cargo-build --",
|
||||
"build": "npm run cargo-build -- --release",
|
||||
"cross": "npm run cross-build -- --release"
|
||||
},
|
||||
"author": "Paul Makles",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@neon-rs/cli": "0.1.73"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/revoltchat/backend"
|
||||
},
|
||||
"keywords": [
|
||||
"revolt",
|
||||
"chat"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/revoltchat/backend/issues"
|
||||
},
|
||||
"homepage": "https://github.com/revoltchat/backend#readme",
|
||||
"dependencies": {
|
||||
"revolt-api": "^0.7.15"
|
||||
}
|
||||
}
|
||||
117
crates/bindings/node/pnpm-lock.yaml
generated
Normal file
117
crates/bindings/node/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,117 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
revolt-api:
|
||||
specifier: ^0.7.15
|
||||
version: 0.7.15
|
||||
|
||||
devDependencies:
|
||||
'@neon-rs/cli':
|
||||
specifier: 0.1.73
|
||||
version: 0.1.73
|
||||
|
||||
packages:
|
||||
|
||||
/@cargo-messages/android-arm-eabi@0.1.72:
|
||||
resolution: {integrity: sha512-gGZxIM1mj+Y5x+ULND6ZCNr7f70OJi9wDlycSK8hGONy9wrChN6JAIHryddC5cqcwlYAoQ6IDcDFElnhAYbybA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/darwin-arm64@0.1.72:
|
||||
resolution: {integrity: sha512-EAzN5MLaXPljZKZDO5qR+aBs44eSq2ZbEnS7AI/FziE3MzeXbrGOS3fLba5+7yWPFXJyZolXzePm8N1EBv8ovg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/darwin-x64@0.1.72:
|
||||
resolution: {integrity: sha512-RLo6j8s3nYbjdd1LDct4wamfChyRit7zokUuxtIYCu9XOlltkN5vnj1vwnrPvoqCMZ/7CbbuHFwSTn9A71de/w==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/linux-arm-gnueabihf@0.1.72:
|
||||
resolution: {integrity: sha512-tHsRshuzfjrX6SDW3jg6al8vMNLTMgczGnVYl5RuBZf/yrAUuwe30KxA9ge6w6mW6Ox797DyBchzAc9OLgTgmQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/linux-x64-gnu@0.1.72:
|
||||
resolution: {integrity: sha512-VGtL6CCnUbhsP4aYuBNT5kfrAL7o0qjrxw97a+ax13t+nJd26tVEEIKHMu5drvvS/Nm/hn7sLT8zMnnCv0pvHg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/win32-arm64-msvc@0.1.72:
|
||||
resolution: {integrity: sha512-V93Cgz39K+yqa3MveNbhh29pYCp8izK5uEavjPoxlNxAbsMCWH+s0verGDdUcfGxjR1H2V7oZ4FszPqR2SqMRQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/win32-x64-msvc@0.1.72:
|
||||
resolution: {integrity: sha512-knz3uSrO0OSbq3U5VWfCY8FB4NsM43BOWLZ7x4sfaMOC1XWv+IyvDdkLe6DhJx8KUw46KIAimYs9YROrp6l46Q==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@neon-rs/cli@0.1.73:
|
||||
resolution: {integrity: sha512-1kv8S/feB6UQWQQwsnGfkSkEBOtlFDNExnioL81E2BwvUWgjQPaseHgpi2EpWVgsPUgur5eBm4QowmlpWkD4/w==}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
'@cargo-messages/android-arm-eabi': 0.1.72
|
||||
'@cargo-messages/darwin-arm64': 0.1.72
|
||||
'@cargo-messages/darwin-x64': 0.1.72
|
||||
'@cargo-messages/linux-arm-gnueabihf': 0.1.72
|
||||
'@cargo-messages/linux-x64-gnu': 0.1.72
|
||||
'@cargo-messages/win32-arm64-msvc': 0.1.72
|
||||
'@cargo-messages/win32-x64-msvc': 0.1.72
|
||||
dev: true
|
||||
|
||||
/axios@0.26.1:
|
||||
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/follow-redirects@1.15.6:
|
||||
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dev: false
|
||||
|
||||
/lodash.defaultsdeep@4.6.1:
|
||||
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
|
||||
dev: false
|
||||
|
||||
/revolt-api@0.7.15:
|
||||
resolution: {integrity: sha512-rWRnjBFvoXt/RHRxHg9KuP+x1Jk61KdsOpKOczVJg5tIqGDmqp/6QZHw5rJTtG+wCH3Ah3YKwY+MM5NHfg1W3Q==}
|
||||
dependencies:
|
||||
axios: 0.26.1
|
||||
lodash.defaultsdeep: 4.6.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
221
crates/bindings/node/src/lib.rs
Normal file
221
crates/bindings/node/src/lib.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use neon::prelude::*;
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
fn js_init(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
||||
static INIT: OnceLock<()> = OnceLock::new();
|
||||
if INIT.get().is_none() {
|
||||
INIT.get_or_init(|| {
|
||||
async_std::task::block_on(async {
|
||||
revolt_config::configure!(api);
|
||||
|
||||
match DatabaseInfo::Auto.connect().await {
|
||||
Ok(db) => {
|
||||
let authifier_db = db.clone().to_authifier().await.database;
|
||||
revolt_database::tasks::start_workers(db, authifier_db);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
})
|
||||
.or_else(|err| cx.throw_error(err))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(cx.undefined())
|
||||
}
|
||||
|
||||
struct DatabaseBinding(Database, Channel);
|
||||
impl Finalize for DatabaseBinding {}
|
||||
impl DatabaseBinding {
|
||||
fn take(&self) -> (Database, Channel) {
|
||||
(self.0.clone(), self.1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn js_database(mut cx: FunctionContext) -> JsResult<JsBox<DatabaseBinding>> {
|
||||
let db = async_std::task::block_on(DatabaseInfo::Auto.connect())
|
||||
.or_else(|err| cx.throw_error(err))?;
|
||||
|
||||
let channel = cx.channel();
|
||||
Ok(cx.boxed(DatabaseBinding(db, channel)))
|
||||
}
|
||||
|
||||
// Implementations for models
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Model {
|
||||
User(revolt_database::User),
|
||||
Error(revolt_result::Error),
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn give(&self) -> Model {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Finalize for Model {}
|
||||
|
||||
macro_rules! shim_boxed {
|
||||
($cx: ident, $name: ident, $model: ident, $( $variable: ident $type: ident $id: expr )+, $cmd: ident, $( $arg: expr, )+) => {
|
||||
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||
$(
|
||||
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||
)+
|
||||
|
||||
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||
let (deferred, promise) = cx.promise();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
let result = db.$cmd($($arg,)+).await;
|
||||
deferred.settle_with(&channel, move |mut cx| {
|
||||
Ok(cx.boxed(match result {
|
||||
Ok(value) => Model::$model(value),
|
||||
Err(error) => Model::Error(error)
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
$cx.export_function(stringify!($name), $name)?;
|
||||
};
|
||||
}
|
||||
|
||||
fn js_data(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
match cx.this::<JsBox<Model>>()?.give() {
|
||||
Model::Error(_) => neon_serde4::to_value(&mut cx, &None::<()>),
|
||||
Model::User(user) => neon_serde4::to_value(&mut cx, &user),
|
||||
}
|
||||
.or_else(|e| cx.throw_error(e.to_string()))
|
||||
}
|
||||
|
||||
fn js_error(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
let value = match cx.this::<JsBox<Model>>()?.give() {
|
||||
Model::Error(err) => Some(err),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
neon_serde4::to_value(&mut cx, &value).or_else(|e| cx.throw_error(e.to_string()))
|
||||
}
|
||||
|
||||
// Basic data implementation
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ResultBinding<T> {
|
||||
#[serde(flatten)]
|
||||
value: Option<T>,
|
||||
error: Option<revolt_result::Error>,
|
||||
}
|
||||
|
||||
macro_rules! shim {
|
||||
($cx: ident, $name: ident, $( $variable: ident $type: ident $id: expr )*, $( $model: ident $modelType: ident $modelId: expr )*, | $db: ident | $closure: expr, $( $arg: expr, )+) => {
|
||||
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||
$(
|
||||
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||
)*
|
||||
|
||||
$(
|
||||
let mut $model = if let Model::$modelType(value) = cx.argument::<JsBox<Model>>($modelId)?.give() {
|
||||
value
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
)*
|
||||
|
||||
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||
let (deferred, promise) = cx.promise();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
let result = (|$db: $crate::Database| $closure)(db.clone()).await;
|
||||
deferred.settle_with(&channel, move |mut cx| {
|
||||
neon_serde4::to_value(
|
||||
&mut cx,
|
||||
&match result {
|
||||
Ok(value) => ResultBinding {
|
||||
value: Some(value),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => ResultBinding {
|
||||
value: None,
|
||||
error: Some(error),
|
||||
},
|
||||
},
|
||||
)
|
||||
.or_else(|e| cx.throw_error(e.to_string()))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
$cx.export_function(stringify!($name), $name)?;
|
||||
};
|
||||
}
|
||||
|
||||
#[neon::main]
|
||||
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||
// initialise required background stuff
|
||||
cx.export_function("init", js_init)?;
|
||||
|
||||
// database & model stuff
|
||||
cx.export_function("database", js_database)?;
|
||||
cx.export_function("model_data", js_data)?;
|
||||
cx.export_function("model_error", js_error)?;
|
||||
|
||||
shim_boxed!(
|
||||
cx,
|
||||
database_fetch_user,
|
||||
User,
|
||||
user_id JsString 0,
|
||||
fetch_user,
|
||||
&user_id,
|
||||
);
|
||||
|
||||
shim_boxed!(
|
||||
cx,
|
||||
database_fetch_user_by_username,
|
||||
User,
|
||||
username JsString 0
|
||||
discriminator JsString 1,
|
||||
fetch_user_by_username,
|
||||
&username, &discriminator,
|
||||
);
|
||||
|
||||
// procedure calls
|
||||
shim!(
|
||||
cx,
|
||||
proc_channels_create_dm,
|
||||
user_a JsString 0
|
||||
user_b JsString 1,
|
||||
,
|
||||
|db| async move {
|
||||
let user_a = db.fetch_user(&user_a).await?;
|
||||
let user_b = db.fetch_user(&user_b).await?;
|
||||
revolt_database::Channel::create_dm(&db, &user_a, &user_b).await
|
||||
},
|
||||
&userA, &userB,
|
||||
);
|
||||
|
||||
shim!(
|
||||
cx,
|
||||
proc_users_suspend,
|
||||
duration JsNumber 1
|
||||
reason JsString 2,
|
||||
user User 0,
|
||||
|db| async move {
|
||||
let duration = duration as usize;
|
||||
user.suspend(&db, if duration == 0 { None } else { Some(duration) }, Some(reason.split('|').map(|x| x.to_owned()).collect())).await
|
||||
},
|
||||
&user,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
53
crates/bindings/node/test.js
Normal file
53
crates/bindings/node/test.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const Internal = require(".");
|
||||
|
||||
// playing around with class wrapper, not practical
|
||||
class Model {
|
||||
constructor(model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
data() {
|
||||
return Internal.model_data.bind(this.model)();
|
||||
}
|
||||
|
||||
error() {
|
||||
return Internal.model_error.bind(this.model)();
|
||||
}
|
||||
}
|
||||
|
||||
class User extends Model {
|
||||
constructor(db, user) {
|
||||
super(user);
|
||||
this.db = db;
|
||||
}
|
||||
}
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.db = Internal.database();
|
||||
}
|
||||
|
||||
async fetchUser(userId) {
|
||||
return new User(
|
||||
this,
|
||||
await Internal.database_fetch_user.bind(this.db)(userId)
|
||||
);
|
||||
}
|
||||
|
||||
async fetchUserByUsername(username, discriminator) {
|
||||
return new User(
|
||||
this,
|
||||
await Internal.database_fetch_user_by_username.bind(this.db)(
|
||||
username,
|
||||
discriminator
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const db = new Database();
|
||||
db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data()));
|
||||
db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data()));
|
||||
db.fetchUserByUsername("dos", "1111").then((user) =>
|
||||
console.info(user.error())
|
||||
);
|
||||
6
crates/bindings/package-lock.json
generated
Normal file
6
crates/bindings/package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "bindings",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
1
crates/bonfire/.gitignore
vendored
Normal file
1
crates/bonfire/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
48
crates/bonfire/Cargo.toml
Normal file
48
crates/bonfire/Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.7.18"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
# 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"
|
||||
|
||||
# serde
|
||||
bincode = "1.3.3"
|
||||
serde_json = "1.0.79"
|
||||
rmp-serde = "1.0.0"
|
||||
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",
|
||||
] }
|
||||
|
||||
# core
|
||||
authifier = { version = "1.0.8" }
|
||||
revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database" }
|
||||
revolt-permissions = { version = "0.7.18", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
10
crates/bonfire/Dockerfile
Normal file
10
crates/bonfire/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
# Build Stage
|
||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||
|
||||
# Bundle Stage
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
||||
|
||||
EXPOSE 14703
|
||||
USER nonroot
|
||||
CMD ["./revolt-bonfire"]
|
||||
1
crates/bonfire/LICENSE
Symbolic link
1
crates/bonfire/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE
|
||||
162
crates/bonfire/src/config.rs
Normal file
162
crates/bonfire/src/config.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_database::events::client::ReadyPayloadFields;
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Enumeration of supported protocol formats
|
||||
#[derive(Debug)]
|
||||
pub enum ProtocolFormat {
|
||||
Json,
|
||||
Msgpack,
|
||||
}
|
||||
|
||||
/// User-provided protocol configuration
|
||||
#[derive(Debug)]
|
||||
pub struct ProtocolConfiguration {
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
}
|
||||
|
||||
impl ProtocolConfiguration {
|
||||
/// Create a new protocol configuration object from provided data
|
||||
pub fn from(
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode some WebSocket message into a T: Deserialize using the client's specified protocol format
|
||||
pub fn decode<'a, T: Deserialize<'a>>(&self, msg: &'a Message) -> Result<T> {
|
||||
match self.format {
|
||||
ProtocolFormat::Json => {
|
||||
if let Message::Text(text) = msg {
|
||||
serde_json::from_str(text).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
ProtocolFormat::Msgpack => {
|
||||
if let Message::Binary(buf) = msg {
|
||||
rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode T: Serialize into a WebSocket message using the client's specified protocol format
|
||||
pub fn encode<T: Serialize>(&self, data: &T) -> Message {
|
||||
match self.format {
|
||||
ProtocolFormat::Json => {
|
||||
Message::Text(serde_json::to_string(data).expect("Failed to serialise (as json)."))
|
||||
}
|
||||
ProtocolFormat::Msgpack => Message::Binary(
|
||||
rmp_serde::to_vec_named(data).expect("Failed to serialise (as msgpack)."),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current session token
|
||||
pub fn set_session_token(&mut self, token: String) {
|
||||
self.session_token.replace(token);
|
||||
}
|
||||
|
||||
/// Get the current session token
|
||||
pub fn get_session_token(&self) -> &Option<String> {
|
||||
&self.session_token
|
||||
}
|
||||
|
||||
/// Get the protocol version specified
|
||||
pub fn get_protocol_version(&self) -> i32 {
|
||||
self.protocol_version
|
||||
}
|
||||
|
||||
/// Get the protocol format specified
|
||||
pub fn get_protocol_format(&self) -> &ProtocolFormat {
|
||||
&self.format
|
||||
}
|
||||
|
||||
/// Get ready payload fields
|
||||
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
|
||||
vec![
|
||||
ReadyPayloadFields::Users,
|
||||
ReadyPayloadFields::Servers,
|
||||
ReadyPayloadFields::Channels,
|
||||
ReadyPayloadFields::Members,
|
||||
ReadyPayloadFields::Emoji,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Object holding one side of a channel for receiving the parsed information
|
||||
pub struct WebsocketHandshakeCallback {
|
||||
sender: Sender<ProtocolConfiguration>,
|
||||
}
|
||||
|
||||
impl WebsocketHandshakeCallback {
|
||||
/// Create a callback using a given sender
|
||||
pub fn from(sender: Sender<ProtocolConfiguration>) -> Self {
|
||||
Self { sender }
|
||||
}
|
||||
}
|
||||
|
||||
impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
/// Handle request to create a new WebSocket connection
|
||||
fn on_request(
|
||||
self,
|
||||
request: &handshake::server::Request,
|
||||
response: handshake::server::Response,
|
||||
) -> Result<handshake::server::Response, handshake::server::ErrorResponse> {
|
||||
// Take and parse query parameters from the URI.
|
||||
let query = request.uri().query().unwrap_or_default();
|
||||
let params = querystring::querify(query);
|
||||
|
||||
// Set default values for the protocol.
|
||||
let mut protocol_version = 1;
|
||||
let mut format = ProtocolFormat::Json;
|
||||
let mut session_token = None;
|
||||
|
||||
// Parse and map parameters from key-value to known variables.
|
||||
for (key, value) in params {
|
||||
match key {
|
||||
"version" => {
|
||||
if let Ok(version) = value.parse() {
|
||||
protocol_version = version;
|
||||
}
|
||||
}
|
||||
"format" => match value {
|
||||
"json" => format = ProtocolFormat::Json,
|
||||
"msgpack" => format = ProtocolFormat::Msgpack,
|
||||
_ => {}
|
||||
},
|
||||
"token" => session_token = Some(value.into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Send configuration information back from this callback.
|
||||
// We have to use a channel as this function does not borrow mutably.
|
||||
if self
|
||||
.sender
|
||||
.send(ProtocolConfiguration {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(handshake::server::ErrorResponse::new(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
21
crates/bonfire/src/database.rs
Normal file
21
crates/bonfire/src/database.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
static DBCONN: OnceCell<Database> = OnceCell::new();
|
||||
|
||||
/// Connect Bonfire to the database.
|
||||
pub async fn connect() {
|
||||
let database = DatabaseInfo::Auto
|
||||
.connect()
|
||||
.await
|
||||
.expect("Failed to connect to the database.");
|
||||
|
||||
if DBCONN.set(database).is_err() {
|
||||
panic!("couldn't set database")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the current database.
|
||||
pub fn get_db() -> &'static Database {
|
||||
DBCONN.get().expect("Valid `Database`")
|
||||
}
|
||||
621
crates/bonfire/src/events/impl.rs
Normal file
621
crates/bonfire/src/events/impl.rs
Normal file
@@ -0,0 +1,621 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_database::{
|
||||
events::client::{EventV1, ReadyPayloadFields},
|
||||
util::permissions::DatabasePermissionQuery,
|
||||
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::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 {
|
||||
match &channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
let member = self.members.get(server);
|
||||
let server = self.servers.get(server);
|
||||
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 {
|
||||
query = query.member(member);
|
||||
}
|
||||
|
||||
if let Some(server) = server {
|
||||
query = query.server(server);
|
||||
}
|
||||
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::ViewChannel)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter a given vector of channels to only include the ones we can access
|
||||
pub async fn filter_accessible_channels(
|
||||
&self,
|
||||
db: &Database,
|
||||
channels: Vec<Channel>,
|
||||
) -> Vec<Channel> {
|
||||
let mut viewable_channels = vec![];
|
||||
for channel in channels {
|
||||
if self.can_view_channel(db, &channel).await {
|
||||
viewable_channels.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
viewable_channels
|
||||
}
|
||||
|
||||
/// 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 user.relationship_with(user_id) {
|
||||
RelationshipStatus::Friend
|
||||
| RelationshipStatus::Incoming
|
||||
| RelationshipStatus::Outgoing
|
||||
| RelationshipStatus::User => true,
|
||||
_ => {
|
||||
let user_id = &user_id.to_string();
|
||||
for channel in self.channels.values() {
|
||||
match channel {
|
||||
Channel::DirectMessage { recipients, .. }
|
||||
| Channel::Group { recipients, .. } => {
|
||||
if recipients.contains(user_id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// State Manager
|
||||
impl State {
|
||||
/// Generate a Ready packet for the current user
|
||||
pub async fn generate_ready_payload(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
fields: Vec<ReadyPayloadFields>,
|
||||
) -> Result<EventV1> {
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
.relations
|
||||
.as_ref()
|
||||
.map(|arr| arr.iter().map(|x| x.id.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Fetch all memberships with their corresponding servers.
|
||||
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
|
||||
self.cache.members = members
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id.server.clone(), x))
|
||||
.collect();
|
||||
|
||||
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![];
|
||||
for server in &servers {
|
||||
channel_ids.append(&mut server.channels.clone());
|
||||
}
|
||||
|
||||
// Fetch DMs and server channels.
|
||||
let mut channels = db.find_direct_messages(&user.id).await?;
|
||||
channels.append(&mut db.fetch_channels(&channel_ids).await?);
|
||||
|
||||
// Filter server channels by permission.
|
||||
let channels = self.cache.filter_accessible_channels(db, channels).await;
|
||||
|
||||
// Append known user IDs from DMs.
|
||||
for channel in &channels {
|
||||
match channel {
|
||||
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
|
||||
user_ids.extend(&mut recipients.clone().into_iter());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch presence data for known users.
|
||||
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
|
||||
|
||||
// Fetch user data.
|
||||
let users = db
|
||||
.fetch_users(
|
||||
&user_ids
|
||||
.into_iter()
|
||||
.filter(|x| x != &user.id)
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
|
||||
Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch user settings
|
||||
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
|
||||
.iter()
|
||||
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
|
||||
{
|
||||
Some(db.fetch_user_settings(&user.id, &keys).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch channel unreads
|
||||
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
|
||||
Some(db.fetch_unreads(&user.id).await?)
|
||||
} 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> = users
|
||||
.into_iter()
|
||||
.map(|other_user| {
|
||||
let is_online = online_ids.contains(&other_user.id);
|
||||
other_user.into_known(&user, is_online)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Make sure we see our own user correctly.
|
||||
users.push(user.into_self(true).await);
|
||||
|
||||
// Set subscription state internally.
|
||||
self.reset_state().await;
|
||||
self.insert_subscription(self.private_topic.clone()).await;
|
||||
|
||||
for user in &users {
|
||||
self.insert_subscription(user.id.clone()).await;
|
||||
}
|
||||
|
||||
for server in &servers {
|
||||
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()).await;
|
||||
}
|
||||
Ok(EventV1::Ready {
|
||||
users: if fields.contains(&ReadyPayloadFields::Users) {
|
||||
Some(users)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
servers: if fields.contains(&ReadyPayloadFields::Servers) {
|
||||
Some(servers.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
channels: if fields.contains(&ReadyPayloadFields::Channels) {
|
||||
Some(channels.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
members: if fields.contains(&ReadyPayloadFields::Members) {
|
||||
Some(members.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
|
||||
user_settings,
|
||||
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Re-determine the currently accessible server channels
|
||||
pub async fn recalculate_server(&mut self, db: &Database, id: &str, event: &mut EventV1) {
|
||||
if let Some(server) = self.cache.servers.get(id) {
|
||||
let mut channel_ids = HashSet::new();
|
||||
let mut added_channels = vec![];
|
||||
let mut removed_channels = vec![];
|
||||
|
||||
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 self.cache.can_view_channel(db, channel).await {
|
||||
added_channels.push(channel_id.clone());
|
||||
} else {
|
||||
removed_channels.push(channel_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let known_ids = server.channels.iter().cloned().collect::<HashSet<String>>();
|
||||
|
||||
let mut bulk_events = vec![];
|
||||
|
||||
for id in added_channels {
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
for id in removed_channels {
|
||||
self.remove_subscription(&id).await;
|
||||
self.cache.channels.remove(&id);
|
||||
|
||||
bulk_events.push(EventV1::ChannelDelete { id });
|
||||
}
|
||||
|
||||
// * NOTE: currently all channels should be cached
|
||||
// * provided that a server was loaded from payload
|
||||
let unknowns = known_ids
|
||||
.difference(&channel_ids)
|
||||
.cloned()
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
if !unknowns.is_empty() {
|
||||
if let Ok(channels) = db.fetch_channels(&unknowns).await {
|
||||
let viewable_channels =
|
||||
self.cache.filter_accessible_channels(db, channels).await;
|
||||
|
||||
for channel in viewable_channels {
|
||||
self.cache
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
bulk_events.push(EventV1::ChannelCreate(channel.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !bulk_events.is_empty() {
|
||||
let mut new_event = EventV1::Bulk { v: bulk_events };
|
||||
std::mem::swap(&mut new_event, event);
|
||||
|
||||
if let EventV1::Bulk { v } = event {
|
||||
v.push(new_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push presence change to the user and all associated server topics
|
||||
pub async fn broadcast_presence_change(&self, target: bool) {
|
||||
if if let Some(status) = &self.cache.users.get(&self.cache.user_id).unwrap().status {
|
||||
status.presence != Some(Presence::Invisible)
|
||||
} else {
|
||||
true
|
||||
} {
|
||||
let event = EventV1::UserUpdate {
|
||||
id: self.cache.user_id.clone(),
|
||||
data: v0::PartialUser {
|
||||
online: Some(target),
|
||||
..Default::default()
|
||||
},
|
||||
clear: vec![],
|
||||
event_id: Some(ulid::Ulid::new().to_string()),
|
||||
};
|
||||
|
||||
for server in self.cache.servers.keys() {
|
||||
event.clone().p(server.clone()).await;
|
||||
}
|
||||
|
||||
event.p(self.cache.user_id.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming event for protocol version 1
|
||||
pub async fn handle_incoming_event_v1(&mut self, db: &Database, event: &mut EventV1) -> bool {
|
||||
/* Superseded by private topics.
|
||||
if match event {
|
||||
EventV1::UserRelationship { id, .. }
|
||||
| EventV1::UserSettingsUpdate { id, .. }
|
||||
| EventV1::ChannelAck { id, .. } => id != &self.cache.user_id,
|
||||
EventV1::ServerCreate { server, .. } => server.owner != self.cache.user_id,
|
||||
EventV1::ChannelCreate(channel) => match channel {
|
||||
Channel::SavedMessages { user, .. } => user != &self.cache.user_id,
|
||||
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
|
||||
!recipients.contains(&self.cache.user_id)
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
} {
|
||||
return false;
|
||||
}*/
|
||||
|
||||
// An event may trigger recalculation of an entire server's permission.
|
||||
// Keep track of whether we need to do anything.
|
||||
let mut queue_server = None;
|
||||
|
||||
// It may also need to sub or unsub a single value.
|
||||
let mut queue_add = None;
|
||||
let mut queue_remove = None;
|
||||
|
||||
match event {
|
||||
EventV1::ChannelCreate(channel) => {
|
||||
let id = channel.id().to_string();
|
||||
self.insert_subscription(id.clone()).await;
|
||||
self.cache.channels.insert(id, channel.clone().into());
|
||||
}
|
||||
EventV1::ChannelUpdate {
|
||||
id, data, clear, ..
|
||||
} => {
|
||||
let could_view: bool = if let Some(channel) = self.cache.channels.get(id) {
|
||||
self.cache.can_view_channel(db, channel).await
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(channel) = self.cache.channels.get_mut(id) {
|
||||
for field in clear {
|
||||
channel.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
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) {
|
||||
let can_view = self.cache.can_view_channel(db, channel).await;
|
||||
if could_view != can_view {
|
||||
if can_view {
|
||||
queue_add = Some(id.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone().into());
|
||||
} else {
|
||||
queue_remove = Some(id.clone());
|
||||
*event = EventV1::ChannelDelete { id: id.clone() };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventV1::ChannelDelete { id } => {
|
||||
self.remove_subscription(id).await;
|
||||
self.cache.channels.remove(id);
|
||||
}
|
||||
EventV1::ChannelGroupJoin { user, .. } => {
|
||||
self.insert_subscription(user.clone()).await;
|
||||
}
|
||||
EventV1::ChannelGroupLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id).await;
|
||||
} else if !self.cache.can_subscribe_to_user(user) {
|
||||
self.remove_subscription(user).await;
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::ServerCreate {
|
||||
id,
|
||||
server,
|
||||
channels,
|
||||
emojis: _,
|
||||
} => {
|
||||
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().into());
|
||||
}
|
||||
|
||||
queue_server = Some(id.clone());
|
||||
}
|
||||
EventV1::ServerUpdate {
|
||||
id, data, clear, ..
|
||||
} => {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
for field in clear {
|
||||
server.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
server.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.default_permissions.is_some() {
|
||||
queue_server = Some(id.clone());
|
||||
}
|
||||
}
|
||||
EventV1::ServerMemberJoin { .. } => {
|
||||
// We will always receive ServerCreate when joining a new server.
|
||||
}
|
||||
EventV1::ServerMemberLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
self.cache.members.remove(id);
|
||||
}
|
||||
}
|
||||
EventV1::ServerDelete { id } => {
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
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(&field.clone().into());
|
||||
}
|
||||
|
||||
member.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.roles.is_some() || clear.contains(&v0::FieldsMember::Roles) {
|
||||
queue_server = Some(id.server.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
EventV1::ServerRoleUpdate {
|
||||
id,
|
||||
role_id,
|
||||
data,
|
||||
clear,
|
||||
..
|
||||
} => {
|
||||
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(&field.clone().into());
|
||||
}
|
||||
|
||||
role.apply_options(data.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
if data.rank.is_some() || data.permissions.is_some() {
|
||||
if let Some(member) = self.cache.members.get(id) {
|
||||
if member.roles.contains(role_id) {
|
||||
queue_server = Some(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EventV1::ServerRoleDelete { id, role_id } => {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
server.roles.remove(role_id);
|
||||
}
|
||||
|
||||
if let Some(member) = self.cache.members.get(id) {
|
||||
if member.roles.contains(role_id) {
|
||||
queue_server = Some(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::UserUpdate { event_id, .. } => {
|
||||
if let Some(id) = event_id {
|
||||
if self.cache.seen_events.contains(id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.cache.seen_events.put(id.to_string(), ());
|
||||
}
|
||||
|
||||
*event_id = None;
|
||||
}
|
||||
EventV1::UserRelationship { id, user, .. } => {
|
||||
self.cache.users.insert(id.clone(), user.clone().into());
|
||||
|
||||
if self.cache.can_subscribe_to_user(id) {
|
||||
self.insert_subscription(id.clone()).await;
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Calculate server permissions if requested.
|
||||
if let Some(server_id) = queue_server {
|
||||
self.recalculate_server(db, &server_id, event).await;
|
||||
}
|
||||
|
||||
// Sub / unsub accordingly.
|
||||
if let Some(id) = queue_add {
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
if let Some(id) = queue_remove {
|
||||
self.remove_subscription(&id).await;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
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;
|
||||
211
crates/bonfire/src/events/state.rs
Normal file
211
crates/bonfire/src/events/state.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use lru::LruCache;
|
||||
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
pub enum SubscriptionStateChange {
|
||||
/// No change
|
||||
None,
|
||||
/// Clear all subscriptions
|
||||
Reset,
|
||||
/// Append or remove subscriptions
|
||||
Change {
|
||||
add: Vec<String>,
|
||||
remove: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Dumb per-state cache implementation
|
||||
///
|
||||
/// Ideally this would use a global cache that
|
||||
/// allows for mutations and could use Rc<> to
|
||||
/// track usage. If Rc<> == 1, then it only
|
||||
/// remains in global cache, hence should be
|
||||
/// dropped.
|
||||
///
|
||||
/// ------------------------------------------------
|
||||
/// We can strip these objects to core information!!
|
||||
/// ------------------------------------------------
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub user_id: String,
|
||||
pub is_bot: bool,
|
||||
|
||||
pub users: HashMap<String, User>,
|
||||
pub channels: HashMap<String, Channel>,
|
||||
pub members: HashMap<String, Member>,
|
||||
pub servers: HashMap<String, Server>,
|
||||
|
||||
pub seen_events: LruCache<String, ()>,
|
||||
}
|
||||
|
||||
impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Cache {
|
||||
user_id: Default::default(),
|
||||
is_bot: false,
|
||||
|
||||
users: Default::default(),
|
||||
channels: Default::default(),
|
||||
members: Default::default(),
|
||||
servers: Default::default(),
|
||||
|
||||
seen_events: LruCache::new(20),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Client state
|
||||
pub struct State {
|
||||
pub cache: Cache,
|
||||
|
||||
pub session_id: String,
|
||||
pub private_topic: String,
|
||||
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, session_id: String) -> State {
|
||||
let mut subscribed = HashSet::new();
|
||||
let private_topic = format!("{}!", user.id);
|
||||
subscribed.insert(private_topic.clone());
|
||||
subscribed.insert(user.id.clone());
|
||||
|
||||
let mut cache: Cache = Cache {
|
||||
user_id: user.id.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
cache.users.insert(user.id.clone(), user);
|
||||
|
||||
State {
|
||||
cache,
|
||||
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 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 {
|
||||
subscribed.insert(id.clone());
|
||||
}
|
||||
|
||||
for id in remove {
|
||||
subscribed.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// Clone the active user
|
||||
pub fn clone_user(&self) -> User {
|
||||
self.cache.users.get(&self.cache.user_id).unwrap().clone()
|
||||
}
|
||||
|
||||
/// Reset the current state
|
||||
pub async fn reset_state(&mut self) {
|
||||
self.state = SubscriptionStateChange::Reset;
|
||||
self.subscribed.write().await.clear();
|
||||
}
|
||||
|
||||
/// Add a new subscription
|
||||
pub async fn insert_subscription(&mut self, subscription: String) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if subscribed.contains(&subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
match &mut self.state {
|
||||
SubscriptionStateChange::None => {
|
||||
self.state = SubscriptionStateChange::Change {
|
||||
add: vec![subscription.clone()],
|
||||
remove: vec![],
|
||||
};
|
||||
}
|
||||
SubscriptionStateChange::Change { add, .. } => {
|
||||
add.push(subscription.clone());
|
||||
}
|
||||
SubscriptionStateChange::Reset => {}
|
||||
}
|
||||
|
||||
subscribed.insert(subscription);
|
||||
}
|
||||
|
||||
/// Remove existing subscription
|
||||
pub async fn remove_subscription(&mut self, subscription: &str) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if !subscribed.contains(&subscription.to_string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
match &mut self.state {
|
||||
SubscriptionStateChange::None => {
|
||||
self.state = SubscriptionStateChange::Change {
|
||||
add: vec![],
|
||||
remove: vec![subscription.to_string()],
|
||||
};
|
||||
}
|
||||
SubscriptionStateChange::Change { remove, .. } => {
|
||||
remove.push(subscription.to_string());
|
||||
}
|
||||
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
|
||||
}
|
||||
|
||||
subscribed.remove(subscription);
|
||||
}
|
||||
}
|
||||
39
crates/bonfire/src/main.rs
Normal file
39
crates/bonfire/src/main.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::env;
|
||||
|
||||
use async_std::net::TcpListener;
|
||||
use revolt_presence::clear_region;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
|
||||
mod database;
|
||||
mod websocket;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
// Configure requirements for Bonfire.
|
||||
revolt_config::configure!(events);
|
||||
database::connect().await;
|
||||
|
||||
// Clean up the current region information.
|
||||
clear_region(None).await;
|
||||
|
||||
// Setup a TCP listener to accept WebSocket connections on.
|
||||
// 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 {
|
||||
async_std::task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
websocket::client(database::get_db(), stream, addr).await;
|
||||
info!("User disconnected from {addr:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
501
crates/bonfire/src/websocket.rs
Normal file
501
crates/bonfire/src/websocket.rs
Normal file
@@ -0,0 +1,501 @@
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::{RedisError, 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},
|
||||
Database, User, UserHint,
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
|
||||
use async_std::{
|
||||
net::TcpStream,
|
||||
sync::{Mutex, RwLock},
|
||||
task::spawn,
|
||||
};
|
||||
use revolt_result::create_error;
|
||||
use sentry::Level;
|
||||
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
use crate::events::state::{State, SubscriptionStateChange};
|
||||
|
||||
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 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;
|
||||
};
|
||||
|
||||
// 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(&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(&err)).await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user, session_id);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// 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 redis_config = RedisConfig::from_url(&REDIS_URI).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;
|
||||
}
|
||||
}
|
||||
|
||||
#[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}");
|
||||
|
||||
if report_internal_error!(subscriber.unsubscribe(id).await).is_err() {
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
}
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
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| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
|
||||
PayloadType::Msgpack => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
|
||||
PayloadType::Bincode => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
|
||||
};
|
||||
|
||||
let Some(mut event) = event else {
|
||||
let err = format!(
|
||||
"Failed to deserialise an event for {}! Introspection: `{:?}`",
|
||||
message.channel,
|
||||
message
|
||||
.value
|
||||
.as_string()
|
||||
.map(|x| x.chars().take(32).collect::<String>())
|
||||
);
|
||||
|
||||
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>,
|
||||
) {
|
||||
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 !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
crates/core/config/Cargo.toml
Normal file
38
crates/core/config/Cargo.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.7.18"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Configuration"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
report-macros = ["revolt-result"]
|
||||
test = ["async-std"]
|
||||
default = ["test"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
dotenv = "0.15.0"
|
||||
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 = "0.31.5"
|
||||
|
||||
# Core
|
||||
revolt-result = { version = "0.7.18", 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.
|
||||
3
crates/core/config/Revolt.test.toml
Normal file
3
crates/core/config/Revolt.test.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost/"
|
||||
232
crates/core/config/Revolt.toml
Normal file
232
crates/core/config/Revolt.toml
Normal file
@@ -0,0 +1,232 @@
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://database"
|
||||
# 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/revoltchat/self-hosted/blob/master/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 = ""
|
||||
|
||||
[api]
|
||||
|
||||
[api.registration]
|
||||
# Whether an invite should be required for registration
|
||||
# See https://github.com/revoltchat/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.vapid]
|
||||
# Generate your own keys:
|
||||
# 1. Run `openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem`
|
||||
# 2. Find `private_key` using `base64 vapid_private.pem`
|
||||
# 3. Find `public_key` using `openssl ec -in vapid_private.pem -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n'`
|
||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
||||
|
||||
[api.fcm]
|
||||
# Google Firebase Cloud Messaging Service Account Key
|
||||
# Obtained from the cloud messaging console
|
||||
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 = ""
|
||||
|
||||
[api.apn]
|
||||
# Apple Push Notifications keys for sending notifications
|
||||
sandbox = false
|
||||
pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
[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
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
hcaptcha_key = ""
|
||||
hcaptcha_sitekey = ""
|
||||
|
||||
[api.workers]
|
||||
# Maximum concurrent connections (to proxy server)
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[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
|
||||
# - 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"
|
||||
# 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"
|
||||
|
||||
[features]
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[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
|
||||
|
||||
[sentry]
|
||||
# Configuration for Sentry error reporting
|
||||
api = ""
|
||||
events = ""
|
||||
files = ""
|
||||
proxy = ""
|
||||
342
crates/core/config/src/lib.rs
Normal file
342
crates/core/config/src/lib.rs
Normal file
@@ -0,0 +1,342 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
use config::{Config, File, FileFormat};
|
||||
use futures_locks::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub use sentry::{capture_error, capture_message, Level};
|
||||
|
||||
#[cfg(feature = "report-macros")]
|
||||
#[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(feature = "report-macros")]
|
||||
#[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",
|
||||
];
|
||||
|
||||
/// 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,
|
||||
));
|
||||
}
|
||||
|
||||
for path in CONFIG_SEARCH_PATHS {
|
||||
if std::path::Path::new(path).exists() {
|
||||
builder = builder.add_source(File::new(path, FileFormat::Toml));
|
||||
}
|
||||
}
|
||||
|
||||
builder.build().unwrap()
|
||||
})
|
||||
});
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Database {
|
||||
pub mongodb: String,
|
||||
pub redis: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Hosts {
|
||||
pub app: String,
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
pub autumn: String,
|
||||
pub january: String,
|
||||
pub voso_legacy: String,
|
||||
pub voso_legacy_ws: 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>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiVapid {
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiFcm {
|
||||
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 ApiApn {
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiWorkers {
|
||||
pub max_concurrent_connections: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Api {
|
||||
pub registration: ApiRegistration,
|
||||
pub smtp: ApiSmtp,
|
||||
pub vapid: ApiVapid,
|
||||
pub fcm: ApiFcm,
|
||||
pub apn: ApiApn,
|
||||
pub security: ApiSecurity,
|
||||
pub workers: ApiWorkers,
|
||||
}
|
||||
|
||||
#[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 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,
|
||||
}
|
||||
|
||||
#[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 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 Features {
|
||||
pub limits: FeaturesLimitsCollection,
|
||||
pub webhooks_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Sentry {
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
pub files: String,
|
||||
pub proxy: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Settings {
|
||||
pub database: Database,
|
||||
pub hosts: Hosts,
|
||||
pub api: Api,
|
||||
pub files: Files,
|
||||
pub features: Features,
|
||||
pub sentry: Sentry,
|
||||
}
|
||||
|
||||
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 {
|
||||
read().await.try_deserialize::<Settings>().unwrap()
|
||||
}
|
||||
|
||||
/// Configure logging and common Rust variables
|
||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if std::env::var("REDIS_URL").is_err() {
|
||||
// Configure redis-kiss library
|
||||
let config = config().await;
|
||||
std::env::set_var("REDIS_URI", config.database.redis);
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
}
|
||||
99
crates/core/database/Cargo.toml
Normal file
99
crates/core/database/Cargo.toml
Normal file
@@ -0,0 +1,99 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.7.18"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Database Implementation"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
# Databases
|
||||
mongodb = ["dep:mongodb", "bson"]
|
||||
|
||||
# ... Other
|
||||
tasks = ["isahc", "linkify", "url-escape"]
|
||||
async-std-runtime = ["async-std"]
|
||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
||||
axum-impl = ["axum"]
|
||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||
|
||||
# Default Features
|
||||
default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.7.18", path = "../config", features = [
|
||||
"report-macros",
|
||||
] }
|
||||
revolt-result = { version = "0.7.18", path = "../result" }
|
||||
revolt-models = { version = "0.7.18", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.18", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.18", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
|
||||
# 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"
|
||||
revolt_optional_struct = "0.2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
|
||||
|
||||
# Events
|
||||
redis-kiss = { version = "0.1.4" }
|
||||
|
||||
# Database
|
||||
bson = { optional = true, version = "2.1.0" }
|
||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
||||
|
||||
# Database Migration
|
||||
unicode-segmentation = "1.10.1"
|
||||
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 }
|
||||
revolt_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
||||
|
||||
# Notifications
|
||||
fcm_v1 = "0.3.0"
|
||||
web-push = "0.10.0"
|
||||
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0.8" }
|
||||
28
crates/core/database/fixtures/group_with_members.json
Normal file
28
crates/core/database/fixtures/group_with_members.json
Normal file
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Member",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "Member",
|
||||
"discriminator": "0002"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "Group",
|
||||
"name": "My Group",
|
||||
"owner": "__ID:0__",
|
||||
"recipients": ["__ID:0__", "__ID:1__"]
|
||||
}
|
||||
]
|
||||
89
crates/core/database/fixtures/server_with_roles.json
Normal file
89
crates/core/database/fixtures/server_with_roles.json
Normal file
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Moderator",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "User",
|
||||
"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__": {
|
||||
"name": "Moderator",
|
||||
"permissions": {
|
||||
"a": 545270208,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 3
|
||||
},
|
||||
"__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
|
||||
}
|
||||
]
|
||||
181
crates/core/database/src/drivers/mod.rs
Normal file
181
crates/core/database/src/drivers/mod.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use authifier::config::Captcha;
|
||||
use authifier::config::EmailVerificationConfig;
|
||||
use authifier::config::ResolveIp;
|
||||
use authifier::config::SMTPSettings;
|
||||
use authifier::config::Shield;
|
||||
use authifier::config::Template;
|
||||
use authifier::config::Templates;
|
||||
use authifier::Authifier;
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
|
||||
pub use self::mongodb::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
/// Database information to use to create a client
|
||||
pub enum DatabaseInfo {
|
||||
/// Auto-detect the database in use
|
||||
Auto,
|
||||
/// Auto-detect the database in use and create an empty testing database
|
||||
Test(String),
|
||||
/// Use the mock database
|
||||
Reference,
|
||||
/// Connect to MongoDB
|
||||
MongoDb { uri: String, database_name: String },
|
||||
/// Use existing MongoDB connection
|
||||
MongoDbFromClient(::mongodb::Client, String),
|
||||
}
|
||||
|
||||
/// Database
|
||||
#[derive(Clone)]
|
||||
pub enum Database {
|
||||
/// Mock database
|
||||
Reference(ReferenceDb),
|
||||
/// MongoDB database
|
||||
MongoDb(MongoDb),
|
||||
}
|
||||
|
||||
impl DatabaseInfo {
|
||||
/// Create a database client from the given database information
|
||||
#[async_recursion]
|
||||
pub async fn connect(self) -> Result<Database, String> {
|
||||
let config = config().await;
|
||||
|
||||
Ok(match self {
|
||||
DatabaseInfo::Auto => {
|
||||
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() {
|
||||
DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name: "revolt".to_string(),
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
} else {
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Test(database_name) => {
|
||||
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" => {
|
||||
DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name,
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
}
|
||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Reference => Database::Reference(Default::default()),
|
||||
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))
|
||||
}
|
||||
DatabaseInfo::MongoDbFromClient(client, database_name) => {
|
||||
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 {
|
||||
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@revolt.chat".into()),
|
||||
),
|
||||
port: config.api.smtp.port,
|
||||
use_tls: config.api.smtp.use_tls,
|
||||
},
|
||||
expiry: Default::default(),
|
||||
templates: Templates {
|
||||
verify: Template {
|
||||
title: "Verify your Revolt 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 Revolt password.".into(),
|
||||
text: include_str!("../../templates/reset.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/reset.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 {
|
||||
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(),
|
||||
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: auth_config,
|
||||
event_channel: Some(crate::tasks::authifier_relay::sender()),
|
||||
}
|
||||
}
|
||||
}
|
||||
243
crates/core/database/src/drivers/mongodb.rs
Normal file
243
crates/core/database/src/drivers/mongodb.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
|
||||
use futures::StreamExt;
|
||||
use mongodb::bson::{doc, to_document, Document};
|
||||
use mongodb::error::Result;
|
||||
use mongodb::options::{FindOneOptions, FindOptions};
|
||||
use mongodb::results::{DeleteResult, InsertOneResult, UpdateResult};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
database_derived!(
|
||||
#[cfg(feature = "mongodb")]
|
||||
/// MongoDB implementation
|
||||
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
||||
);
|
||||
|
||||
impl Deref for MongoDb {
|
||||
type Target = mongodb::Client;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl MongoDb {
|
||||
/// Get the Revolt database
|
||||
pub fn db(&self) -> mongodb::Database {
|
||||
self.database(&self.1)
|
||||
}
|
||||
|
||||
/// Get a collection by its name
|
||||
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
|
||||
self.db().collection(collection)
|
||||
}
|
||||
|
||||
/// Insert one document into a collection
|
||||
pub async fn insert_one<T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
document: T,
|
||||
) -> Result<InsertOneResult> {
|
||||
self.col::<T>(collection).insert_one(document, None).await
|
||||
}
|
||||
|
||||
/// Count documents by projection
|
||||
pub async fn count_documents(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<u64> {
|
||||
self.col::<Document>(collection)
|
||||
.count_documents(projection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find multiple documents in a collection with options
|
||||
pub async fn find_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
options: O,
|
||||
) -> Result<Vec<T>>
|
||||
where
|
||||
O: Into<Option<FindOptions>>,
|
||||
{
|
||||
Ok(self
|
||||
.col::<T>(collection)
|
||||
.find(projection, options)
|
||||
.await?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
// Hard fail on invalid documents
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<T>>()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Find multiple documents in a collection
|
||||
pub async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<Vec<T>> {
|
||||
self.find_with_options(collection, projection, None).await
|
||||
}
|
||||
|
||||
/// Find one document with options
|
||||
pub async fn find_one_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
options: O,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
O: Into<Option<FindOneOptions>>,
|
||||
{
|
||||
self.col::<T>(collection)
|
||||
.find_one(projection, options)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find one document
|
||||
pub async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<Option<T>> {
|
||||
self.find_one_with_options(collection, projection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find one document by its ID
|
||||
pub async fn find_one_by_id<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
) -> Result<Option<T>> {
|
||||
self.find_one(
|
||||
collection,
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Update one document given a projection, partial document, and list of paths to unset
|
||||
pub async fn update_one<P, T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
partial: T,
|
||||
remove: Vec<&dyn IntoDocumentPath>,
|
||||
prefix: P,
|
||||
) -> Result<UpdateResult>
|
||||
where
|
||||
P: Into<Option<String>>,
|
||||
{
|
||||
let prefix = prefix.into();
|
||||
|
||||
let mut unset = doc! {};
|
||||
for field in remove {
|
||||
if let Some(path) = field.as_path() {
|
||||
if let Some(prefix) = &prefix {
|
||||
unset.insert(prefix.to_owned() + path, 1_i32);
|
||||
} else {
|
||||
unset.insert(path, 1_i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let query = doc! {
|
||||
"$unset": unset,
|
||||
"$set": if let Some(prefix) = &prefix {
|
||||
to_document(&prefix_keys(&partial, prefix))
|
||||
} else {
|
||||
to_document(&partial)
|
||||
}?
|
||||
};
|
||||
|
||||
self.col::<Document>(collection)
|
||||
.update_one(projection, query, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Update one document given an ID, partial document, and list of paths to unset
|
||||
pub async fn update_one_by_id<P, T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
partial: T,
|
||||
remove: Vec<&dyn IntoDocumentPath>,
|
||||
prefix: P,
|
||||
) -> Result<UpdateResult>
|
||||
where
|
||||
P: Into<Option<String>>,
|
||||
{
|
||||
self.update_one(
|
||||
collection,
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
partial,
|
||||
remove,
|
||||
prefix,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Delete one document by the given projection
|
||||
pub async fn delete_one(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<DeleteResult> {
|
||||
self.col::<Document>(collection)
|
||||
.delete_one(projection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Delete one document by the given ID
|
||||
pub async fn delete_one_by_id(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
) -> Result<DeleteResult> {
|
||||
self.delete_one(
|
||||
collection,
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Just a string ID struct
|
||||
#[derive(Deserialize)]
|
||||
pub struct DocumentId {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
pub trait IntoDocumentPath: Send + Sync {
|
||||
/// Create JSON key path
|
||||
fn as_path(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
/// Prefix keys on an arbitrary object
|
||||
pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_json::Value> {
|
||||
let v: String = serde_json::to_string(t).unwrap();
|
||||
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)| (format!("{}{}", prefix.to_owned(), k), v))
|
||||
.collect()
|
||||
}
|
||||
33
crates/core/database/src/drivers/reference.rs
Normal file
33
crates/core/database/src/drivers/reference.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
|
||||
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default)]
|
||||
pub struct ReferenceDb {
|
||||
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 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 safety_reports: Arc<Mutex<HashMap<String, Report>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
|
||||
}
|
||||
);
|
||||
296
crates/core/database/src/events/client.rs
Normal file
296
crates/core/database/src/events/client.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use authifier::AuthifierEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, 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)]
|
||||
pub enum Ping {
|
||||
Binary(Vec<u8>),
|
||||
Number(usize),
|
||||
}
|
||||
|
||||
/// Untagged Error
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum ErrorEvent {
|
||||
Error(WebSocketError),
|
||||
APIError(Error),
|
||||
}
|
||||
|
||||
/// Fields provided in Ready payload
|
||||
#[derive(PartialEq)]
|
||||
pub enum ReadyPayloadFields {
|
||||
Users,
|
||||
Servers,
|
||||
Channels,
|
||||
Members,
|
||||
Emoji,
|
||||
|
||||
UserSettings(Vec<String>),
|
||||
ChannelUnreads,
|
||||
}
|
||||
|
||||
/// Protocol Events
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum EventV1 {
|
||||
/// Multiple events
|
||||
Bulk { v: Vec<EventV1> },
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
/// Logged out
|
||||
Logout,
|
||||
/// Basic data to cache
|
||||
Ready {
|
||||
#[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")]
|
||||
user_settings: Option<UserSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping },
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
/// Update existing message
|
||||
MessageUpdate {
|
||||
id: String,
|
||||
channel: String,
|
||||
data: PartialMessage,
|
||||
clear: Vec<FieldsMessage>,
|
||||
},
|
||||
|
||||
/// Append information to existing message
|
||||
MessageAppend {
|
||||
id: String,
|
||||
channel: String,
|
||||
append: AppendMessage,
|
||||
},
|
||||
|
||||
/// Delete message
|
||||
MessageDelete { id: String, channel: String },
|
||||
|
||||
/// New reaction to a message
|
||||
MessageReact {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
user_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Remove user's reaction from message
|
||||
MessageUnreact {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
user_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Remove a reaction from message
|
||||
MessageRemoveReaction {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
emoji_id: String,
|
||||
},
|
||||
|
||||
/// Bulk delete messages
|
||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
||||
|
||||
/// New server
|
||||
ServerCreate {
|
||||
id: String,
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
emojis: Vec<Emoji>,
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
ServerUpdate {
|
||||
id: String,
|
||||
data: PartialServer,
|
||||
clear: Vec<FieldsServer>,
|
||||
},
|
||||
|
||||
/// Delete server
|
||||
ServerDelete { id: String },
|
||||
|
||||
/// Update existing server member
|
||||
ServerMemberUpdate {
|
||||
id: MemberCompositeKey,
|
||||
data: PartialMember,
|
||||
clear: Vec<FieldsMember>,
|
||||
},
|
||||
|
||||
/// User joins server
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
reason: RemovalIntention,
|
||||
},
|
||||
|
||||
/// Server role created or updated
|
||||
ServerRoleUpdate {
|
||||
id: String,
|
||||
role_id: String,
|
||||
data: PartialRole,
|
||||
clear: Vec<FieldsRole>,
|
||||
},
|
||||
|
||||
/// Server role deleted
|
||||
ServerRoleDelete { id: String, role_id: String },
|
||||
|
||||
/// Update existing user
|
||||
UserUpdate {
|
||||
id: String,
|
||||
data: PartialUser,
|
||||
clear: Vec<FieldsUser>,
|
||||
event_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Relationship with another user changed
|
||||
UserRelationship { id: String, user: User },
|
||||
/// Settings updated remotely
|
||||
UserSettingsUpdate { id: String, update: UserSettings },
|
||||
|
||||
/// User has been platform banned or deleted their account
|
||||
///
|
||||
/// Clients should remove the following associated data:
|
||||
/// - Messages
|
||||
/// - DM Channels
|
||||
/// - Relationships
|
||||
/// - Server Memberships
|
||||
///
|
||||
/// 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),
|
||||
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
|
||||
/// New report
|
||||
ReportCreate(Report),
|
||||
/// New channel
|
||||
ChannelCreate(Channel),
|
||||
|
||||
/// Update existing channel
|
||||
ChannelUpdate {
|
||||
id: String,
|
||||
data: PartialChannel,
|
||||
clear: Vec<FieldsChannel>,
|
||||
},
|
||||
|
||||
/// Delete channel
|
||||
ChannelDelete { id: String },
|
||||
|
||||
/// User joins a group
|
||||
ChannelGroupJoin { id: String, user: String },
|
||||
|
||||
/// User leaves a group
|
||||
ChannelGroupLeave { id: String, user: String },
|
||||
|
||||
/// User started typing in a channel
|
||||
ChannelStartTyping { id: String, user: String },
|
||||
|
||||
/// User stopped typing in a channel
|
||||
ChannelStopTyping { id: String, user: String },
|
||||
|
||||
/// User acknowledged message in channel
|
||||
ChannelAck {
|
||||
id: String,
|
||||
user: String,
|
||||
message_id: String,
|
||||
},
|
||||
|
||||
/// New webhook
|
||||
WebhookCreate(Webhook),
|
||||
|
||||
/// Update existing webhook
|
||||
WebhookUpdate {
|
||||
id: String,
|
||||
data: PartialWebhook,
|
||||
remove: Vec<FieldsWebhook>,
|
||||
},
|
||||
|
||||
/// Delete webhook
|
||||
WebhookDelete { id: String },
|
||||
|
||||
/// Auth events
|
||||
Auth(AuthifierEvent),
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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().server(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish private event
|
||||
pub async fn private(self, id: String) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
2
crates/core/database/src/events/mod.rs
Normal file
2
crates/core/database/src/events/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
13
crates/core/database/src/events/server.rs
Normal file
13
crates/core/database/src/events/server.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::client::Ping;
|
||||
|
||||
#[derive(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<()> },
|
||||
}
|
||||
96
crates/core/database/src/lib.rs
Normal file
96
crates/core/database/src/lib.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_recursion;
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[macro_use]
|
||||
extern crate revolt_optional_struct;
|
||||
|
||||
#[macro_use]
|
||||
extern crate revolt_result;
|
||||
|
||||
pub use iso8601_timestamp;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use mongodb;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
#[macro_use]
|
||||
extern crate bson;
|
||||
|
||||
macro_rules! database_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
#[derive(Clone)]
|
||||
$item
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! auto_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
$item
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! auto_derived_partial {
|
||||
( $item:item, $name:expr ) => {
|
||||
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[optional_name = $name]
|
||||
#[opt_skip_serializing_none]
|
||||
#[opt_some_priority]
|
||||
$item
|
||||
};
|
||||
}
|
||||
|
||||
mod drivers;
|
||||
pub use drivers::*;
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! database_test {
|
||||
( | $db: ident | $test:expr ) => {
|
||||
let db = $crate::DatabaseInfo::Test(format!(
|
||||
"{}:{}",
|
||||
file!().replace('/', "_").replace(".rs", ""),
|
||||
line!()
|
||||
))
|
||||
.connect()
|
||||
.await
|
||||
.expect("Database connection failed.");
|
||||
|
||||
db.drop_database().await;
|
||||
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
(|$db: $crate::Database| $test)(db.clone()).await;
|
||||
|
||||
db.drop_database().await
|
||||
};
|
||||
}
|
||||
|
||||
mod models;
|
||||
pub mod util;
|
||||
pub use models::*;
|
||||
|
||||
pub mod events;
|
||||
pub mod tasks;
|
||||
|
||||
/// 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)
|
||||
}
|
||||
5
crates/core/database/src/models/admin_migrations/mod.rs
Normal file
5
crates/core/database/src/models/admin_migrations/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
24
crates/core/database/src/models/admin_migrations/model.rs
Normal file
24
crates/core/database/src/models/admin_migrations/model.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
auto_derived!(
|
||||
/// Document representing migration information
|
||||
pub struct MigrationInfo {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: i32,
|
||||
/// Current database revision
|
||||
pub revision: i32,
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[async_std::test]
|
||||
async fn migrate() {
|
||||
database_test!(|db| async move {
|
||||
// Initialise the database
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Migrate the existing database
|
||||
db.migrate_database().await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
12
crates/core/database/src/models/admin_migrations/ops.rs
Normal file
12
crates/core/database/src/models/admin_migrations/ops.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractMigrations: Sync + Send {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self);
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()>;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractMigrations;
|
||||
|
||||
mod init;
|
||||
mod scripts;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractMigrations for MongoDb {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self) {
|
||||
self.db().drop(None).await.ok();
|
||||
}
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()> {
|
||||
info!("Migrating the database.");
|
||||
|
||||
let list = self
|
||||
.list_database_names(None, None)
|
||||
.await
|
||||
.expect("Failed to fetch database names.");
|
||||
|
||||
if list.iter().any(|x| x == &self.1) {
|
||||
scripts::migrate_database(self).await;
|
||||
} else {
|
||||
init::create_database(self).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
use super::super::get_db;
|
||||
use super::scripts::LATEST_REVISION;
|
||||
|
||||
use log::info;
|
||||
use mongodb::bson::doc;
|
||||
use mongodb::options::CreateCollectionOptions;
|
||||
use crate::mongodb::bson::doc;
|
||||
use crate::mongodb::options::CreateCollectionOptions;
|
||||
use crate::MongoDb;
|
||||
|
||||
pub async fn create_database() {
|
||||
pub async fn create_database(db: &MongoDb) {
|
||||
info!("Creating database.");
|
||||
let db = get_db();
|
||||
let db = db.db();
|
||||
|
||||
db.create_collection("accounts", None)
|
||||
.await
|
||||
@@ -45,6 +44,10 @@ pub async fn create_database() {
|
||||
.await
|
||||
.expect("Failed to create channel_unreads collection.");
|
||||
|
||||
db.create_collection("channel_webhooks", None)
|
||||
.await
|
||||
.expect("Failed to create channel_webhooks collection.");
|
||||
|
||||
db.create_collection("migrations", None)
|
||||
.await
|
||||
.expect("Failed to create migrations collection.");
|
||||
@@ -53,14 +56,34 @@ pub async fn create_database() {
|
||||
.await
|
||||
.expect("Failed to create attachments collection.");
|
||||
|
||||
db.create_collection("attachment_hashes", None)
|
||||
.await
|
||||
.expect("Failed to create attachment_hashes collection.");
|
||||
|
||||
db.create_collection("user_settings", None)
|
||||
.await
|
||||
.expect("Failed to create user_settings collection.");
|
||||
|
||||
db.create_collection("safety_reports", None)
|
||||
.await
|
||||
.expect("Failed to create safety_reports collection.");
|
||||
|
||||
db.create_collection("safety_snapshots", None)
|
||||
.await
|
||||
.expect("Failed to create safety_snapshots collection.");
|
||||
|
||||
db.create_collection("safety_strikes", None)
|
||||
.await
|
||||
.expect("Failed to create safety_strikes collection.");
|
||||
|
||||
db.create_collection("bots", None)
|
||||
.await
|
||||
.expect("Failed to create bots collection.");
|
||||
|
||||
db.create_collection("ratelimit_events", None)
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events collection.");
|
||||
|
||||
db.create_collection(
|
||||
"pubsub",
|
||||
CreateCollectionOptions::builder()
|
||||
@@ -77,13 +100,25 @@ pub async fn create_database() {
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"username": 1
|
||||
"username": 1_i32
|
||||
},
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2
|
||||
"strength": 2_i32
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -105,10 +140,24 @@ pub async fn create_database() {
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1
|
||||
"channel": 1_i32,
|
||||
"_id": 1_i32
|
||||
},
|
||||
"name": "channel"
|
||||
}
|
||||
"name": "channel_id_compound"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
"name": "channel_pinned_compound"
|
||||
},
|
||||
]
|
||||
},
|
||||
None,
|
||||
@@ -116,21 +165,20 @@ pub async fn create_database() {
|
||||
.await
|
||||
.expect("Failed to create message index.");
|
||||
|
||||
get_db()
|
||||
.run_command(
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "messages",
|
||||
"createIndexes": "channel_unreads",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.channel": 1,
|
||||
"_id.user": 1,
|
||||
"_id.channel": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "compound_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
@@ -141,21 +189,20 @@ pub async fn create_database() {
|
||||
.await
|
||||
.expect("Failed to create channel_unreads index.");
|
||||
|
||||
get_db()
|
||||
.run_command(
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "messages",
|
||||
"createIndexes": "server_members",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.server": 1,
|
||||
"_id.user": 1,
|
||||
"_id.server": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "compound_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
@@ -166,10 +213,50 @@ pub async fn create_database() {
|
||||
.await
|
||||
.expect("Failed to create server_members index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "attachments",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"hash": 1_i32
|
||||
},
|
||||
"name": "hash"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"used_for.id": 1_i32
|
||||
},
|
||||
"name": "used_for_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create attachments index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "attachment_hashes",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"processed_hash": 1_i32
|
||||
},
|
||||
"name": "processed_hash"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create attachment_hashes index.");
|
||||
|
||||
db.collection("migrations")
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": 0,
|
||||
"_id": 0_i32,
|
||||
"revision": LATEST_REVISION
|
||||
},
|
||||
None,
|
||||
@@ -177,5 +264,24 @@ pub async fn create_database() {
|
||||
.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"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
|
||||
info!("Created database.");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractMigrations;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractMigrations for ReferenceDb {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self) {}
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()> {
|
||||
// Here you would do your typical migrations if this was a real database.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/bots/mod.rs
Normal file
5
crates/core/database/src/models/bots/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
223
crates/core/database/src/models/bots/model.rs
Normal file
223
crates/core/database/src/models/bots/model.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Bot
|
||||
pub struct Bot {
|
||||
/// Bot Id
|
||||
///
|
||||
/// This equals the associated bot user's id.
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// User Id of the bot owner
|
||||
pub owner: String,
|
||||
/// Token used to authenticate requests for this bot
|
||||
pub token: String,
|
||||
/// Whether the bot is public
|
||||
/// (may be invited by anyone)
|
||||
pub public: bool,
|
||||
|
||||
/// Whether to enable analytics
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub analytics: bool,
|
||||
/// Whether this bot should be publicly discoverable
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub discoverable: bool,
|
||||
/// Reserved; URL for handling interactions
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub interactions_url: String,
|
||||
/// URL for terms of service
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub terms_of_service_url: String,
|
||||
/// URL for privacy policy
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub privacy_policy_url: String,
|
||||
|
||||
/// Enum of bot flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
},
|
||||
"PartialBot"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Optional fields on bot object
|
||||
pub enum FieldsBot {
|
||||
Token,
|
||||
InteractionsURL,
|
||||
}
|
||||
);
|
||||
|
||||
#[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 {
|
||||
FieldsBot::Token => self.token = nanoid::nanoid!(64),
|
||||
FieldsBot::InteractionsURL => {
|
||||
self.interactions_url = String::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update this bot
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
mut partial: PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
if remove.contains(&FieldsBot::Token) {
|
||||
partial.token = Some(nanoid::nanoid!(64));
|
||||
}
|
||||
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Delete this bot
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
db.fetch_user(&self.id).await?.mark_deleted(db).await?;
|
||||
db.delete_bot(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Bot, FieldsBot, PartialBot, User};
|
||||
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let owner = User::create(&db, "Owner".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
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();
|
||||
|
||||
assert!(!bot.interactions_url.is_empty());
|
||||
|
||||
let mut updated_bot = bot.clone();
|
||||
updated_bot
|
||||
.update(
|
||||
&db,
|
||||
PartialBot {
|
||||
public: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
vec![FieldsBot::Token, FieldsBot::InteractionsURL],
|
||||
)
|
||||
.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(&owner.id).await.unwrap();
|
||||
|
||||
assert!(!bot.public);
|
||||
assert!(fetched_bot1.public);
|
||||
assert!(!bot.interactions_url.is_empty());
|
||||
assert!(fetched_bot1.interactions_url.is_empty());
|
||||
assert_ne!(bot.token, fetched_bot1.token);
|
||||
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(&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(&owner.id).await.unwrap());
|
||||
assert_eq!(db.fetch_user(&bot.id).await.unwrap().flags, Some(2))
|
||||
});
|
||||
}
|
||||
}
|
||||
35
crates/core/database/src/models/bots/ops.rs
Normal file
35
crates/core/database/src/models/bots/ops.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractBots: Sync + Send {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()>;
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot>;
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot>;
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>>;
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize>;
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
92
crates/core/database/src/models/bots/ops/mongodb.rs
Normal file
92
crates/core/database/src/models/bots/ops/mongodb.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractBots;
|
||||
|
||||
static COL: &str = "bots";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractBots for MongoDb {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
|
||||
query!(self, insert_one, COL, &bot).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"token": token
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"owner": user_id
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
|
||||
query!(
|
||||
self,
|
||||
count_documents,
|
||||
COL,
|
||||
doc! {
|
||||
"owner": user_id
|
||||
}
|
||||
)
|
||||
.map(|v| v as usize)
|
||||
}
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsBot {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
FieldsBot::InteractionsURL => Some("interactions_url"),
|
||||
FieldsBot::Token => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
82
crates/core/database/src/models/bots/ops/reference.rs
Normal file
82
crates/core/database/src/models/bots/ops/reference.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
use super::AbstractBots;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractBots for ReferenceDb {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if bots.contains_key(&bot.id) {
|
||||
Err(create_database_error!("insert", "bot"))
|
||||
} else {
|
||||
bots.insert(bot.id.to_string(), bot.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
|
||||
let bots = self.bots.lock().await;
|
||||
bots.get(id).cloned().ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
|
||||
let bots = self.bots.lock().await;
|
||||
bots.values()
|
||||
.find(|bot| bot.token == token)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
|
||||
let bots = self.bots.lock().await;
|
||||
Ok(bots
|
||||
.values()
|
||||
.filter(|bot| bot.owner == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
|
||||
let bots = self.bots.lock().await;
|
||||
Ok(bots.values().filter(|bot| bot.owner == user_id).count())
|
||||
}
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if let Some(bot) = bots.get_mut(id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
bot.remove_field(&field);
|
||||
}
|
||||
|
||||
bot.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if bots.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/channel_invites/mod.rs
Normal file
5
crates/core/database/src/models/channel_invites/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
106
crates/core/database/src/models/channel_invites/model.rs
Normal file
106
crates/core/database/src/models/channel_invites/model.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use revolt_result::{create_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',
|
||||
];
|
||||
|
||||
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 {
|
||||
match self {
|
||||
Invite::Server { code, .. } | Invite::Group { code, .. } => code,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ID of the user who created this invite
|
||||
pub fn creator(&'_ self) -> &'_ str {
|
||||
match self {
|
||||
Invite::Server { creator, .. } | Invite::Group { creator, .. } => creator,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new invite from given information
|
||||
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, .. } => {
|
||||
Ok(Invite::Server {
|
||||
code,
|
||||
creator: creator.id.clone(),
|
||||
server: server.clone(),
|
||||
channel: id.clone(),
|
||||
})
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}?;
|
||||
|
||||
db.insert_invite(&invite).await?;
|
||||
Ok(invite)
|
||||
}
|
||||
|
||||
/// Resolve an invite by its ID or by a public server ID
|
||||
pub async fn find(db: &Database, code: &str) -> Result<Invite> {
|
||||
if let Ok(invite) = db.fetch_invite(code).await {
|
||||
return Ok(invite);
|
||||
} else if let Ok(server) = db.fetch_server(code).await {
|
||||
if server.discoverable {
|
||||
if let Some(channel) = server.channels.into_iter().next() {
|
||||
return Ok(Invite::Server {
|
||||
code: code.to_string(),
|
||||
server: server.id,
|
||||
creator: server.owner,
|
||||
channel,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
21
crates/core/database/src/models/channel_invites/ops.rs
Normal file
21
crates/core/database/src/models/channel_invites/ops.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
|
||||
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,50 @@
|
||||
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,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.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,
|
||||
}
|
||||
);
|
||||
34
crates/core/database/src/models/channel_unreads/ops.rs
Normal file
34
crates/core/database/src/models/channel_unreads/ops.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ChannelUnread;
|
||||
|
||||
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<()>;
|
||||
|
||||
/// 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>>;
|
||||
}
|
||||
139
crates/core/database/src/models/channel_unreads/ops/mongodb.rs
Normal file
139
crates/core/database/src/models/channel_unreads/ops/mongodb.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
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": {
|
||||
"$lt": message_id
|
||||
}
|
||||
},
|
||||
"$set": {
|
||||
"last_id": message_id
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
None,
|
||||
)
|
||||
.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>>(),
|
||||
None,
|
||||
)
|
||||
.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
|
||||
}
|
||||
}
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", 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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
100
crates/core/database/src/models/channel_unreads/ops/reference.rs
Normal file
100
crates/core/database/src/models/channel_unreads/ops/reference.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/channel_webhooks/mod.rs
Normal file
5
crates/core/database/src/models/channel_webhooks/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
179
crates/core/database/src/models/channel_webhooks/model.rs
Normal file
179
crates/core/database/src/models/channel_webhooks/model.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::events::client::EventV1;
|
||||
use crate::{Database, File};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Webhook
|
||||
pub struct Webhook {
|
||||
/// Webhook Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// The name of the webhook
|
||||
pub name: String,
|
||||
|
||||
/// The avatar of the webhook
|
||||
#[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>,
|
||||
},
|
||||
"PartialWebhook"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Optional fields on webhook object
|
||||
pub enum FieldsWebhook {
|
||||
Avatar,
|
||||
}
|
||||
);
|
||||
|
||||
#[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<()> {
|
||||
db.insert_webhook(self).await?;
|
||||
|
||||
// Avoid leaking the token to people who receive the event
|
||||
let mut webhook = self.clone();
|
||||
webhook.token = None;
|
||||
|
||||
EventV1::WebhookCreate(webhook.into())
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn assert_token(&self, token: &str) -> Result<()> {
|
||||
if self.token.as_deref() == Some(token) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotAuthenticated))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
mut partial: PartialWebhook,
|
||||
remove: Vec<FieldsWebhook>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field)
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_webhook(&self.id, &partial, &remove).await?;
|
||||
|
||||
partial.token = None; // Avoid leaking the token to people who receive the event
|
||||
|
||||
EventV1::WebhookUpdate {
|
||||
id: self.id.clone(),
|
||||
data: partial.into(),
|
||||
remove: remove.into_iter().map(|v| v.into()).collect(),
|
||||
}
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsWebhook) {
|
||||
match field {
|
||||
FieldsWebhook::Avatar => self.avatar = None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
db.delete_webhook(&self.id).await?;
|
||||
|
||||
EventV1::WebhookDelete {
|
||||
id: self.id.clone(),
|
||||
}
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let webhook_id = "webhook";
|
||||
let channel_id = "channel";
|
||||
|
||||
let webhook = Webhook {
|
||||
id: webhook_id.to_string(),
|
||||
name: "Webhook Name".to_string(),
|
||||
channel_id: channel_id.to_string(),
|
||||
avatar: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_webhook(&webhook).await.unwrap();
|
||||
|
||||
let mut updated_webhook = webhook.clone();
|
||||
updated_webhook
|
||||
.update(
|
||||
&db,
|
||||
PartialWebhook {
|
||||
name: Some("New Name".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![FieldsWebhook::Avatar],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched_webhook = db.fetch_webhook(webhook_id).await.unwrap();
|
||||
let fetched_webhooks = db.fetch_webhooks_for_channel(channel_id).await.unwrap();
|
||||
|
||||
assert_eq!(updated_webhook, fetched_webhook);
|
||||
assert_ne!(webhook, fetched_webhook);
|
||||
assert_eq!(1, fetched_webhooks.len());
|
||||
assert_eq!(fetched_webhook, fetched_webhooks[0]);
|
||||
|
||||
webhook.delete(&db).await.unwrap();
|
||||
assert!(db.fetch_webhook(webhook_id).await.is_err());
|
||||
assert_eq!(
|
||||
0,
|
||||
db.fetch_webhooks_for_channel(channel_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
29
crates/core/database/src/models/channel_webhooks/ops.rs
Normal file
29
crates/core/database/src/models/channel_webhooks/ops.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractWebhooks: Sync + Send {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>;
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook>;
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>>;
|
||||
|
||||
/// Update webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractWebhooks;
|
||||
|
||||
static COL: &str = "channel_webhooks";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractWebhooks for MongoDb {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
|
||||
query!(self, insert_one, COL, &webhook).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
|
||||
query!(self, find_one_by_id, COL, webhook_id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
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,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Update webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
webhook_id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, webhook_id).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsWebhook {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsWebhook::Avatar => "avatar",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
use super::AbstractWebhooks;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractWebhooks for ReferenceDb {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if webhooks.contains_key(&webhook.id) {
|
||||
Err(create_database_error!("insert", "webhook"))
|
||||
} else {
|
||||
webhooks.insert(webhook.id.to_string(), webhook.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
|
||||
let webhooks = self.channel_webhooks.lock().await;
|
||||
webhooks
|
||||
.get(webhook_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
||||
let webhooks = self.channel_webhooks.lock().await;
|
||||
Ok(webhooks
|
||||
.values()
|
||||
.filter(|webhook| webhook.channel_id == channel_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Update webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if let Some(webhook) = webhooks.get_mut(webhook_id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
webhook.remove_field(field);
|
||||
}
|
||||
|
||||
webhook.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if webhooks.remove(webhook_id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/channels/mod.rs
Normal file
5
crates/core/database/src/models/channels/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
826
crates/core/database/src/models/channels/model.rs
Normal file
826
crates/core/database/src/models/channels/model.rs
Normal file
@@ -0,0 +1,826 @@
|
||||
use std::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, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
|
||||
Server, SystemMessage, User,
|
||||
};
|
||||
|
||||
auto_derived!(
|
||||
#[serde(tag = "channel_type")]
|
||||
pub enum Channel {
|
||||
/// Personal "Saved Notes" channel which allows users to save messages
|
||||
SavedMessages {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the user this channel belongs to
|
||||
user: String,
|
||||
},
|
||||
/// Direct message channel between two users
|
||||
DirectMessage {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
|
||||
/// Whether this direct message channel is currently open on both sides
|
||||
active: bool,
|
||||
/// 2-tuple of user ids participating in direct message
|
||||
recipients: Vec<String>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
},
|
||||
/// Group channel between 1 or more participants
|
||||
Group {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
/// User id of the owner of the group
|
||||
owner: String,
|
||||
/// Channel description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
/// Array of user ids participating in channel
|
||||
recipients: Vec<String>,
|
||||
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
|
||||
/// Permissions assigned to members of this group
|
||||
/// (does not apply to the owner of the group)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
permissions: Option<i64>,
|
||||
|
||||
/// Whether this group is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
/// Text channel belonging to a server
|
||||
TextChannel {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
/// Channel description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
|
||||
/// 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,
|
||||
},
|
||||
/// 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,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
#[derive(Default)]
|
||||
pub struct PartialChannel {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub owner: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<File>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nsfw: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub permissions: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role_permissions: Option<HashMap<String, OverrideField>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_permissions: Option<OverrideField>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_message_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Optional fields on channel object
|
||||
pub enum FieldsChannel {
|
||||
Description,
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl 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),
|
||||
},
|
||||
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
|
||||
id: id.clone(),
|
||||
server: server.id.to_owned(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
icon: None,
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
},
|
||||
};
|
||||
|
||||
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 recipients = data.users.into_iter().collect::<Vec<String>>();
|
||||
let channel = Channel::Group {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
|
||||
name: data.name,
|
||||
owner: owner_id,
|
||||
description: data.description,
|
||||
recipients: recipients.clone(),
|
||||
|
||||
icon: None,
|
||||
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?;
|
||||
|
||||
match &channel {
|
||||
Channel::DirectMessage { .. } => {
|
||||
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: &User,
|
||||
by_id: &str,
|
||||
) -> Result<()> {
|
||||
if let Channel::Group { recipients, .. } = self {
|
||||
if recipients.contains(&String::from(&user.id)) {
|
||||
return Err(create_error!(AlreadyInGroup));
|
||||
}
|
||||
|
||||
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?;
|
||||
|
||||
EventV1::ChannelGroupJoin {
|
||||
id: id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
SystemMessage::UserAdded {
|
||||
id: user.id.to_string(),
|
||||
by: by_id.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user.id.to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map out whether it is a direct DM
|
||||
pub fn is_direct_dm(&self) -> bool {
|
||||
matches!(self, Channel::DirectMessage { .. })
|
||||
}
|
||||
|
||||
/// Check whether has a user as a recipient
|
||||
pub fn contains_user(&self, user_id: &str) -> bool {
|
||||
match self {
|
||||
Channel::Group { recipients, .. } => recipients.contains(&String::from(user_id)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of recipients
|
||||
pub fn users(&self) -> Result<Vec<String>> {
|
||||
match self {
|
||||
Channel::Group { recipients, .. } => Ok(recipients.to_owned()),
|
||||
_ => Err(create_error!(NotFound)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set role permission on a channel
|
||||
pub async fn set_role_permission(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
db.set_channel_role_permission(id, role_id, permissions)
|
||||
.await?;
|
||||
|
||||
role_permissions.insert(role_id.to_string(), permissions);
|
||||
|
||||
EventV1::ChannelUpdate {
|
||||
id: id.clone(),
|
||||
data: PartialChannel {
|
||||
role_permissions: Some(role_permissions.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
clear: vec![],
|
||||
}
|
||||
.p(server.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update channel data
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
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, .. } | Self::VoiceChannel { server, .. } => server.clone(),
|
||||
_ => id,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a field from Channel object
|
||||
pub fn remove_field(&mut self, field: &FieldsChannel) {
|
||||
match field {
|
||||
FieldsChannel::Description => match self {
|
||||
Self::Group { description, .. }
|
||||
| Self::TextChannel { description, .. }
|
||||
| Self::VoiceChannel { description, .. } => {
|
||||
description.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Icon => match self {
|
||||
Self::Group { icon, .. }
|
||||
| Self::TextChannel { icon, .. }
|
||||
| Self::VoiceChannel { icon, .. } => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::DefaultPermissions => match self {
|
||||
Self::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
} => {
|
||||
default_permissions.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove multiple fields from Channel object
|
||||
pub fn remove_fields(&mut self, partial: Vec<FieldsChannel>) {
|
||||
for field in partial {
|
||||
self.remove_field(&field)
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply partial channel to channel
|
||||
pub fn apply_options(&mut self, partial: PartialChannel) {
|
||||
match self {
|
||||
Self::SavedMessages { .. } => {}
|
||||
Self::DirectMessage { active, .. } => {
|
||||
if let Some(v) = partial.active {
|
||||
*active = v;
|
||||
}
|
||||
}
|
||||
Self::Group {
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
permissions,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
*name = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.owner {
|
||||
*owner = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.description {
|
||||
description.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.icon {
|
||||
icon.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.nsfw {
|
||||
*nsfw = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.permissions {
|
||||
permissions.replace(v);
|
||||
}
|
||||
}
|
||||
Self::TextChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
*name = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.description {
|
||||
description.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.icon {
|
||||
icon.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.nsfw {
|
||||
*nsfw = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.role_permissions {
|
||||
*role_permissions = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.default_permissions {
|
||||
default_permissions.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;
|
||||
|
||||
crate::tasks::ack::queue(
|
||||
self.id().to_string(),
|
||||
user.to_string(),
|
||||
AckEvent::AckMessage {
|
||||
id: message.to_string(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove user from a group
|
||||
pub async fn remove_user_from_group(
|
||||
&self,
|
||||
db: &Database,
|
||||
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) {
|
||||
db.update_channel(
|
||||
id,
|
||||
&PartialChannel {
|
||||
owner: Some(new_owner.into()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
SystemMessage::ChannelOwnershipChanged {
|
||||
from: owner.to_string(),
|
||||
to: new_owner.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: name,
|
||||
avatar: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
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(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
if !silent {
|
||||
if let Some(by) = by_id {
|
||||
SystemMessage::UserRemove {
|
||||
id: user.id.to_string(),
|
||||
by: by.to_string(),
|
||||
}
|
||||
} else {
|
||||
SystemMessage::UserLeft {
|
||||
id: user.id.to_string(),
|
||||
}
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsChannel {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsChannel::Description => "description",
|
||||
FieldsChannel::Icon => "icon",
|
||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
});
|
||||
}
|
||||
}
|
||||
50
crates/core/database/src/models/channels/ops.rs
Normal file
50
crates/core/database/src/models/channels/ops.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannels: Sync + Send {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()>;
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel>;
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>>;
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>>;
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel>;
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel>;
|
||||
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()>;
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel_id: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()>;
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
|
||||
}
|
||||
312
crates/core/database/src/models/channels/ops/mongodb.rs
Normal file
312
crates/core/database/src/models/channels/ops/mongodb.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
use super::AbstractChannels;
|
||||
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
||||
use bson::{Bson, Document};
|
||||
use futures::StreamExt;
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
static COL: &str = "channels";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannels for MongoDb {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
query!(self, insert_one, COL, &channel).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
|
||||
query!(self, find_one_by_id, COL, channel_id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
||||
Ok(self
|
||||
.col::<Channel>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("fetch", "channels"))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"$or": [
|
||||
{
|
||||
"$or": [
|
||||
{
|
||||
"channel_type": "DirectMessage"
|
||||
},
|
||||
{
|
||||
"channel_type": "Group"
|
||||
}
|
||||
],
|
||||
"recipients": user_id
|
||||
},
|
||||
{
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_id
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_id
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(InternalError))
|
||||
}
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
||||
let doc = match (user_a, user_b) {
|
||||
self_user if self_user.0 == self_user.1 => {
|
||||
doc! {
|
||||
"channel_type": "SavedMessages",
|
||||
"user": self_user.0
|
||||
}
|
||||
}
|
||||
users => {
|
||||
doc! {
|
||||
"channel_type": "DirectMessage",
|
||||
"recipients": {
|
||||
"$all": [ users.0, users.1 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
query!(self, find_one, COL, doc)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": channel
|
||||
},
|
||||
doc! {
|
||||
"$push": {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channel"))
|
||||
}
|
||||
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel: &str,
|
||||
role: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! { "_id": channel },
|
||||
doc! {
|
||||
"$set": {
|
||||
"role_permissions.".to_owned() + role: permissions
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channel"))
|
||||
}
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
channel,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": channel
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channels"))
|
||||
}
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let id = channel.id().to_string();
|
||||
let server_id = match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Delete invites and unreads.
|
||||
self.delete_associated_channel_objects(Bson::String(id.to_string()))
|
||||
.await?;
|
||||
|
||||
// Delete messages.
|
||||
self.delete_bulk_messages(doc! {
|
||||
"channel": &id
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Remove from server object.
|
||||
if let Some(server) = server_id {
|
||||
let server = self.fetch_server(server).await?;
|
||||
let mut update = doc! {
|
||||
"$pull": {
|
||||
"channels": &id
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(sys) = &server.system_messages {
|
||||
let mut unset = doc! {};
|
||||
|
||||
if let Some(cid) = &sys.user_joined {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_joined", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_left {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_left", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_kicked {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_kicked", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_banned {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_banned", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if !unset.is_empty() {
|
||||
update.insert("$unset", unset);
|
||||
}
|
||||
}
|
||||
|
||||
self.col::<Document>("servers")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": server.id
|
||||
},
|
||||
update,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", "servers"))?;
|
||||
}
|
||||
|
||||
// Delete associated attachments
|
||||
self.delete_many_attachments(doc! {
|
||||
"used_for.id": &id
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Delete the channel itself
|
||||
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
|
||||
// Delete all invites to these channels.
|
||||
self.col::<Document>("channel_invites")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
|
||||
|
||||
// Delete unread message objects on channels.
|
||||
self.col::<Document>("channel_unreads")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
|
||||
.map(|_| ())?;
|
||||
|
||||
// update many attachments with parent id
|
||||
|
||||
// Delete all webhooks on this channel.
|
||||
self.col::<Document>("webhooks")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "webhooks"))
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
157
crates/core/database/src/models/channels/ops/reference.rs
Normal file
157
crates/core/database/src/models/channels/ops/reference.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use super::AbstractChannels;
|
||||
use crate::ReferenceDb;
|
||||
use crate::{Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannels for ReferenceDb {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Entry::Vacant(entry) = channels.entry(channel.id().to_string()) {
|
||||
entry.insert(channel.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_database_error!("insert", "channel"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
channels
|
||||
.get(channel_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
||||
let channels = self.channels.lock().await;
|
||||
ids.iter()
|
||||
.map(|id| {
|
||||
channels
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
||||
let channels = self.channels.lock().await;
|
||||
Ok(channels
|
||||
.values()
|
||||
.filter(|channel| channel.contains_user(user_id))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
channels
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_database_error!("fetch", "channel"))
|
||||
}
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
for (_, data) in channels.iter() {
|
||||
if data.contains_user(user_a) && data.contains_user(user_b) {
|
||||
return Ok(data.to_owned());
|
||||
}
|
||||
}
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
|
||||
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(channel_id) {
|
||||
recipients.push(String::from(user_id));
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(InvalidOperation))
|
||||
}
|
||||
}
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
|
||||
if let Some(mut channel) = channels.get_mut(channel_id) {
|
||||
match &mut channel {
|
||||
Channel::TextChannel {
|
||||
role_permissions, ..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
role_permissions, ..
|
||||
} => {
|
||||
if role_permissions.get(role_id).is_some() {
|
||||
role_permissions.remove(role_id);
|
||||
role_permissions.insert(String::from(role_id), permissions);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
_ => Err(create_error!(NotFound)),
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Some(channel_data) = channels.get_mut(id) {
|
||||
channel_data.apply_options(channel.to_owned());
|
||||
channel_data.remove_fields(remove);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Some(channel_data) = channels.get_mut(channel) {
|
||||
if channel_data.users()?.contains(&String::from(user)) {
|
||||
channel_data.users()?.retain(|x| x != user);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
}
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if channels.remove(channel.id()).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/emojis/mod.rs
Normal file
5
crates/core/database/src/models/emojis/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
87
crates/core/database/src/models/emojis/model.rs
Normal file
87
crates/core/database/src/models/emojis/model.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::events::client::EventV1;
|
||||
use crate::Database;
|
||||
|
||||
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
include_str!("unicode_emoji.txt")
|
||||
.split('\n')
|
||||
.map(|x| x.into())
|
||||
.collect()
|
||||
});
|
||||
|
||||
auto_derived!(
|
||||
/// Emoji
|
||||
pub struct Emoji {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// What owns this emoji
|
||||
pub parent: EmojiParent,
|
||||
/// Uploader user id
|
||||
pub creator_id: String,
|
||||
/// Emoji name
|
||||
pub name: String,
|
||||
/// Whether the emoji is animated
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub animated: bool,
|
||||
/// Whether the emoji is marked as nsfw
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub nsfw: bool,
|
||||
}
|
||||
|
||||
/// Parent Id of the emoji
|
||||
#[serde(tag = "type")]
|
||||
pub enum EmojiParent {
|
||||
Server { id: String },
|
||||
Detached,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Emoji {
|
||||
/// Get parent id
|
||||
fn parent(&self) -> &str {
|
||||
match &self.parent {
|
||||
EmojiParent::Server { id } => id,
|
||||
EmojiParent::Detached => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an emoji
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_emoji(self).await?;
|
||||
|
||||
EventV1::EmojiCreate(self.clone().into())
|
||||
.p(self.parent().to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete an emoji
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
EventV1::EmojiDelete {
|
||||
id: self.id.to_string(),
|
||||
}
|
||||
.p(self.parent().to_string())
|
||||
.await;
|
||||
|
||||
db.detach_emoji(&self).await
|
||||
}
|
||||
|
||||
/// Check whether we can use a given emoji
|
||||
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
||||
if Ulid::from_str(emoji).is_ok() {
|
||||
db.fetch_emoji(emoji).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user