Compare commits
508 Commits
0.2.9
...
0.5.3-5-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
1303dbc844 | ||
|
|
7981748b7b | ||
|
|
53277cb235 | ||
|
|
92c7f023b1 | ||
|
|
839d7d0d9a | ||
|
|
ea8784215d | ||
|
|
084c635e9c | ||
|
|
924398df1d | ||
|
|
c204288b76 | ||
|
|
dcee48c69f | ||
|
|
c6efe3b190 | ||
|
|
13ed849657 | ||
|
|
08c1aa6f5d | ||
|
|
84ba038350 | ||
|
|
8eb6f5a8af | ||
|
|
012a30ce61 | ||
|
|
0025bea23d | ||
|
|
997d1fffc0 | ||
|
|
6366e0ac24 | ||
|
|
8d25dd1d65 | ||
|
|
49c7bc0ffe | ||
|
|
fe5ba39e21 | ||
|
|
2a4e290489 | ||
|
|
193846e289 | ||
|
|
7421ffc8a2 | ||
|
|
2ef6b28029 | ||
|
|
39a759396f | ||
|
|
e5e0031cad | ||
|
|
211d74bb47 | ||
|
|
0dcf8cb281 | ||
|
|
24a20a8719 | ||
|
|
37d45889ec | ||
|
|
8e9991a2de | ||
|
|
a6d49d388f | ||
|
|
6eee1bc669 | ||
|
|
f8e9924f18 | ||
|
|
6e1f3a79cf | ||
|
|
b1f5ba32ff | ||
|
|
6e70a39fce | ||
|
|
53aa2818b4 | ||
|
|
00a7bcb958 | ||
|
|
edcb7c50ce | ||
|
|
2e40927457 | ||
|
|
e636879065 | ||
|
|
46c4de8c68 | ||
|
|
6e46362d13 | ||
|
|
a2bfaa5791 | ||
|
|
e2da00e6b0 | ||
|
|
839b66109c | ||
|
|
7f3141eec4 | ||
|
|
536e335b25 | ||
|
|
4b8205b75b | ||
|
|
3cbc548dd8 | ||
|
|
6a44a00eeb | ||
|
|
2660b96250 | ||
|
|
b4db321d0e | ||
|
|
0eb52d32ef | ||
|
|
9b6893ca48 | ||
|
|
8aa92564fc | ||
|
|
ddfc0a5f86 | ||
|
|
ad364320c8 | ||
|
|
4f1d841312 | ||
|
|
405f45d0f7 | ||
|
|
0b0249f544 | ||
|
|
d0cb73106d | ||
|
|
5d3fef3bfa | ||
|
|
8bb5fb25dd | ||
|
|
fa8eb874d0 | ||
|
|
06f968022d | ||
|
|
65165b1f8e | ||
|
|
6a29c7cf03 | ||
|
|
8443cfab00 | ||
|
|
99a6e19cfc | ||
|
|
77422f023e | ||
|
|
b99807cf2a | ||
|
|
fc5d14d4aa | ||
|
|
fcb59b2493 | ||
|
|
2f452b15b1 | ||
|
|
602a65f809 | ||
|
|
88a1d46bcc | ||
|
|
d8d0d93a21 | ||
|
|
fe085ef100 | ||
|
|
8813515786 | ||
|
|
6813fdd513 | ||
|
|
175d257e34 | ||
|
|
5328c66cbb | ||
|
|
862f57235c | ||
|
|
835a32932f | ||
|
|
03cb48ed00 | ||
|
|
dbfed52f17 | ||
|
|
43b75a2310 | ||
|
|
5a17038012 | ||
|
|
f6b55574b1 | ||
|
|
b5aa0ee971 | ||
|
|
697132082a | ||
|
|
1a847d0084 | ||
|
|
17a378494c | ||
|
|
6c6a1508dc | ||
|
|
b15e07c707 | ||
|
|
ffa721117e | ||
|
|
c29f6d830c | ||
|
|
effe3763da | ||
|
|
b0d2e99588 | ||
|
|
1ecd74eb6c | ||
|
|
ceb35e58ce | ||
|
|
31ebf19cf9 | ||
|
|
40a4cf244d | ||
|
|
6d2d3003bd | ||
|
|
302c3e0d4b | ||
|
|
867962cc3c | ||
|
|
bcfde79745 | ||
|
|
326e539ad3 | ||
|
|
c632341eb7 | ||
|
|
6de1d1f98b | ||
|
|
721ca3a4f5 | ||
|
|
48d2fbe5ae | ||
|
|
c1d03960ed | ||
|
|
7c7573d3cf | ||
|
|
8ca3e50d91 | ||
|
|
906dd3fb04 | ||
|
|
08fe01a7f2 | ||
|
|
d972395e43 | ||
|
|
5179504ad9 | ||
|
|
d4f7c4a77d | ||
|
|
28c4312880 | ||
|
|
7e84d2671a | ||
|
|
bffac8c4c0 | ||
|
|
3b5bebab78 | ||
|
|
e35a6b12e1 | ||
|
|
f04137d50f | ||
|
|
b3bdf403cf | ||
|
|
bd3606176a | ||
|
|
a71bfa908c | ||
|
|
9f749cfdf8 | ||
|
|
93610ebbc3 | ||
|
|
084d71f050 | ||
|
|
e70f848f21 | ||
|
|
e7065cbf1d | ||
|
|
fdba2d9e15 | ||
|
|
8890e3bab0 | ||
|
|
f09fde616a | ||
|
|
6fe0725799 | ||
|
|
382972ea22 | ||
|
|
9fec4e48c0 | ||
|
|
33262cc703 | ||
|
|
e655f53641 | ||
|
|
74b4c6bf2a | ||
|
|
3c0d736f1e | ||
|
|
8f6e5be1d4 | ||
|
|
7d3ce0c96a | ||
|
|
c6de871ad3 | ||
|
|
f32eaa98d4 | ||
|
|
d4743b1453 | ||
|
|
dd65b9f358 | ||
|
|
ec59d0e9a9 | ||
|
|
2407d4da3a | ||
|
|
a5f7314f97 | ||
|
|
deb2344428 | ||
|
|
37bb0f397f | ||
|
|
ca04f4d510 | ||
|
|
682b6cbd9f | ||
|
|
1a8006b3e4 | ||
|
|
27a9fc3e21 | ||
|
|
1872432725 | ||
|
|
6f8802ab5f | ||
|
|
fe821b8283 | ||
|
|
a4c227ce41 | ||
|
|
edd8ef8a1d | ||
|
|
96c9190fa8 | ||
|
|
e452d6a362 | ||
|
|
57f81e018c | ||
|
|
31c93b9954 | ||
|
|
51dc25c94c | ||
|
|
a017872c99 | ||
|
|
f35a1049da | ||
|
|
07e74e1074 | ||
|
|
35b0c69cdb | ||
|
|
b45ea266d8 | ||
|
|
9a417aa6ad | ||
|
|
ad06ff16c4 | ||
|
|
b10d4f3559 | ||
|
|
4727f997ed | ||
|
|
d2b17b64e7 | ||
|
|
598f6374b3 | ||
|
|
bad9024537 | ||
|
|
6d67b3f767 | ||
|
|
9a2ccecb89 | ||
|
|
c4b39c56d4 | ||
|
|
f33611aee6 | ||
|
|
95d7c23d32 | ||
|
|
6b8158bf54 | ||
|
|
9be9b76035 | ||
|
|
03fa923ef4 | ||
|
|
78c890aa59 | ||
|
|
0f18a6781d | ||
|
|
f0d1ab390b | ||
|
|
335f729c53 | ||
|
|
c763a292a7 | ||
|
|
fce92382bf | ||
|
|
4e90cf1f58 | ||
|
|
782bfb7e03 | ||
|
|
1f1d9613e2 | ||
|
|
0af4fa4be9 | ||
|
|
380369e1c4 | ||
|
|
dbd70abaca | ||
|
|
f9fbe54b17 | ||
|
|
681b2b8ab6 | ||
|
|
812fa2a98f | ||
|
|
1713ad057d | ||
|
|
f32f447233 | ||
|
|
8627f27a50 | ||
|
|
fea68faa54 | ||
|
|
46bf5fcdd1 | ||
|
|
6bb4501c52 | ||
|
|
bff72fa6c3 | ||
|
|
a4c1fee4cc | ||
|
|
62ffa10939 | ||
|
|
a39adb64c6 | ||
|
|
3cd3bc54af | ||
|
|
5aa9624d5c | ||
|
|
7aad88ad3f | ||
|
|
fa3bc56ef4 | ||
|
|
7c7943b9f6 | ||
|
|
67b9b8ceee | ||
|
|
f1462d4e4d | ||
|
|
fae0198343 | ||
|
|
10f116b4a3 | ||
|
|
6716a2d32b | ||
|
|
6cc92b877e | ||
|
|
cc0307f702 | ||
|
|
274d6f2b5a | ||
|
|
0f100213ba | ||
|
|
7293abfc53 | ||
|
|
cf7bb832da | ||
|
|
efc3794f3d | ||
|
|
a319e72655 | ||
|
|
2173b1e9f8 | ||
|
|
c086fe7ac4 | ||
|
|
fa960ebc94 | ||
|
|
b47067b311 | ||
|
|
2e996a487b | ||
|
|
92bface6ae | ||
|
|
8cfa5d7091 | ||
|
|
59b18fd376 | ||
|
|
f135a57a9b | ||
|
|
5da26cb833 | ||
|
|
c8981ac695 | ||
|
|
33b0658680 | ||
|
|
81002c75d2 | ||
|
|
f163cb65de | ||
|
|
f7bcd3ad93 | ||
|
|
9e2250783c | ||
|
|
2050a0609f | ||
|
|
a547177325 | ||
|
|
c3fa7bf74e | ||
|
|
7f5d6f2312 | ||
|
|
9492e145f9 | ||
|
|
78c42fd412 | ||
|
|
3797878d20 | ||
|
|
d2bf477fd8 | ||
|
|
c9bff789de | ||
|
|
56bd763820 | ||
|
|
32cd9d8a13 | ||
|
|
60731e1c70 | ||
|
|
37df18dbf7 | ||
|
|
0a9d5de369 | ||
|
|
d2864ac025 | ||
|
|
4eb76fd154 | ||
|
|
3c7852271a | ||
|
|
dadad271b4 | ||
|
|
db6047f2d3 | ||
|
|
5baf85a8e9 | ||
|
|
10cac358a9 | ||
|
|
64d2707366 | ||
|
|
78cfbf9d21 | ||
|
|
02077a4024 | ||
|
|
52e070c21c | ||
|
|
a2f14d2d37 | ||
|
|
a7ea29d821 | ||
|
|
b6b51bca26 | ||
|
|
2bafffbbc1 | ||
|
|
bdf5fb95d0 | ||
|
|
ea8de2400a | ||
|
|
f5e0b49bbb | ||
|
|
b0d07ec58f | ||
|
|
4c99d079e0 | ||
|
|
38bedbaaea | ||
|
|
ea276c3517 | ||
|
|
648e33a13e | ||
|
|
a76ec207b3 | ||
|
|
687b300bfc | ||
|
|
6586d8f02c | ||
|
|
a8a8c08c29 | ||
|
|
1806ea824b | ||
|
|
222a417fff | ||
|
|
3b85dcce14 | ||
|
|
f42480886b | ||
|
|
23ec2d61f1 | ||
|
|
abd8b1821d | ||
|
|
75a35831da | ||
|
|
99e2f874a1 | ||
|
|
11f7092fcd | ||
|
|
cb882ce1b2 | ||
|
|
5486a68bfa | ||
|
|
4aaf328435 | ||
|
|
9d251f794b | ||
|
|
1d390d483d | ||
|
|
81de29e723 | ||
|
|
984017eb61 | ||
|
|
5ab329dfdd | ||
|
|
3a63d502d9 | ||
|
|
8bb694a1c8 | ||
|
|
c38977e026 | ||
|
|
8cbba0671d | ||
|
|
c21d7c4620 | ||
|
|
901b29f49e | ||
|
|
34ac8f54ef | ||
|
|
c562d33c8f | ||
|
|
accd6d7789 | ||
|
|
ba08746e92 | ||
|
|
a1a921bbcb | ||
|
|
bd789b6825 | ||
|
|
c6ba72d924 | ||
|
|
c401663c47 | ||
|
|
3d3db80e61 | ||
|
|
15357008d6 | ||
|
|
124aa8a9bf | ||
|
|
af8731ac38 | ||
|
|
a9f258de6b | ||
|
|
f6c52de171 | ||
|
|
5e70ceea01 | ||
|
|
bb3667a83b | ||
|
|
80e3baaa15 | ||
|
|
0d3ef9a3b7 | ||
|
|
4f9029f7b9 | ||
|
|
bb73f905e6 | ||
|
|
66a2930a6f | ||
|
|
f24d478454 | ||
|
|
1b711a88ef | ||
|
|
b87f396f40 | ||
|
|
ffff620508 | ||
|
|
7728768539 | ||
|
|
cac21ce2b6 | ||
|
|
0640f68f9c | ||
|
|
1aaa6f3c41 | ||
|
|
f39bc07bb9 | ||
|
|
af56f5e2d8 | ||
|
|
c704f13d72 | ||
|
|
84d09db9b3 | ||
|
|
eb382fa1ec | ||
|
|
798047625a | ||
|
|
16c5a28637 | ||
|
|
c748b4349b | ||
|
|
6253a91276 | ||
|
|
b33f19a3c2 | ||
|
|
a91bae2784 | ||
|
|
4a95132ee8 | ||
|
|
d171374aa1 | ||
|
|
6cfec0ee08 | ||
|
|
5711986768 | ||
|
|
c3362a6e4f | ||
|
|
c95dc19f67 | ||
|
|
ab2a510bc6 | ||
|
|
7c374ec756 | ||
|
|
cbac802978 | ||
|
|
ff0e539c7b | ||
|
|
8bb1977025 | ||
|
|
000ffe6aaa |
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
target
|
||||
.mongo
|
||||
.env
|
||||
79
.env.example
Normal file
@@ -0,0 +1,79 @@
|
||||
# MongoDB URI
|
||||
MONGODB=mongodb://localhost
|
||||
|
||||
# URL to where the Revolt app is publicly accessible
|
||||
REVOLT_APP_URL=http://local.revolt.chat:5000
|
||||
|
||||
# URL to where the API is publicly accessible
|
||||
REVOLT_PUBLIC_URL=http://local.revolt.chat:8000
|
||||
VITE_API_URL=http://local.revolt.chat:8000
|
||||
|
||||
# URL to where the WebSocket server is publicly accessible
|
||||
REVOLT_EXTERNAL_WS_URL=ws://local.revolt.chat:9000
|
||||
|
||||
# URL to where Autumn is publicly available
|
||||
AUTUMN_PUBLIC_URL=http://local.revolt.chat:3000
|
||||
|
||||
# URL to where January is publicly available
|
||||
JANUARY_PUBLIC_URL=http://local.revolt.chat:7000
|
||||
|
||||
# URL to where Vortex is publicly available
|
||||
# VOSO_PUBLIC_URL=https://voso.revolt.chat
|
||||
|
||||
|
||||
##
|
||||
## hCaptcha Settings
|
||||
##
|
||||
|
||||
# If you are sure that you don't want to use hCaptcha, set to 1.
|
||||
REVOLT_UNSAFE_NO_CAPTCHA=1
|
||||
|
||||
# hCaptcha API key
|
||||
# REVOLT_HCAPTCHA_KEY=0x0000000000000000000000000000000000000000
|
||||
|
||||
# hCaptcha site key
|
||||
# REVOLT_HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
|
||||
|
||||
|
||||
##
|
||||
## Email Settings
|
||||
##
|
||||
|
||||
# If you are sure that you don't want to use email verification, set to 1.
|
||||
REVOLT_UNSAFE_NO_EMAIL=1
|
||||
|
||||
# SMTP host
|
||||
# REVOLT_SMTP_HOST=smtp.example.com
|
||||
|
||||
# SMTP username
|
||||
# REVOLT_SMTP_USERNAME=noreply@example.com
|
||||
|
||||
# SMTP password
|
||||
# REVOLT_SMTP_PASSWORD=CHANGEME
|
||||
|
||||
# SMTP From header
|
||||
# REVOLT_SMTP_FROM=Revolt <noreply@example.com>
|
||||
|
||||
|
||||
##
|
||||
## Application Settings
|
||||
##
|
||||
|
||||
# Whether to only allow users to sign up if they have an invite code
|
||||
REVOLT_INVITE_ONLY=0
|
||||
|
||||
# Maximum number of people that can be in a group chat
|
||||
REVOLT_MAX_GROUP_SIZE=150
|
||||
|
||||
# VAPID keys for push notifications
|
||||
# Generate using this guide: https://gitlab.insrt.uk/revolt/delta/-/wikis/vapid
|
||||
# --> Please replace these keys before going into production! <--
|
||||
REVOLT_VAPID_PRIVATE_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw=
|
||||
|
||||
|
||||
##
|
||||
## Vortex configuration
|
||||
##
|
||||
|
||||
# VOSO_MANAGE_TOKEN=CHANGEME
|
||||
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
ko_fi: insertish
|
||||
custom: https://insrt.uk/donate
|
||||
103
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
33
.github/workflows/rust.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Rust build and test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Rust project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install latest nightly
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
49
.github/workflows/triage_issue.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Add Issue to Board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > 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
|
||||
|
||||
- name: Add issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
72
.github/workflows/triage_pr.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > 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
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation (
|
||||
$project: ID!
|
||||
$item: ID!
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectNextItemField(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: $status_value
|
||||
}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
4
.gitignore
vendored
@@ -1,4 +1,8 @@
|
||||
Rocket.toml
|
||||
/target
|
||||
/target_backup
|
||||
**/*.rs.bk
|
||||
.mongo
|
||||
.data
|
||||
.env
|
||||
avatar.png
|
||||
|
||||
34
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "(gdb) Launch",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/target/debug/revolt",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"environment": [{
|
||||
"name": "ROCKET_ADDRESS",
|
||||
"value": "0.0.0.0"
|
||||
}, {
|
||||
"name": "MONGODB",
|
||||
"value": "mongodb://localhost"
|
||||
}],
|
||||
"externalConsole": false,
|
||||
"MIMode": "gdb",
|
||||
"setupCommands": [
|
||||
{
|
||||
"description": "Enable pretty-printing for gdb",
|
||||
"text": "-enable-pretty-printing",
|
||||
"ignoreFailures": true
|
||||
},
|
||||
{
|
||||
"description": "Set Disassembly Flavor to Intel",
|
||||
"text": "-gdb-set disassembly-flavor intel",
|
||||
"ignoreFailures": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy"
|
||||
}
|
||||
3846
Cargo.lock
generated
80
Cargo.toml
@@ -1,32 +1,68 @@
|
||||
[package]
|
||||
name = "revolt"
|
||||
version = "0.2.9"
|
||||
# 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"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
mongodb = { version = "1.1.0-beta", default-features = false, features = ["sync"] } # FIXME: rewrite database with async API
|
||||
rocket = { version = "0.4.4", default-features = false }
|
||||
once_cell = "1.3.1"
|
||||
dotenv = "0.15.0"
|
||||
ulid = "0.3.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
rocket_contrib = "0.4.4"
|
||||
validator = "0.10.0"
|
||||
bcrypt = "0.7.0"
|
||||
chrono = "0.4.11"
|
||||
rand = "0.7.3"
|
||||
time = "0.2.9"
|
||||
reqwest = { version = "0.10.4", features = ["blocking", "json"] }
|
||||
num_enum = "0.4.3"
|
||||
ws = "0.9.1"
|
||||
hashbrown = "0.7.1"
|
||||
serde_json = "1.0.51"
|
||||
rocket_cors = "0.5.2"
|
||||
bitfield = "0.13.2"
|
||||
lru = "0.5.3"
|
||||
lazy_static = "1.4.0"
|
||||
# Utility
|
||||
lru = "0.7.0"
|
||||
url = "2.2.2"
|
||||
log = "0.4.11"
|
||||
dotenv = "0.15.0"
|
||||
dashmap = "5.2.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"
|
||||
|
||||
# ID / key generation
|
||||
ulid = "0.4.1"
|
||||
nanoid = "0.4.0"
|
||||
|
||||
# serde
|
||||
serde_json = "1.0.57"
|
||||
serde = { version = "1.0.115", features = ["derive"] }
|
||||
validator = { version = "0.14", features = ["derive"] }
|
||||
|
||||
# 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
|
||||
lettre = "0.10.0-alpha.4"
|
||||
rauth = { git = "https://github.com/insertish/rauth", rev = "001a9698c56cea79e69e4ae71d7bc2cb48aec1a6" }
|
||||
|
||||
# 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
|
||||
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] }
|
||||
mongodb = { version = "1.2.2", features = ["async-std-runtime"], default-features = false }
|
||||
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }
|
||||
|
||||
# spec generation
|
||||
schemars = "0.8.8"
|
||||
# rocket_okapi = "0.8.0-rc.1"
|
||||
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "dcf0df115596ee07a587a7a543cddf3d7944645b", features = [ "swagger" ] }
|
||||
|
||||
# quark
|
||||
revolt-quark = { git = "https://github.com/revoltchat/quark", rev = "076755e513aa6b1e955cf917f60aad9e62a73883" }
|
||||
# revolt-quark = { path = "../quark" }
|
||||
|
||||
22
Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Build Stage
|
||||
FROM rustlang/rust:nightly-slim AS builder
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
RUN USER=root cargo new --bin revolt
|
||||
WORKDIR /home/rust/src/revolt
|
||||
RUN apt-get update && apt-get install -y libssl-dev pkg-config
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY assets ./assets
|
||||
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 ./
|
||||
EXPOSE 8000
|
||||
ENV ROCKET_ADDRESS 0.0.0.0
|
||||
ENV ROCKET_PORT 8000
|
||||
CMD ["./revolt"]
|
||||
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
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.
|
||||
|
||||
Revolt Delta
|
||||
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
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
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/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
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/>.
|
||||
42
README.md
@@ -1,9 +1,45 @@
|
||||

|
||||
# Delta
|
||||
|
||||
Delta is a **blazing fast API server** built with Rust for the REVOLT platform.
|
||||
## Description
|
||||
|
||||
Delta is the API server for the Revolt platform.
|
||||
|
||||
**Features:**
|
||||
|
||||
Features:
|
||||
- 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.
|
||||
|
||||
## Stack
|
||||
|
||||
- [Rocket](https://rocket.rs/) (REST)
|
||||
- [Async Tungstenite](https://github.com/sdroege/async-tungstenite) (WebSockets)
|
||||
- [MongoDB](https://mongodb.com/)
|
||||
|
||||
## Resources
|
||||
|
||||
### Revolt
|
||||
|
||||
- [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)
|
||||
|
||||
## CLI Commands
|
||||
|
||||
| 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. |
|
||||
|
||||
## Contributing
|
||||
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
Delta is licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/delta/blob/master/LICENSE).
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB |
63
assets/templates/reset.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!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>
|
||||
5
assets/templates/reset.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
You requested a password reset, if you did not perform this action you can safely ignore this email.
|
||||
|
||||
Reset your password here: {{url}}
|
||||
|
||||
Sent by Revolt.
|
||||
14
assets/templates/reset_plain.html
Normal file
@@ -0,0 +1,14 @@
|
||||
<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>
|
||||
63
assets/templates/verify.html
Normal file
@@ -0,0 +1,63 @@
|
||||
<!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>
|
||||
6
assets/templates/verify.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
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.
|
||||
15
assets/templates/verify_plain.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<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>
|
||||
BIN
assets/user/1.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
assets/user/2.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/user/3.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
assets/user/4.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
assets/user/5.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/user/6.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/user/7.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
210
deny.toml
Normal file
@@ -0,0 +1,210 @@
|
||||
# This template contains all of the possible sections and their default values
|
||||
|
||||
# Note that all fields that take a lint level have these possible values:
|
||||
# * deny - An error will be produced and the check will fail
|
||||
# * warn - A warning will be produced, but the check will not fail
|
||||
# * allow - No warning or error will be produced, though in some cases a note
|
||||
# will be
|
||||
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
# dependency, such as, for example, the `nix` crate only being used via the
|
||||
# `target_family = "unix"` configuration, that only having windows targets in
|
||||
# this list would mean the nix crate, as well as any of its exclusive
|
||||
# dependencies not shared by any other crates, would be ignored, as the target
|
||||
# list here is effectively saying which targets you are building for.
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#{ triple = "x86_64-unknown-linux-musl" },
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory database is cloned/fetched into
|
||||
db-path = "~/.cargo/advisory-db"
|
||||
# The url(s) of the advisory databases to use
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# The lint level for security vulnerabilities
|
||||
vulnerability = "deny"
|
||||
# The lint level for unmaintained crates
|
||||
unmaintained = "warn"
|
||||
# The lint level for crates that have been yanked from their source registry
|
||||
yanked = "warn"
|
||||
# The lint level for crates with security notices. Note that as of
|
||||
# 2019-12-17 there are no security notice advisories in
|
||||
# https://github.com/rustsec/advisory-db
|
||||
notice = "warn"
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
# * None - CVSS Score 0.0
|
||||
# * Low - CVSS Score 0.1 - 3.9
|
||||
# * Medium - CVSS Score 4.0 - 6.9
|
||||
# * High - CVSS Score 7.0 - 8.9
|
||||
# * Critical - CVSS Score 9.0 - 10.0
|
||||
#severity-threshold =
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# The lint level for crates which do not have a detectable license
|
||||
unlicensed = "warn"
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
"MIT",
|
||||
"ISC",
|
||||
"0BSD",
|
||||
"CC0-1.0",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
#"Apache-2.0 WITH LLVM-exception",
|
||||
]
|
||||
# List of explicitly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
deny = [
|
||||
#"Nokia",
|
||||
]
|
||||
# Lint level for licenses considered copyleft
|
||||
copyleft = "warn"
|
||||
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
|
||||
# * both - The license will be approved if it is both OSI-approved *AND* FSF
|
||||
# * either - The license will be approved if it is either OSI-approved *OR* FSF
|
||||
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
|
||||
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
|
||||
# * neither - This predicate is ignored and the default lint level is used
|
||||
allow-osi-fsf-free = "neither"
|
||||
# Lint level used when no other predicates are matched
|
||||
# 1. License isn't in the allow or deny lists
|
||||
# 2. License isn't copyleft
|
||||
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
|
||||
default = "deny"
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.8
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The name of the crate the clarification applies to
|
||||
#name = "ring"
|
||||
# The optional version constraint for the crate
|
||||
#version = "*"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
|
||||
]
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite
|
||||
skip-tree = [
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = ["https://gitlab.insrt.uk/insert/hive"]
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
github = ["insertish"]
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
bitbucket = [""]
|
||||
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
database:
|
||||
image: mongo
|
||||
restart: always
|
||||
web:
|
||||
build: .
|
||||
environment:
|
||||
- REVOLT_MONGO_URI=mongodb://localhost
|
||||
- REVOLT_PUBLIC_URL=https://local.revolt.chat
|
||||
- REVOLT_USE_EMAIL_VERIFICATION=0
|
||||
- REVOLT_UNSAFE_NO_EMAIL=1
|
||||
- REVOLT_UNSAFE_NO_CAPTCHA=1
|
||||
ports:
|
||||
- "8000:8000"
|
||||
restart: unless-stopped
|
||||
7
publish.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
source set_version.sh
|
||||
|
||||
docker build -t revoltchat/server:${version} . &&
|
||||
docker tag revoltchat/server:${version} revoltchat/server:latest &&
|
||||
docker push revoltchat/server:${version} &&
|
||||
docker push revoltchat/server:latest
|
||||
3
set_version.sh
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
export version=0.5.3-5-patch.2
|
||||
echo "pub const VERSION: &str = \"${version}\";" > src/version.rs
|
||||
@@ -1,219 +0,0 @@
|
||||
use super::get_collection;
|
||||
|
||||
use lru::LruCache;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use mongodb::bson::{doc, from_bson, Bson};
|
||||
use rocket::http::RawStr;
|
||||
use rocket::request::FromParam;
|
||||
use rocket_contrib::json::JsonValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct LastMessage {
|
||||
// message id
|
||||
id: String,
|
||||
// author's id
|
||||
user_id: String,
|
||||
// truncated content with author's name prepended (for GDM / GUILD)
|
||||
short_content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Channel {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
#[serde(rename = "type")]
|
||||
pub channel_type: u8,
|
||||
|
||||
// DM: whether the DM is active
|
||||
pub active: Option<bool>,
|
||||
// DM + GDM: last message in channel
|
||||
pub last_message: Option<LastMessage>,
|
||||
// DM + GDM: recipients for channel
|
||||
pub recipients: Option<Vec<String>>,
|
||||
// GDM: owner of group
|
||||
pub owner: Option<String>,
|
||||
// GUILD: channel parent
|
||||
pub guild: Option<String>,
|
||||
// GUILD + GDM: channel name
|
||||
pub name: Option<String>,
|
||||
// GUILD + GDM: channel description
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn serialise(self) -> JsonValue {
|
||||
match self.channel_type {
|
||||
0 => json!({
|
||||
"id": self.id,
|
||||
"type": self.channel_type,
|
||||
"last_message": self.last_message,
|
||||
"recipients": self.recipients,
|
||||
}),
|
||||
1 => {
|
||||
json!({
|
||||
"id": self.id,
|
||||
"type": self.channel_type,
|
||||
"last_message": self.last_message,
|
||||
"recipients": self.recipients,
|
||||
"name": self.name,
|
||||
"owner": self.owner,
|
||||
"description": self.description,
|
||||
})
|
||||
}
|
||||
2 => {
|
||||
json!({
|
||||
"id": self.id,
|
||||
"type": self.channel_type,
|
||||
"guild": self.guild,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
})
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CACHE: Arc<Mutex<LruCache<String, Channel>>> =
|
||||
Arc::new(Mutex::new(LruCache::new(4_000_000)));
|
||||
}
|
||||
|
||||
pub fn fetch_channel(id: &str) -> Result<Option<Channel>, String> {
|
||||
{
|
||||
if let Ok(mut cache) = CACHE.lock() {
|
||||
let existing = cache.get(&id.to_string());
|
||||
|
||||
if let Some(channel) = existing {
|
||||
return Ok(Some((*channel).clone()));
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to lock cache.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let col = get_collection("channels");
|
||||
if let Ok(result) = col.find_one(doc! { "_id": id }, None) {
|
||||
if let Some(doc) = result {
|
||||
if let Ok(channel) = from_bson(Bson::Document(doc)) as Result<Channel, _> {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
cache.put(id.to_string(), channel.clone());
|
||||
|
||||
Ok(Some(channel))
|
||||
} else {
|
||||
Err("Failed to deserialize channel!".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err("Failed to fetch channel from database.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_channels(ids: &Vec<String>) -> Result<Vec<Channel>, String> {
|
||||
let mut missing = vec![];
|
||||
let mut channels = vec![];
|
||||
|
||||
{
|
||||
if let Ok(mut cache) = CACHE.lock() {
|
||||
for id in ids {
|
||||
let existing = cache.get(id);
|
||||
|
||||
if let Some(channel) = existing {
|
||||
channels.push((*channel).clone());
|
||||
} else {
|
||||
missing.push(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to lock cache.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if missing.len() == 0 {
|
||||
return Ok(channels);
|
||||
}
|
||||
|
||||
let col = get_collection("channels");
|
||||
if let Ok(result) = col.find(doc! { "_id": { "$in": missing } }, None) {
|
||||
for item in result {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
if let Ok(doc) = item {
|
||||
if let Ok(channel) = from_bson(Bson::Document(doc)) as Result<Channel, _> {
|
||||
cache.put(channel.id.clone(), channel.clone());
|
||||
channels.push(channel);
|
||||
} else {
|
||||
return Err("Failed to deserialize channel!".to_string());
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to fetch channel.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(channels)
|
||||
} else {
|
||||
Err("Failed to fetch channel from database.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> FromParam<'r> for Channel {
|
||||
type Error = &'r RawStr;
|
||||
|
||||
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
|
||||
if let Ok(result) = fetch_channel(param) {
|
||||
if let Some(channel) = result {
|
||||
Ok(channel)
|
||||
} else {
|
||||
Err(param)
|
||||
}
|
||||
} else {
|
||||
Err(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::notifications::events::Notification;
|
||||
|
||||
pub fn process_event(event: &Notification) {
|
||||
match event {
|
||||
Notification::group_user_join(ev) => {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
if let Some(channel) = cache.peek_mut(&ev.id) {
|
||||
channel.recipients.as_mut().unwrap().push(ev.user.clone());
|
||||
}
|
||||
}
|
||||
Notification::group_user_leave(ev) => {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
if let Some(channel) = cache.peek_mut(&ev.id) {
|
||||
let recipients = channel.recipients.as_mut().unwrap();
|
||||
if let Some(pos) = recipients.iter().position(|x| *x == ev.user) {
|
||||
recipients.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
Notification::guild_channel_create(ev) => {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
cache.put(
|
||||
ev.id.clone(),
|
||||
Channel {
|
||||
id: ev.channel.clone(),
|
||||
channel_type: 2,
|
||||
active: None,
|
||||
last_message: None,
|
||||
recipients: None,
|
||||
owner: None,
|
||||
guild: Some(ev.id.clone()),
|
||||
name: Some(ev.name.clone()),
|
||||
description: Some(ev.description.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
Notification::guild_channel_delete(ev) => {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
cache.pop(&ev.channel);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
use super::get_collection;
|
||||
use super::channel::fetch_channels;
|
||||
|
||||
use lru::LruCache;
|
||||
use mongodb::bson::{doc, from_bson, Bson};
|
||||
use rocket::http::RawStr;
|
||||
use rocket::request::FromParam;
|
||||
use rocket_contrib::json::JsonValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct MemberRef {
|
||||
pub guild: String,
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Member {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: MemberRef,
|
||||
pub nickname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Invite {
|
||||
pub code: String,
|
||||
pub creator: String,
|
||||
pub channel: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Ban {
|
||||
pub id: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Guild {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
// pub nonce: String, used internally
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub owner: String,
|
||||
|
||||
pub channels: Vec<String>,
|
||||
pub invites: Vec<Invite>,
|
||||
pub bans: Vec<Ban>,
|
||||
|
||||
pub default_permissions: u32,
|
||||
}
|
||||
|
||||
impl Guild {
|
||||
pub fn serialise(self) -> JsonValue {
|
||||
json!({
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"owner": self.owner
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fetch_channels(&self) -> Result<Vec<super::channel::Channel>, String> {
|
||||
super::channel::fetch_channels(&self.channels)
|
||||
}
|
||||
|
||||
pub fn seralise_with_channels(self) -> Result<JsonValue, String> {
|
||||
let channels = self.fetch_channels()?
|
||||
.into_iter()
|
||||
.map(|x| x.serialise())
|
||||
.collect();
|
||||
|
||||
let mut value = self.serialise();
|
||||
value.as_object_mut().unwrap().insert("channels".to_string(), channels);
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Hash, Eq, PartialEq)]
|
||||
pub struct MemberKey(pub String, pub String);
|
||||
|
||||
lazy_static! {
|
||||
static ref CACHE: Arc<Mutex<LruCache<String, Guild>>> =
|
||||
Arc::new(Mutex::new(LruCache::new(4_000_000)));
|
||||
static ref MEMBER_CACHE: Arc<Mutex<LruCache<MemberKey, Member>>> =
|
||||
Arc::new(Mutex::new(LruCache::new(4_000_000)));
|
||||
}
|
||||
|
||||
pub fn fetch_guild(id: &str) -> Result<Option<Guild>, String> {
|
||||
{
|
||||
if let Ok(mut cache) = CACHE.lock() {
|
||||
let existing = cache.get(&id.to_string());
|
||||
|
||||
if let Some(guild) = existing {
|
||||
return Ok(Some((*guild).clone()));
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to lock cache.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let col = get_collection("guilds");
|
||||
if let Ok(result) = col.find_one(doc! { "_id": id }, None) {
|
||||
if let Some(doc) = result {
|
||||
if let Ok(guild) = from_bson(Bson::Document(doc)) as Result<Guild, _> {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
cache.put(id.to_string(), guild.clone());
|
||||
|
||||
Ok(Some(guild))
|
||||
} else {
|
||||
Err("Failed to deserialize guild!".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err("Failed to fetch guild from database.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_guilds(ids: &Vec<String>) -> Result<Vec<Guild>, String> {
|
||||
let mut missing = vec![];
|
||||
let mut guilds = vec![];
|
||||
|
||||
{
|
||||
if let Ok(mut cache) = CACHE.lock() {
|
||||
for id in ids {
|
||||
let existing = cache.get(id);
|
||||
|
||||
if let Some(guild) = existing {
|
||||
guilds.push((*guild).clone());
|
||||
} else {
|
||||
missing.push(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to lock cache.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if missing.len() == 0 {
|
||||
return Ok(guilds);
|
||||
}
|
||||
|
||||
let col = get_collection("guilds");
|
||||
if let Ok(result) = col.find(doc! { "_id": { "$in": missing } }, None) {
|
||||
for item in result {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
if let Ok(doc) = item {
|
||||
if let Ok(guild) = from_bson(Bson::Document(doc)) as Result<Guild, _> {
|
||||
cache.put(guild.id.clone(), guild.clone());
|
||||
guilds.push(guild);
|
||||
} else {
|
||||
return Err("Failed to deserialize guild!".to_string());
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to fetch guild.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(guilds)
|
||||
} else {
|
||||
Err("Failed to fetch channel from database.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialise_guilds_with_channels(ids: &Vec<String>) -> Result<Vec<JsonValue>, String> {
|
||||
let guilds = fetch_guilds(&ids)?;
|
||||
let cids: Vec<String> = guilds
|
||||
.iter()
|
||||
.flat_map(|x| x.channels.clone())
|
||||
.collect();
|
||||
|
||||
let channels = fetch_channels(&cids)?;
|
||||
Ok(guilds
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let id = x.id.clone();
|
||||
let mut obj = x.serialise();
|
||||
obj.as_object_mut().unwrap().insert(
|
||||
"channels".to_string(),
|
||||
channels.iter()
|
||||
.filter(|x| x.guild.is_some() && x.guild.as_ref().unwrap() == &id)
|
||||
.map(|x| x.clone().serialise())
|
||||
.collect()
|
||||
);
|
||||
obj
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn fetch_member(key: MemberKey) -> Result<Option<Member>, String> {
|
||||
{
|
||||
if let Ok(mut cache) = MEMBER_CACHE.lock() {
|
||||
let existing = cache.get(&key);
|
||||
|
||||
if let Some(member) = existing {
|
||||
return Ok(Some((*member).clone()));
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to lock cache.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let col = get_collection("members");
|
||||
if let Ok(result) = col.find_one(
|
||||
doc! {
|
||||
"_id.guild": &key.0,
|
||||
"_id.user": &key.1,
|
||||
},
|
||||
None,
|
||||
) {
|
||||
if let Some(doc) = result {
|
||||
if let Ok(member) = from_bson(Bson::Document(doc)) as Result<Member, _> {
|
||||
let mut cache = MEMBER_CACHE.lock().unwrap();
|
||||
cache.put(key, member.clone());
|
||||
|
||||
Ok(Some(member))
|
||||
} else {
|
||||
Err("Failed to deserialize member!".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err("Failed to fetch member from database.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> FromParam<'r> for Guild {
|
||||
type Error = &'r RawStr;
|
||||
|
||||
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
|
||||
if let Ok(result) = fetch_guild(param) {
|
||||
if let Some(channel) = result {
|
||||
Ok(channel)
|
||||
} else {
|
||||
Err(param)
|
||||
}
|
||||
} else {
|
||||
Err(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_invite<U: Into<Option<String>>>(
|
||||
code: &String,
|
||||
user: U,
|
||||
) -> Option<(String, String, Invite)> {
|
||||
let mut doc = doc! {
|
||||
"invites": {
|
||||
"$elemMatch": {
|
||||
"code": &code
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(user_id) = user.into() {
|
||||
doc.insert(
|
||||
"bans",
|
||||
doc! {
|
||||
"$not": {
|
||||
"$elemMatch": {
|
||||
"id": user_id
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if let Ok(result) = get_collection("guilds").find_one(
|
||||
doc,
|
||||
mongodb::options::FindOneOptions::builder()
|
||||
.projection(doc! {
|
||||
"_id": 1,
|
||||
"name": 1,
|
||||
"invites.$": 1,
|
||||
})
|
||||
.build(),
|
||||
) {
|
||||
if let Some(doc) = result {
|
||||
let invite = doc
|
||||
.get_array("invites")
|
||||
.unwrap()
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.as_document()
|
||||
.unwrap();
|
||||
|
||||
Some((
|
||||
doc.get_str("_id").unwrap().to_string(),
|
||||
doc.get_str("name").unwrap().to_string(),
|
||||
from_bson(Bson::Document(invite.clone())).unwrap(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
use crate::notifications::events::Notification;
|
||||
|
||||
pub fn process_event(event: &Notification) {
|
||||
match event {
|
||||
Notification::guild_channel_create(_ev) => {} // ? for later use
|
||||
Notification::guild_channel_delete(_ev) => {} // ? for later use
|
||||
Notification::guild_delete(ev) => {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
cache.pop(&ev.id);
|
||||
}
|
||||
Notification::guild_user_join(ev) => {
|
||||
let mut cache = MEMBER_CACHE.lock().unwrap();
|
||||
cache.put(
|
||||
MemberKey ( ev.id.clone(), ev.user.clone() ),
|
||||
Member {
|
||||
id: MemberRef {
|
||||
guild: ev.id.clone(),
|
||||
user: ev.user.clone()
|
||||
},
|
||||
nickname: None
|
||||
}
|
||||
);
|
||||
}
|
||||
Notification::guild_user_leave(ev) => {
|
||||
let mut cache = MEMBER_CACHE.lock().unwrap();
|
||||
cache.pop(&MemberKey ( ev.id.clone(), ev.user.clone() ));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
use super::get_collection;
|
||||
use crate::database::channel::Channel;
|
||||
use crate::notifications;
|
||||
use crate::notifications::events::message::Create;
|
||||
use crate::notifications::events::Notification;
|
||||
use crate::routes::channel::ChannelType;
|
||||
|
||||
use mongodb::bson::from_bson;
|
||||
use mongodb::bson::{doc, to_bson, Bson, DateTime};
|
||||
use rocket::http::RawStr;
|
||||
use rocket::request::FromParam;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PreviousEntry {
|
||||
pub content: String,
|
||||
pub time: DateTime,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Message {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
pub nonce: Option<String>,
|
||||
pub channel: String,
|
||||
pub author: String,
|
||||
|
||||
pub content: String,
|
||||
pub edited: Option<DateTime>,
|
||||
|
||||
pub previous_content: Vec<PreviousEntry>,
|
||||
}
|
||||
|
||||
// ? TODO: write global send message
|
||||
// ? pub fn send_message();
|
||||
// ? handle websockets?
|
||||
impl Message {
|
||||
pub fn send(&self, target: &Channel) -> bool {
|
||||
if get_collection("messages")
|
||||
.insert_one(to_bson(&self).unwrap().as_document().unwrap().clone(), None)
|
||||
.is_ok()
|
||||
{
|
||||
notifications::send_message_given_channel(
|
||||
Notification::message_create(Create {
|
||||
id: self.id.clone(),
|
||||
nonce: self.nonce.clone(),
|
||||
channel: self.channel.clone(),
|
||||
author: self.author.clone(),
|
||||
content: self.content.clone(),
|
||||
}),
|
||||
&target,
|
||||
);
|
||||
|
||||
let short_content: String = self.content.chars().take(24).collect();
|
||||
|
||||
// !! this stuff can be async
|
||||
if target.channel_type == ChannelType::DM as u8
|
||||
|| target.channel_type == ChannelType::GROUPDM as u8
|
||||
{
|
||||
let mut update = doc! {
|
||||
"$set": {
|
||||
"last_message": {
|
||||
"id": &self.id,
|
||||
"user_id": &self.author,
|
||||
"short_content": short_content,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if target.channel_type == ChannelType::DM as u8 {
|
||||
update
|
||||
.get_document_mut("$set")
|
||||
.unwrap()
|
||||
.insert("active", true);
|
||||
}
|
||||
|
||||
if get_collection("channels")
|
||||
.update_one(doc! { "_id": &target.id }, update, None)
|
||||
.is_ok()
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> FromParam<'r> for Message {
|
||||
type Error = &'r RawStr;
|
||||
|
||||
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
|
||||
let col = get_collection("messages");
|
||||
let result = col
|
||||
.find_one(doc! { "_id": param.to_string() }, None)
|
||||
.unwrap();
|
||||
|
||||
if let Some(message) = result {
|
||||
Ok(from_bson(Bson::Document(message)).expect("Failed to unwrap message."))
|
||||
} else {
|
||||
Err(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use super::super::get_db;
|
||||
use super::scripts::LATEST_REVISION;
|
||||
|
||||
use mongodb::options::CreateCollectionOptions;
|
||||
use mongodb::bson::doc;
|
||||
use log::info;
|
||||
|
||||
pub fn create_database() {
|
||||
info!("Creating database.");
|
||||
let db = get_db();
|
||||
|
||||
db.create_collection("users", None).expect("Failed to create users collection.");
|
||||
db.create_collection("channels", None).expect("Failed to create channels collection.");
|
||||
db.create_collection("guilds", None).expect("Failed to create guilds collection.");
|
||||
db.create_collection("members", None).expect("Failed to create members collection.");
|
||||
db.create_collection("messages", None).expect("Failed to create messages collection.");
|
||||
db.create_collection("migrations", None).expect("Failed to create migrations collection.");
|
||||
|
||||
db.create_collection(
|
||||
"pubsub",
|
||||
CreateCollectionOptions::builder()
|
||||
.capped(true)
|
||||
.size(1_000_000)
|
||||
.build()
|
||||
)
|
||||
.expect("Failed to create pubsub collection.");
|
||||
|
||||
db.collection("migrations")
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": 0,
|
||||
"revision": LATEST_REVISION
|
||||
},
|
||||
None
|
||||
)
|
||||
.expect("Failed to save migration info.");
|
||||
|
||||
info!("Created database.");
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
use super::get_connection;
|
||||
|
||||
pub mod init;
|
||||
pub mod scripts;
|
||||
|
||||
pub fn run_migrations() {
|
||||
let client = get_connection();
|
||||
|
||||
let list = client.list_database_names(
|
||||
None,
|
||||
None
|
||||
).expect("Failed to fetch database names.");
|
||||
|
||||
if list.iter().position(|x| x == "revolt").is_none() {
|
||||
init::create_database();
|
||||
} else {
|
||||
scripts::migrate_database();
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
use super::super::get_collection;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use mongodb::bson::{Bson, from_bson, doc};
|
||||
use mongodb::options::FindOptions;
|
||||
use log::info;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MigrationInfo {
|
||||
_id: i32,
|
||||
revision: i32
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 2;
|
||||
|
||||
pub fn migrate_database() {
|
||||
let migrations = get_collection("migrations");
|
||||
let data = migrations.find_one(None, None)
|
||||
.expect("Failed to fetch migration data.");
|
||||
|
||||
if let Some(doc) = data {
|
||||
let info: MigrationInfo = from_bson(Bson::Document(doc))
|
||||
.expect("Failed to read migration information.");
|
||||
|
||||
let revision = run_migrations(info.revision);
|
||||
|
||||
migrations.update_one(
|
||||
doc! {
|
||||
"_id": info._id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"revision": revision
|
||||
}
|
||||
},
|
||||
None
|
||||
).expect("Failed to commit migration information.");
|
||||
|
||||
info!("Migration complete. Currently at revision {}.", revision);
|
||||
} else {
|
||||
panic!("Database was configured incorrectly, possibly because initalization failed.")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_migrations(revision: i32) -> i32 {
|
||||
info!("Starting database migration.");
|
||||
|
||||
if revision <= 0 {
|
||||
info!("Running migration [revision 0]: Test migration system.");
|
||||
}
|
||||
|
||||
if revision <= 1 {
|
||||
info!("Running migration [revision 1]: Add channels to guild object.");
|
||||
|
||||
let col = get_collection("guilds");
|
||||
let guilds = col.find(
|
||||
None,
|
||||
FindOptions::builder()
|
||||
.projection(doc! { "_id": 1 })
|
||||
.build()
|
||||
)
|
||||
.expect("Failed to fetch guilds.");
|
||||
|
||||
let result = get_collection("channels").find(
|
||||
doc! {
|
||||
"type": 2
|
||||
},
|
||||
FindOptions::builder()
|
||||
.projection(doc! { "_id": 1, "guild": 1 })
|
||||
.build()
|
||||
).expect("Failed to fetch channels.");
|
||||
|
||||
let mut channels = vec![];
|
||||
for doc in result {
|
||||
let channel = doc.expect("Failed to fetch channel.");
|
||||
let id = channel.get_str("_id").expect("Failed to get channel id.").to_string();
|
||||
let gid = channel.get_str("guild").expect("Failed to get guild id.").to_string();
|
||||
|
||||
channels.push(( id, gid ));
|
||||
}
|
||||
|
||||
for doc in guilds {
|
||||
let guild = doc.expect("Failed to fetch guild.");
|
||||
let id = guild.get_str("_id").expect("Failed to get guild id.");
|
||||
|
||||
let list: Vec<String> = channels
|
||||
.iter()
|
||||
.filter(|x| x.1 == id)
|
||||
.map(|x| x.0.clone())
|
||||
.collect();
|
||||
|
||||
col.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"channels": list
|
||||
}
|
||||
},
|
||||
None
|
||||
).expect("Failed to update guild.");
|
||||
}
|
||||
}
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
LATEST_REVISION
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use mongodb::sync::{Client, Collection, Database};
|
||||
use std::env;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
static DBCONN: OnceCell<Client> = OnceCell::new();
|
||||
|
||||
pub fn connect() {
|
||||
let client =
|
||||
Client::with_uri_str(&env::var("DB_URI").expect("DB_URI not in environment variables!"))
|
||||
.expect("Failed to init db connection.");
|
||||
|
||||
DBCONN.set(client).unwrap();
|
||||
migrations::run_migrations();
|
||||
}
|
||||
|
||||
pub fn get_connection() -> &'static Client {
|
||||
DBCONN.get().unwrap()
|
||||
}
|
||||
|
||||
pub fn get_db() -> Database {
|
||||
get_connection().database("revolt")
|
||||
}
|
||||
|
||||
pub fn get_collection(collection: &str) -> Collection {
|
||||
get_db().collection(collection)
|
||||
}
|
||||
|
||||
pub mod migrations;
|
||||
|
||||
pub mod channel;
|
||||
pub mod guild;
|
||||
pub mod message;
|
||||
pub mod mutual;
|
||||
pub mod permissions;
|
||||
pub mod user;
|
||||
|
||||
pub use permissions::*;
|
||||
@@ -1,131 +0,0 @@
|
||||
use super::{get_collection, MemberPermissions};
|
||||
|
||||
use mongodb::bson::doc;
|
||||
use mongodb::options::FindOptions;
|
||||
|
||||
pub fn find_mutual_guilds(user_id: &str, target_id: &str) -> Vec<String> {
|
||||
let col = get_collection("members");
|
||||
if let Ok(result) = col.find(
|
||||
doc! {
|
||||
"$and": [
|
||||
{ "id": user_id },
|
||||
{ "id": target_id },
|
||||
]
|
||||
},
|
||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
||||
) {
|
||||
let mut results = vec![];
|
||||
|
||||
for doc in result {
|
||||
if let Ok(guild) = doc {
|
||||
results.push(guild.get_str("_id").unwrap().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_mutual_friends(user_id: &str, target_id: &str) -> Vec<String> {
|
||||
let col = get_collection("users");
|
||||
if let Ok(result) = col.find(
|
||||
doc! {
|
||||
"$and": [
|
||||
{ "relations": { "$elemMatch": { "id": user_id, "status": 0 } } },
|
||||
{ "relations": { "$elemMatch": { "id": target_id, "status": 0 } } },
|
||||
]
|
||||
},
|
||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
||||
) {
|
||||
let mut results = vec![];
|
||||
|
||||
for doc in result {
|
||||
if let Ok(user) = doc {
|
||||
results.push(user.get_str("_id").unwrap().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_mutual_groups(user_id: &str, target_id: &str) -> Vec<String> {
|
||||
let col = get_collection("channels");
|
||||
if let Ok(result) = col.find(
|
||||
doc! {
|
||||
"type": 1,
|
||||
"$and": [
|
||||
{ "recipients": user_id },
|
||||
{ "recipients": target_id },
|
||||
]
|
||||
},
|
||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
||||
) {
|
||||
let mut results = vec![];
|
||||
|
||||
for doc in result {
|
||||
if let Ok(group) = doc {
|
||||
results.push(group.get_str("_id").unwrap().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_mutual_connection(user_id: &str, target_id: &str, with_permission: bool) -> bool {
|
||||
let mut doc = doc! { "_id": 1 };
|
||||
|
||||
if with_permission {
|
||||
doc.insert("default_permissions", 1);
|
||||
}
|
||||
|
||||
let opt = FindOptions::builder().projection(doc);
|
||||
|
||||
if let Ok(result) = get_collection("guilds").find(
|
||||
doc! {
|
||||
"$and": [
|
||||
{ "members": { "$elemMatch": { "id": user_id } } },
|
||||
{ "members": { "$elemMatch": { "id": target_id } } },
|
||||
]
|
||||
},
|
||||
if with_permission {
|
||||
opt.build()
|
||||
} else {
|
||||
opt.limit(1).build()
|
||||
},
|
||||
) {
|
||||
if with_permission {
|
||||
for item in result {
|
||||
// ? logic should match permissions.rs#calculate
|
||||
if let Ok(guild) = item {
|
||||
if guild.get_str("owner").unwrap() == user_id {
|
||||
return true;
|
||||
}
|
||||
|
||||
let permissions = guild.get_i32("default_permissions").unwrap() as u32;
|
||||
|
||||
if MemberPermissions([permissions]).get_send_direct_messages() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
} else {
|
||||
if result.count() > 0 {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
use super::mutual::has_mutual_connection;
|
||||
use crate::database::channel::Channel;
|
||||
use crate::database::guild::{fetch_guild, fetch_member, Guild, Member, MemberKey};
|
||||
use crate::database::user::{User, UserRelationship};
|
||||
|
||||
use num_enum::TryFromPrimitive;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Relationship {
|
||||
Friend = 0,
|
||||
Outgoing = 1,
|
||||
Incoming = 2,
|
||||
Blocked = 3,
|
||||
BlockedOther = 4,
|
||||
NONE = 5,
|
||||
SELF = 6,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
|
||||
#[repr(u32)]
|
||||
pub enum Permission {
|
||||
Access = 1,
|
||||
CreateInvite = 2,
|
||||
KickMembers = 4,
|
||||
BanMembers = 8,
|
||||
ReadMessages = 16,
|
||||
SendMessages = 32,
|
||||
ManageMessages = 64,
|
||||
ManageChannels = 128,
|
||||
ManageServer = 256,
|
||||
ManageRoles = 512,
|
||||
SendDirectMessages = 1024,
|
||||
}
|
||||
|
||||
bitfield! {
|
||||
pub struct MemberPermissions(MSB0 [u32]);
|
||||
u32;
|
||||
pub get_access, set_access: 31;
|
||||
pub get_create_invite, set_create_invite: 30;
|
||||
pub get_kick_members, set_kick_members: 29;
|
||||
pub get_ban_members, set_ban_members: 28;
|
||||
pub get_read_messages, set_read_messages: 27;
|
||||
pub get_send_messages, set_send_messages: 26;
|
||||
pub get_manage_messages, set_manage_messages: 25;
|
||||
pub get_manage_channels, set_manage_channels: 24;
|
||||
pub get_manage_server, set_manage_server: 23;
|
||||
pub get_manage_roles, set_manage_roles: 22;
|
||||
pub get_send_direct_messages, set_send_direct_messages: 21;
|
||||
}
|
||||
|
||||
pub fn get_relationship_internal(
|
||||
user_id: &str,
|
||||
target_id: &str,
|
||||
relationships: &Option<Vec<UserRelationship>>,
|
||||
) -> Relationship {
|
||||
if user_id == target_id {
|
||||
return Relationship::SELF;
|
||||
}
|
||||
|
||||
if let Some(arr) = &relationships {
|
||||
for entry in arr {
|
||||
if entry.id == target_id {
|
||||
match entry.status {
|
||||
0 => return Relationship::Friend,
|
||||
1 => return Relationship::Outgoing,
|
||||
2 => return Relationship::Incoming,
|
||||
3 => return Relationship::Blocked,
|
||||
4 => return Relationship::BlockedOther,
|
||||
_ => return Relationship::NONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Relationship::NONE
|
||||
}
|
||||
|
||||
pub fn get_relationship(a: &User, b: &User) -> Relationship {
|
||||
if a.id == b.id {
|
||||
return Relationship::SELF;
|
||||
}
|
||||
|
||||
get_relationship_internal(&a.id, &b.id, &a.relations)
|
||||
}
|
||||
|
||||
pub struct PermissionCalculator {
|
||||
pub user: User,
|
||||
pub channel: Option<Channel>,
|
||||
pub guild: Option<Guild>,
|
||||
pub member: Option<Member>,
|
||||
}
|
||||
|
||||
impl PermissionCalculator {
|
||||
pub fn new(user: User) -> PermissionCalculator {
|
||||
PermissionCalculator {
|
||||
user,
|
||||
channel: None,
|
||||
guild: None,
|
||||
member: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel(self, channel: Channel) -> PermissionCalculator {
|
||||
PermissionCalculator {
|
||||
channel: Some(channel),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guild(self, guild: Guild) -> PermissionCalculator {
|
||||
PermissionCalculator {
|
||||
guild: Some(guild),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_data(mut self) -> PermissionCalculator {
|
||||
let guild = if let Some(value) = self.guild {
|
||||
Some(value)
|
||||
} else if let Some(channel) = &self.channel {
|
||||
match channel.channel_type {
|
||||
0..=1 => None,
|
||||
2 => {
|
||||
if let Some(id) = &channel.guild {
|
||||
if let Ok(result) = fetch_guild(id) {
|
||||
result
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(guild) = &guild {
|
||||
if let Ok(result) = fetch_member(MemberKey(guild.id.clone(), self.user.id.clone())) {
|
||||
self.member = result;
|
||||
}
|
||||
}
|
||||
|
||||
self.guild = guild;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn calculate(&self) -> u32 {
|
||||
let mut permissions: u32 = 0;
|
||||
if let Some(guild) = &self.guild {
|
||||
if let Some(_member) = &self.member {
|
||||
// ? logic should match mutual.rs#has_mutual_connection
|
||||
if guild.owner == self.user.id {
|
||||
return u32::MAX;
|
||||
}
|
||||
|
||||
permissions = guild.default_permissions as u32;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(channel) = &self.channel {
|
||||
match channel.channel_type {
|
||||
0 => {
|
||||
if let Some(arr) = &channel.recipients {
|
||||
let mut other_user = None;
|
||||
for item in arr {
|
||||
if item != &self.user.id {
|
||||
other_user = Some(item);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(other) = other_user {
|
||||
let relationship =
|
||||
get_relationship_internal(&self.user.id, &other, &self.user.relations);
|
||||
|
||||
if relationship == Relationship::Friend {
|
||||
permissions = 1024 + 128 + 32 + 16 + 1;
|
||||
} else if relationship == Relationship::Blocked
|
||||
|| relationship == Relationship::BlockedOther
|
||||
{
|
||||
permissions = 1;
|
||||
} else if has_mutual_connection(&self.user.id, other, true) {
|
||||
permissions = 1024 + 128 + 32 + 16 + 1;
|
||||
} else {
|
||||
permissions = 1;
|
||||
}
|
||||
} else {
|
||||
// ? In this case, it is a "self DM".
|
||||
return 1024 + 128 + 32 + 16 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if let Some(id) = &channel.owner {
|
||||
if &self.user.id == id {
|
||||
return u32::MAX;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arr) = &channel.recipients {
|
||||
for item in arr {
|
||||
if item == &self.user.id {
|
||||
permissions = 177;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
// nothing implemented yet
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
permissions
|
||||
}
|
||||
|
||||
pub fn as_permission(&self) -> MemberPermissions<[u32; 1]> {
|
||||
MemberPermissions([self.calculate()])
|
||||
}
|
||||
}
|
||||
@@ -1,318 +0,0 @@
|
||||
use super::get_collection;
|
||||
use super::guild::{serialise_guilds_with_channels};
|
||||
use super::channel::{fetch_channels};
|
||||
|
||||
use lru::LruCache;
|
||||
use mongodb::bson::{doc, from_bson, Bson, DateTime};
|
||||
use mongodb::options::FindOptions;
|
||||
use rocket::http::{RawStr, Status};
|
||||
use rocket::request::{self, FromParam, FromRequest, Request};
|
||||
use rocket_contrib::json::JsonValue;
|
||||
use rocket::Outcome;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UserEmailVerification {
|
||||
pub verified: bool,
|
||||
pub target: Option<String>,
|
||||
pub expiry: Option<DateTime>,
|
||||
pub rate_limit: Option<DateTime>,
|
||||
pub code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UserRelationship {
|
||||
pub id: String,
|
||||
pub status: u8,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct User {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub display_name: String,
|
||||
pub access_token: Option<String>,
|
||||
pub email_verification: UserEmailVerification,
|
||||
pub relations: Option<Vec<UserRelationship>>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn serialise(self, relationship: i32) -> JsonValue {
|
||||
if relationship == super::Relationship::SELF as i32 {
|
||||
json!({
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"display_name": self.display_name,
|
||||
"email": self.email,
|
||||
"verified": self.email_verification.verified,
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"display_name": self.display_name,
|
||||
"relationship": relationship
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_guilds(&self) -> Result<Vec<String>, String> {
|
||||
let members = get_collection("members")
|
||||
.find(
|
||||
doc! {
|
||||
"_id.user": &self.id
|
||||
},
|
||||
None
|
||||
).map_err(|_| "Failed to fetch members.")?;
|
||||
|
||||
Ok(members.into_iter()
|
||||
.filter_map(|x| match x {
|
||||
Ok(doc) => {
|
||||
match doc.get_document("_id") {
|
||||
Ok(id) => {
|
||||
match id.get_str("guild") {
|
||||
Ok(value) => Some(value.to_string()),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
Err(_) => None
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn find_dms(&self) -> Result<Vec<String>, String> {
|
||||
let channels = get_collection("channels")
|
||||
.find(
|
||||
doc! {
|
||||
"recipients": &self.id
|
||||
},
|
||||
FindOptions::builder()
|
||||
.projection(doc! { "_id": 1 })
|
||||
.build()
|
||||
).map_err(|_| "Failed to fetch channel ids.")?;
|
||||
|
||||
Ok(channels.into_iter()
|
||||
.filter_map(|x| x.ok())
|
||||
.filter_map(|x| {
|
||||
match x.get_str("_id") {
|
||||
Ok(value) => Some(value.to_string()),
|
||||
Err(_) => None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn create_payload(self) -> Result<JsonValue, String> {
|
||||
let v = vec![];
|
||||
let relations = self.relations.as_ref().unwrap_or(&v);
|
||||
|
||||
let users: Vec<JsonValue> = fetch_users(
|
||||
&relations
|
||||
.iter()
|
||||
.map(|x| x.id.clone())
|
||||
.collect()
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
let id = x.id.clone();
|
||||
x.serialise(
|
||||
relations.iter()
|
||||
.find(|y| y.id == id)
|
||||
.unwrap()
|
||||
.status as i32
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let channels: Vec<JsonValue> = fetch_channels(&self.find_dms()?)?
|
||||
.into_iter()
|
||||
.map(|x| x.serialise())
|
||||
.collect();
|
||||
|
||||
Ok(json!({
|
||||
"users": users,
|
||||
"channels": channels,
|
||||
"guilds": serialise_guilds_with_channels(&self.find_guilds()?)?,
|
||||
"user": self.serialise(super::Relationship::SELF as i32)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CACHE: Arc<Mutex<LruCache<String, User>>> =
|
||||
Arc::new(Mutex::new(LruCache::new(4_000_000)));
|
||||
}
|
||||
|
||||
pub fn fetch_user(id: &str) -> Result<Option<User>, String> {
|
||||
{
|
||||
if let Ok(mut cache) = CACHE.lock() {
|
||||
let existing = cache.get(&id.to_string());
|
||||
|
||||
if let Some(user) = existing {
|
||||
return Ok(Some((*user).clone()));
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to lock cache.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let col = get_collection("users");
|
||||
if let Ok(result) = col.find_one(doc! { "_id": id }, None) {
|
||||
if let Some(doc) = result {
|
||||
if let Ok(user) = from_bson(Bson::Document(doc)) as Result<User, _> {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
cache.put(id.to_string(), user.clone());
|
||||
|
||||
Ok(Some(user))
|
||||
} else {
|
||||
Err("Failed to deserialize user!".to_string())
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Err("Failed to fetch user from database.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_users(ids: &Vec<String>) -> Result<Vec<User>, String> {
|
||||
let mut missing = vec![];
|
||||
let mut users = vec![];
|
||||
|
||||
{
|
||||
if let Ok(mut cache) = CACHE.lock() {
|
||||
for id in ids {
|
||||
let existing = cache.get(id);
|
||||
|
||||
if let Some(user) = existing {
|
||||
users.push((*user).clone());
|
||||
} else {
|
||||
missing.push(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to lock cache.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if missing.len() == 0 {
|
||||
return Ok(users);
|
||||
}
|
||||
|
||||
let col = get_collection("users");
|
||||
if let Ok(result) = col.find(doc! { "_id": { "$in": missing } }, None) {
|
||||
for item in result {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
if let Ok(doc) = item {
|
||||
if let Ok(user) = from_bson(Bson::Document(doc)) as Result<User, _> {
|
||||
cache.put(user.id.clone(), user.clone());
|
||||
users.push(user);
|
||||
} else {
|
||||
return Err("Failed to deserialize user!".to_string());
|
||||
}
|
||||
} else {
|
||||
return Err("Failed to fetch user.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(users)
|
||||
} else {
|
||||
Err("Failed to fetch user from database.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthError {
|
||||
Failed,
|
||||
Missing,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl<'a, 'r> FromRequest<'a, 'r> for User {
|
||||
type Error = AuthError;
|
||||
|
||||
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
|
||||
let u = request.headers().get("x-user").next();
|
||||
let t = request.headers().get("x-auth-token").next();
|
||||
|
||||
if let Some(uid) = u {
|
||||
if let Some(token) = t {
|
||||
if let Ok(result) = fetch_user(uid) {
|
||||
if let Some(user) = result {
|
||||
if let Some(access_token) = &user.access_token {
|
||||
if access_token == token {
|
||||
Outcome::Success(user)
|
||||
} else {
|
||||
Outcome::Failure((Status::Forbidden, AuthError::Invalid))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((Status::Forbidden, AuthError::Invalid))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((Status::Forbidden, AuthError::Invalid))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((Status::Forbidden, AuthError::Failed))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((Status::Forbidden, AuthError::Missing))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((Status::Forbidden, AuthError::Missing))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> FromParam<'r> for User {
|
||||
type Error = &'r RawStr;
|
||||
|
||||
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
|
||||
if let Ok(result) = fetch_user(¶m.to_string()) {
|
||||
if let Some(user) = result {
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(param)
|
||||
}
|
||||
} else {
|
||||
Err(param)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use crate::notifications::events::Notification;
|
||||
|
||||
pub fn process_event(event: &Notification) {
|
||||
match event {
|
||||
Notification::user_friend_status(ev) => {
|
||||
let mut cache = CACHE.lock().unwrap();
|
||||
if let Some(user) = cache.peek_mut(&ev.id) {
|
||||
if let Some(relations) = user.relations.as_mut() {
|
||||
if ev.status == 0 {
|
||||
if let Some(pos) = relations.iter().position(|x| x.id == ev.user) {
|
||||
relations.remove(pos);
|
||||
}
|
||||
} else {
|
||||
if let Some(entry) = relations.iter_mut().find(|x| x.id == ev.user) {
|
||||
entry.status = ev.status as u8;
|
||||
} else {
|
||||
relations.push(
|
||||
UserRelationship {
|
||||
id: ev.id.clone(),
|
||||
status: ev.status as u8
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
61
src/email.rs
@@ -1,61 +0,0 @@
|
||||
use reqwest::blocking::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
fn public_uri() -> String {
|
||||
env::var("PUBLIC_URI").expect("PUBLIC_URI not in environment variables!")
|
||||
}
|
||||
|
||||
fn portal() -> String {
|
||||
env::var("PORTAL_URL").expect("PORTAL_URL not in environment variables!")
|
||||
}
|
||||
|
||||
pub fn send_email(target: String, subject: String, body: String, html: String) -> Result<(), ()> {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("target", target.clone());
|
||||
map.insert("subject", subject);
|
||||
map.insert("body", body);
|
||||
map.insert("html", html);
|
||||
|
||||
let client = Client::new();
|
||||
match client.post(&portal()).json(&map).send() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_verification_email(email: String, code: String) -> bool {
|
||||
let url = format!("{}/api/account/verify/{}", public_uri(), code);
|
||||
send_email(
|
||||
email,
|
||||
"Verify your email!".to_string(),
|
||||
format!("Verify your email here: {}", url),
|
||||
format!("<a href=\"{}\">Click to verify your email!</a>", url),
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn send_password_reset(email: String, code: String) -> bool {
|
||||
let url = format!("{}/api/account/reset/{}", public_uri(), code);
|
||||
send_email(
|
||||
email,
|
||||
"Reset your password.".to_string(),
|
||||
format!("Reset your password here: {}", url),
|
||||
format!("<a href=\"{}\">Click to reset your password!</a>", url),
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn send_welcome_email(email: String, username: String) -> bool {
|
||||
send_email(
|
||||
email,
|
||||
"Welcome to REVOLT!".to_string(),
|
||||
format!("Welcome, {}! You can now use REVOLT.", username.clone()),
|
||||
format!(
|
||||
"<b>Welcome, {}!</b><br/>You can now use REVOLT.<br/><a href=\"{}\">Go to REVOLT</a>",
|
||||
username.clone(),
|
||||
public_uri()
|
||||
),
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
149
src/main.rs
@@ -1,44 +1,149 @@
|
||||
#![feature(proc_macro_hygiene, decl_macro)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate rocket_contrib;
|
||||
extern crate rocket_okapi;
|
||||
#[macro_use]
|
||||
extern crate bitfield;
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
extern crate ctrlc;
|
||||
|
||||
pub mod notifications;
|
||||
pub mod database;
|
||||
pub mod routes;
|
||||
pub mod email;
|
||||
pub mod util;
|
||||
pub mod version;
|
||||
|
||||
use dotenv;
|
||||
use log::info;
|
||||
use rauth::{
|
||||
config::{Captcha, Config, EmailVerification, SMTPSettings, Template, Templates},
|
||||
logic::Auth,
|
||||
};
|
||||
use revolt_quark::variables::delta::{
|
||||
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME,
|
||||
USE_EMAIL, USE_HCAPTCHA,
|
||||
};
|
||||
use revolt_quark::DatabaseInfo;
|
||||
use rocket_cors::AllowedOrigins;
|
||||
use std::thread;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn main() {
|
||||
dotenv::dotenv().ok();
|
||||
env_logger::init();
|
||||
database::connect();
|
||||
notifications::start_worker();
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
let _guard = revolt_quark::setup_logging();
|
||||
|
||||
thread::spawn(|| {
|
||||
notifications::pubsub::launch_subscriber();
|
||||
});
|
||||
info!(
|
||||
"Starting Revolt server [version {}].",
|
||||
crate::version::VERSION
|
||||
);
|
||||
|
||||
thread::spawn(|| {
|
||||
notifications::ws::launch_server();
|
||||
});
|
||||
revolt_quark::variables::delta::preflight_checks();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
ctrlc::set_handler(move || {
|
||||
// Force ungraceful exit to avoid hang.
|
||||
std::process::exit(0);
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let cors = rocket_cors::CorsOptions {
|
||||
allowed_origins: AllowedOrigins::All,
|
||||
allowed_methods: [
|
||||
"Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| FromStr::from_str(s).unwrap())
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
.to_cors()
|
||||
.unwrap();
|
||||
.expect("Failed to create CORS.");
|
||||
|
||||
routes::mount(rocket::ignite()).attach(cors).launch();
|
||||
let mut config = Config {
|
||||
email_verification: if *USE_EMAIL {
|
||||
EmailVerification::Enabled {
|
||||
smtp: SMTPSettings {
|
||||
from: (*SMTP_FROM).to_string(),
|
||||
host: (*SMTP_HOST).to_string(),
|
||||
username: (*SMTP_USERNAME).to_string(),
|
||||
password: (*SMTP_PASSWORD).to_string(),
|
||||
reply_to: Some("support@revolt.chat".into()),
|
||||
port: None,
|
||||
use_tls: None,
|
||||
},
|
||||
expiry: Default::default(),
|
||||
templates: Templates {
|
||||
verify: Template {
|
||||
title: "Verify your Revolt account.".into(),
|
||||
text: include_str!(crate::asset!("templates/verify.txt")).into(),
|
||||
url: format!("{}/login/verify/", *APP_URL),
|
||||
html: None,
|
||||
},
|
||||
reset: Template {
|
||||
title: "Reset your Revolt password.".into(),
|
||||
text: include_str!(crate::asset!("templates/reset.txt")).into(),
|
||||
url: format!("{}/login/reset/", *APP_URL),
|
||||
html: None,
|
||||
},
|
||||
welcome: None,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
EmailVerification::Disabled
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if *INVITE_ONLY {
|
||||
config.invite_only = true;
|
||||
}
|
||||
|
||||
if *USE_HCAPTCHA {
|
||||
config.captcha = Captcha::HCaptcha {
|
||||
secret: HCAPTCHA_KEY.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
let db = DatabaseInfo::Auto.connect().await.unwrap();
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// This is entirely temporary code until rauth is migrated to quark.
|
||||
// (and / or otherwise gets updated to MongoDB v2 driver)
|
||||
let mongo_db = mongodb::Client::with_uri_str(
|
||||
&std::env::var("MONGODB").unwrap_or_else(|_| "mongodb://localhost".to_string()),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to init db connection.");
|
||||
|
||||
rauth::entities::sync_models(&mongo_db.database("revolt")).await;
|
||||
|
||||
// Launch background task workers.
|
||||
async_std::task::spawn(revolt_quark::tasks::start_workers(db.clone()));
|
||||
|
||||
let auth = Auth::new(mongo_db.database("revolt"), config);
|
||||
let rocket = rocket::build();
|
||||
routes::mount(rocket)
|
||||
.mount("/", rocket_cors::catch_all_options_routes())
|
||||
.mount("/", util::ratelimiter::routes())
|
||||
.mount(
|
||||
"/swagger/",
|
||||
rocket_okapi::swagger_ui::make_swagger_ui(&rocket_okapi::swagger_ui::SwaggerUIConfig {
|
||||
url: "../openapi.json".to_owned(),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.manage(auth)
|
||||
.manage(db)
|
||||
.manage(cors.clone())
|
||||
.attach(util::ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
.launch()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Resolve asset
|
||||
macro_rules! asset {
|
||||
($path:literal) => {
|
||||
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/", $path)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use asset;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UserJoin {
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UserLeave {
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UserJoin {
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct UserLeave {
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
pub banned: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ChannelCreate {
|
||||
pub id: String,
|
||||
pub channel: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ChannelDelete {
|
||||
pub id: String,
|
||||
pub channel: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Delete {
|
||||
pub id: String,
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Create {
|
||||
pub id: String,
|
||||
pub nonce: Option<String>,
|
||||
pub channel: String,
|
||||
pub author: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Edit {
|
||||
pub id: String,
|
||||
pub channel: String,
|
||||
pub author: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Delete {
|
||||
pub id: String,
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub mod groups;
|
||||
pub mod guilds;
|
||||
pub mod message;
|
||||
pub mod users;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum Notification {
|
||||
message_create(message::Create),
|
||||
message_edit(message::Edit),
|
||||
message_delete(message::Delete),
|
||||
group_user_join(groups::UserJoin),
|
||||
group_user_leave(groups::UserLeave),
|
||||
guild_user_join(guilds::UserJoin),
|
||||
guild_user_leave(guilds::UserLeave),
|
||||
guild_channel_create(guilds::ChannelCreate),
|
||||
guild_channel_delete(guilds::ChannelDelete),
|
||||
guild_delete(guilds::Delete),
|
||||
user_friend_status(users::FriendStatus),
|
||||
}
|
||||
|
||||
impl Notification {
|
||||
pub fn serialize(self) -> String {
|
||||
if let Value::Object(obj) = json!(self) {
|
||||
let (key, value) = obj.iter().next().unwrap();
|
||||
|
||||
if let Value::Object(data) = value {
|
||||
let mut data = data.clone();
|
||||
data.insert("type".to_string(), Value::String(key.to_string()));
|
||||
json!(data).to_string()
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_to_cache(&self) {
|
||||
crate::database::channel::process_event(&self);
|
||||
crate::database::guild::process_event(&self);
|
||||
crate::database::user::process_event(&self);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct FriendStatus {
|
||||
pub id: String,
|
||||
pub user: String,
|
||||
pub status: i32,
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
use crate::database::channel::Channel;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::sync::mpsc::{channel, Sender};
|
||||
use std::thread;
|
||||
|
||||
pub mod events;
|
||||
pub mod pubsub;
|
||||
pub mod state;
|
||||
pub mod ws;
|
||||
|
||||
pub fn send_message<U: Into<Option<Vec<String>>>, G: Into<Option<String>>>(
|
||||
users: U,
|
||||
guild: G,
|
||||
data: events::Notification,
|
||||
) -> bool {
|
||||
let users = users.into();
|
||||
let guild = guild.into();
|
||||
|
||||
data.push_to_cache();
|
||||
|
||||
if pubsub::send_message(users.clone(), guild.clone(), data.clone()) {
|
||||
state::send_message(users, guild, data.serialize());
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationArguments {
|
||||
users: Option<Vec<String>>,
|
||||
guild: Option<String>,
|
||||
data: events::Notification,
|
||||
}
|
||||
|
||||
static mut SENDER: OnceCell<Sender<NotificationArguments>> = OnceCell::new();
|
||||
|
||||
pub fn start_worker() {
|
||||
let (sender, receiver) = channel();
|
||||
unsafe {
|
||||
SENDER.set(sender).unwrap();
|
||||
}
|
||||
|
||||
thread::spawn(move || {
|
||||
while let Ok(data) = receiver.recv() {
|
||||
send_message(data.users, data.guild, data.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn send_message_threaded<U: Into<Option<Vec<String>>>, G: Into<Option<String>>>(
|
||||
users: U,
|
||||
guild: G,
|
||||
data: events::Notification,
|
||||
) -> bool {
|
||||
unsafe {
|
||||
SENDER
|
||||
.get()
|
||||
.unwrap()
|
||||
.send(NotificationArguments {
|
||||
users: users.into(),
|
||||
guild: guild.into(),
|
||||
data,
|
||||
})
|
||||
.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message_given_channel(data: events::Notification, channel: &Channel) {
|
||||
match channel.channel_type {
|
||||
0..=1 => send_message_threaded(channel.recipients.clone(), None, data),
|
||||
2 => send_message_threaded(None, channel.guild.clone(), data),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
use super::events::Notification;
|
||||
use crate::database::get_collection;
|
||||
|
||||
use mongodb::bson::{doc, from_bson, to_bson, Bson};
|
||||
use mongodb::options::{CursorType, FindOneOptions, FindOptions};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use ulid::Ulid;
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
static SOURCEID: OnceCell<String> = OnceCell::new();
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct PubSubMessage {
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
source: String,
|
||||
|
||||
user_recipients: Option<Vec<String>>,
|
||||
target_guild: Option<String>,
|
||||
data: Notification,
|
||||
}
|
||||
|
||||
pub fn send_message(users: Option<Vec<String>>, guild: Option<String>, data: Notification) -> bool {
|
||||
let message = PubSubMessage {
|
||||
id: Ulid::new().to_string(),
|
||||
source: SOURCEID.get().unwrap().to_string(),
|
||||
user_recipients: users.into(),
|
||||
target_guild: guild.into(),
|
||||
data,
|
||||
};
|
||||
|
||||
if get_collection("pubsub")
|
||||
.insert_one(
|
||||
to_bson(&message)
|
||||
.expect("Failed to serialize pubsub message.")
|
||||
.as_document()
|
||||
.expect("Failed to convert to a document.")
|
||||
.clone(),
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch_subscriber() {
|
||||
let source = Ulid::new().to_string();
|
||||
SOURCEID
|
||||
.set(source.clone())
|
||||
.expect("Failed to create and set source ID.");
|
||||
|
||||
let pubsub = get_collection("pubsub");
|
||||
if let Ok(result) = pubsub.find_one(
|
||||
doc! {},
|
||||
FindOneOptions::builder().sort(doc! { "_id": -1 }).build(),
|
||||
) {
|
||||
let query = if let Some(doc) = result {
|
||||
doc! { "_id": { "$gt": doc.get_str("_id").unwrap() } }
|
||||
} else {
|
||||
doc! {}
|
||||
};
|
||||
|
||||
if let Ok(mut cursor) = pubsub.find(
|
||||
query,
|
||||
FindOptions::builder()
|
||||
.cursor_type(CursorType::TailableAwait)
|
||||
.no_cursor_timeout(true)
|
||||
.max_await_time(Duration::from_secs(1200))
|
||||
.build(),
|
||||
) {
|
||||
loop {
|
||||
while let Some(item) = cursor.next() {
|
||||
if let Ok(doc) = item {
|
||||
if let Ok(message) =
|
||||
from_bson(Bson::Document(doc)) as Result<PubSubMessage, _>
|
||||
{
|
||||
if &message.source != &source {
|
||||
super::state::send_message(
|
||||
message.user_recipients,
|
||||
message.target_guild,
|
||||
message.data.serialize(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!("Failed to deserialize pubsub message.");
|
||||
}
|
||||
} else {
|
||||
eprintln!("Failed to unwrap a document from pubsub.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("Failed to open subscriber cursor.");
|
||||
}
|
||||
} else {
|
||||
eprintln!("Failed to fetch latest document from pubsub collection.");
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
use crate::database;
|
||||
use crate::util::vec_to_set;
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use mongodb::bson::doc;
|
||||
use mongodb::options::FindOneOptions;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::sync::RwLock;
|
||||
use ws::Sender;
|
||||
|
||||
pub enum StateResult {
|
||||
DatabaseError,
|
||||
InvalidToken,
|
||||
Success(String),
|
||||
}
|
||||
|
||||
static mut CONNECTIONS: OnceCell<RwLock<HashMap<String, Sender>>> = OnceCell::new();
|
||||
|
||||
pub fn add_connection(id: String, sender: Sender) {
|
||||
unsafe {
|
||||
CONNECTIONS
|
||||
.get()
|
||||
.unwrap()
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(id, sender);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct User {
|
||||
connections: HashSet<String>,
|
||||
guilds: HashSet<String>,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new() -> User {
|
||||
User {
|
||||
connections: HashSet::new(),
|
||||
guilds: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Guild {
|
||||
users: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Guild {
|
||||
pub fn new() -> Guild {
|
||||
Guild {
|
||||
users: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlobalState {
|
||||
users: HashMap<String, User>,
|
||||
guilds: HashMap<String, Guild>,
|
||||
}
|
||||
|
||||
impl GlobalState {
|
||||
pub fn new() -> GlobalState {
|
||||
GlobalState {
|
||||
users: HashMap::new(),
|
||||
guilds: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_to_guild(&mut self, guild: String, user: String) {
|
||||
if !self.guilds.contains_key(&guild) {
|
||||
self.guilds.insert(guild.clone(), Guild::new());
|
||||
}
|
||||
|
||||
self.guilds.get_mut(&guild).unwrap().users.insert(user);
|
||||
}
|
||||
|
||||
pub fn try_authenticate(&mut self, connection: String, access_token: String) -> StateResult {
|
||||
if let Ok(result) = database::get_collection("users").find_one(
|
||||
doc! {
|
||||
"access_token": access_token,
|
||||
},
|
||||
FindOneOptions::builder()
|
||||
.projection(doc! { "_id": 1 })
|
||||
.build(),
|
||||
) {
|
||||
if let Some(user) = result {
|
||||
let user_id = user.get_str("_id").unwrap();
|
||||
|
||||
if self.users.contains_key(user_id) {
|
||||
self.users
|
||||
.get_mut(user_id)
|
||||
.unwrap()
|
||||
.connections
|
||||
.insert(connection);
|
||||
|
||||
return StateResult::Success(user_id.to_string());
|
||||
}
|
||||
|
||||
if let Ok(results) =
|
||||
database::get_collection("members").find(doc! { "_id.user": &user_id }, None)
|
||||
{
|
||||
let mut guilds = vec![];
|
||||
for result in results {
|
||||
if let Ok(entry) = result {
|
||||
guilds.push(
|
||||
entry
|
||||
.get_document("_id")
|
||||
.unwrap()
|
||||
.get_str("guild")
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut user = User::new();
|
||||
for guild in guilds {
|
||||
user.guilds.insert(guild.clone());
|
||||
self.push_to_guild(guild, user_id.to_string());
|
||||
}
|
||||
|
||||
user.connections.insert(connection);
|
||||
self.users.insert(user_id.to_string(), user);
|
||||
|
||||
StateResult::Success(user_id.to_string())
|
||||
} else {
|
||||
StateResult::DatabaseError
|
||||
}
|
||||
} else {
|
||||
StateResult::InvalidToken
|
||||
}
|
||||
} else {
|
||||
StateResult::DatabaseError
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect<U: Into<Option<String>>>(&mut self, user_id: U, connection: String) {
|
||||
if let Some(user_id) = user_id.into() {
|
||||
let user = self.users.get_mut(&user_id).unwrap();
|
||||
user.connections.remove(&connection);
|
||||
|
||||
if user.connections.len() == 0 {
|
||||
for guild in &user.guilds {
|
||||
self.guilds.get_mut(guild).unwrap().users.remove(&user_id);
|
||||
}
|
||||
|
||||
self.users.remove(&user_id);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
CONNECTIONS
|
||||
.get()
|
||||
.unwrap()
|
||||
.write()
|
||||
.unwrap()
|
||||
.remove(&connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub static mut DATA: OnceCell<RwLock<GlobalState>> = OnceCell::new();
|
||||
|
||||
pub fn init() {
|
||||
unsafe {
|
||||
if CONNECTIONS.set(RwLock::new(HashMap::new())).is_err() {
|
||||
panic!("Failed to set global connections map.");
|
||||
}
|
||||
|
||||
if DATA.set(RwLock::new(GlobalState::new())).is_err() {
|
||||
panic!("Failed to set global state.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message(users: Option<Vec<String>>, guild: Option<String>, data: String) {
|
||||
let state = unsafe { DATA.get().unwrap().read().unwrap() };
|
||||
let mut connections = HashSet::new();
|
||||
|
||||
let mut users = vec_to_set(&users.unwrap_or(vec![]));
|
||||
if let Some(guild) = guild {
|
||||
if let Some(entry) = state.guilds.get(&guild) {
|
||||
for user in &entry.users {
|
||||
users.insert(user.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for user in users {
|
||||
if let Some(entry) = state.users.get(&user) {
|
||||
for connection in &entry.connections {
|
||||
connections.insert(connection.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let targets = unsafe { CONNECTIONS.get().unwrap().read().unwrap() };
|
||||
for conn in connections {
|
||||
if let Some(sender) = targets.get(&conn) {
|
||||
if sender.send(data.clone()).is_err() {
|
||||
eprintln!("Failed to send a notification to a websocket. [{}]", &conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
use super::state::{self, StateResult};
|
||||
|
||||
use serde_json::{from_str, json, Value};
|
||||
use std::env;
|
||||
use ulid::Ulid;
|
||||
use ws::{listen, CloseCode, Error, Handler, Handshake, Message, Result, Sender};
|
||||
|
||||
struct Server {
|
||||
sender: Sender,
|
||||
user_id: Option<String>,
|
||||
id: String,
|
||||
}
|
||||
|
||||
impl Handler for Server {
|
||||
fn on_open(&mut self, _: Handshake) -> Result<()> {
|
||||
state::add_connection(self.id.clone(), self.sender.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_message(&mut self, msg: Message) -> Result<()> {
|
||||
if let Message::Text(text) = msg {
|
||||
if let Ok(data) = from_str(&text) as std::result::Result<Value, _> {
|
||||
if let Value::String(packet_type) = &data["type"] {
|
||||
if packet_type == "authenticate" {
|
||||
if self.user_id.is_some() {
|
||||
self.sender.send(
|
||||
json!({
|
||||
"type": "authenticate",
|
||||
"success": false,
|
||||
"error": "Already authenticated!"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
} else if let Value::String(token) = &data["token"] {
|
||||
let mut state = unsafe { state::DATA.get().unwrap().write().unwrap() };
|
||||
|
||||
match state.try_authenticate(self.id.clone(), token.to_string()) {
|
||||
StateResult::Success(user_id) => {
|
||||
let user = crate::database::user::fetch_user(&user_id).unwrap().unwrap();
|
||||
self.user_id = Some(user_id);
|
||||
|
||||
self.sender.send(
|
||||
json!({
|
||||
"type": "authenticate",
|
||||
"success": true,
|
||||
})
|
||||
.to_string(),
|
||||
)?;
|
||||
|
||||
self.sender.send(
|
||||
json!({
|
||||
"type": "ready",
|
||||
"data": user.create_payload()
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
StateResult::DatabaseError => self.sender.send(
|
||||
json!({
|
||||
"type": "authenticate",
|
||||
"success": false,
|
||||
"error": "Had database error."
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
StateResult::InvalidToken => self.sender.send(
|
||||
json!({
|
||||
"type": "authenticate",
|
||||
"success": false,
|
||||
"error": "Invalid token."
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
self.sender.send(
|
||||
json!({
|
||||
"type": "authenticate",
|
||||
"success": false,
|
||||
"error": "Token not present."
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn on_close(&mut self, _code: CloseCode, _reason: &str) {
|
||||
unsafe {
|
||||
state::DATA
|
||||
.get()
|
||||
.unwrap()
|
||||
.write()
|
||||
.unwrap()
|
||||
.disconnect(self.user_id.clone(), self.id.clone());
|
||||
}
|
||||
|
||||
println!("User disconnected. [{}]", self.id);
|
||||
}
|
||||
|
||||
fn on_error(&mut self, err: Error) {
|
||||
println!("The server encountered an error: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch_server() {
|
||||
state::init();
|
||||
|
||||
listen(
|
||||
env::var("WS_HOST").unwrap_or("0.0.0.0:9999".to_string()),
|
||||
|sender| Server {
|
||||
sender,
|
||||
user_id: None,
|
||||
id: Ulid::new().to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
use super::Response;
|
||||
use crate::database;
|
||||
use crate::email;
|
||||
use crate::util::gen_token;
|
||||
use crate::util::captcha;
|
||||
|
||||
use bcrypt::{hash, verify};
|
||||
use chrono::prelude::*;
|
||||
use database::user::User;
|
||||
use mongodb::bson::{doc, from_bson, Bson};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use validator::validate_email;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Create {
|
||||
username: String,
|
||||
password: String,
|
||||
email: String,
|
||||
captcha: Option<String>,
|
||||
}
|
||||
|
||||
/// create a new Revolt account
|
||||
/// (1) validate input
|
||||
/// [username] 2 to 32 characters
|
||||
/// [password] 8 to 72 characters
|
||||
/// [email] validate against RFC
|
||||
/// (2) check email existence
|
||||
/// (3) add user and send email verification
|
||||
#[post("/create", data = "<info>")]
|
||||
pub fn create(info: Json<Create>) -> Response {
|
||||
if let Err(error) = captcha::verify(&info.captcha) {
|
||||
return Response::BadRequest(
|
||||
json!({ "error": error })
|
||||
);
|
||||
}
|
||||
|
||||
if true {
|
||||
return Response::BadRequest(
|
||||
json!({ "error": "Registration disabled." })
|
||||
);
|
||||
}
|
||||
|
||||
let col = database::get_collection("users");
|
||||
|
||||
if info.username.len() < 2 || info.username.len() > 32 {
|
||||
return Response::NotAcceptable(
|
||||
json!({ "error": "Username needs to be at least 2 chars and less than 32 chars." }),
|
||||
);
|
||||
}
|
||||
|
||||
if info.password.len() < 8 || info.password.len() > 72 {
|
||||
return Response::NotAcceptable(
|
||||
json!({ "error": "Password needs to be at least 8 chars and at most 72." }),
|
||||
);
|
||||
}
|
||||
|
||||
if !validate_email(info.email.clone()) {
|
||||
return Response::UnprocessableEntity(json!({ "error": "Invalid email." }));
|
||||
}
|
||||
|
||||
if let Some(_) = col
|
||||
.find_one(doc! { "email": info.email.clone() }, None)
|
||||
.expect("Failed user lookup")
|
||||
{
|
||||
return Response::Conflict(json!({ "error": "Email already in use!" }));
|
||||
}
|
||||
|
||||
if let Some(_) = col
|
||||
.find_one(doc! { "username": info.username.clone() }, None)
|
||||
.expect("Failed user lookup")
|
||||
{
|
||||
return Response::Conflict(json!({ "error": "Username already in use!" }));
|
||||
}
|
||||
|
||||
if let Ok(hashed) = hash(info.password.clone(), 10) {
|
||||
let access_token = gen_token(92);
|
||||
let code = gen_token(48);
|
||||
|
||||
match col.insert_one(
|
||||
doc! {
|
||||
"_id": Ulid::new().to_string(),
|
||||
"email": info.email.clone(),
|
||||
"username": info.username.clone(),
|
||||
"display_name": info.username.clone(),
|
||||
"password": hashed,
|
||||
"access_token": access_token,
|
||||
"email_verification": {
|
||||
"verified": false,
|
||||
"target": info.email.clone(),
|
||||
"expiry": Bson::DateTime(Utc::now() + chrono::Duration::days(1)),
|
||||
"rate_limit": Bson::DateTime(Utc::now() + chrono::Duration::minutes(1)),
|
||||
"code": code.clone(),
|
||||
}
|
||||
},
|
||||
None,
|
||||
) {
|
||||
Ok(_) => {
|
||||
let sent = email::send_verification_email(info.email.clone(), code);
|
||||
|
||||
Response::Success(json!({
|
||||
"email_sent": sent,
|
||||
}))
|
||||
}
|
||||
Err(_) => {
|
||||
Response::InternalServerError(json!({ "error": "Failed to create account." }))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Response::InternalServerError(json!({ "error": "Failed to hash." }))
|
||||
}
|
||||
}
|
||||
|
||||
/// verify an email for a Revolt account
|
||||
/// (1) check if code is valid
|
||||
/// (2) check if it expired yet
|
||||
/// (3) set account as verified
|
||||
#[get("/verify/<code>")]
|
||||
pub fn verify_email(code: String) -> Response {
|
||||
let col = database::get_collection("users");
|
||||
|
||||
if let Some(u) = col
|
||||
.find_one(doc! { "email_verification.code": code.clone() }, None)
|
||||
.expect("Failed user lookup")
|
||||
{
|
||||
let user: User = from_bson(Bson::Document(u)).expect("Failed to unwrap user.");
|
||||
let ev = user.email_verification;
|
||||
|
||||
if Utc::now() > *ev.expiry.unwrap() {
|
||||
Response::Gone(json!({
|
||||
"success": false,
|
||||
"error": "Token has expired!",
|
||||
}))
|
||||
} else {
|
||||
let target = ev.target.unwrap();
|
||||
col.update_one(
|
||||
doc! { "_id": user.id },
|
||||
doc! {
|
||||
"$unset": {
|
||||
"email_verification.code": "",
|
||||
"email_verification.expiry": "",
|
||||
"email_verification.target": "",
|
||||
"email_verification.rate_limit": "",
|
||||
},
|
||||
"$set": {
|
||||
"email_verification.verified": true,
|
||||
"email": target.clone(),
|
||||
},
|
||||
},
|
||||
None,
|
||||
)
|
||||
.expect("Failed to update user!");
|
||||
|
||||
email::send_welcome_email(target.to_string(), user.username);
|
||||
|
||||
Response::Redirect(super::Redirect::to("https://app.revolt.chat"))
|
||||
}
|
||||
} else {
|
||||
Response::BadRequest(json!({ "error": "Invalid code." }))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Resend {
|
||||
email: String,
|
||||
captcha: Option<String>,
|
||||
}
|
||||
|
||||
/// resend a verification email
|
||||
/// (1) check if verification is pending for x email
|
||||
/// (2) check for rate limit
|
||||
/// (3) resend the email
|
||||
#[post("/resend", data = "<info>")]
|
||||
pub fn resend_email(info: Json<Resend>) -> Response {
|
||||
if let Err(error) = captcha::verify(&info.captcha) {
|
||||
return Response::BadRequest(
|
||||
json!({ "error": error })
|
||||
);
|
||||
}
|
||||
|
||||
let col = database::get_collection("users");
|
||||
|
||||
if let Some(u) = col
|
||||
.find_one(
|
||||
doc! { "email_verification.target": info.email.clone() },
|
||||
None,
|
||||
)
|
||||
.expect("Failed user lookup.")
|
||||
{
|
||||
let user: User = from_bson(Bson::Document(u)).expect("Failed to unwrap user.");
|
||||
let ev = user.email_verification;
|
||||
|
||||
let expiry = ev.expiry.unwrap();
|
||||
let rate_limit = ev.rate_limit.unwrap();
|
||||
|
||||
if Utc::now() < *rate_limit {
|
||||
Response::TooManyRequests(
|
||||
json!({ "error": "You are being rate limited, please try again in a while." }),
|
||||
)
|
||||
} else {
|
||||
let mut new_expiry = Bson::DateTime(Utc::now() + chrono::Duration::days(1));
|
||||
if info.email.clone() != user.email {
|
||||
if Utc::now() > *expiry {
|
||||
return Response::Gone(
|
||||
json!({ "error": "To help protect your account, please login and change your email again. The original request was made over one day ago." }),
|
||||
);
|
||||
}
|
||||
|
||||
new_expiry = Bson::DateTime(*expiry);
|
||||
}
|
||||
|
||||
let code = gen_token(48);
|
||||
col.update_one(
|
||||
doc! { "_id": user.id },
|
||||
doc! {
|
||||
"$set": {
|
||||
"email_verification.code": code.clone(),
|
||||
"email_verification.expiry": new_expiry,
|
||||
"email_verification.rate_limit": Bson::DateTime(Utc::now() + chrono::Duration::minutes(1)),
|
||||
},
|
||||
},
|
||||
None,
|
||||
).expect("Failed to update user!");
|
||||
|
||||
match email::send_verification_email(info.email.to_string(), code) {
|
||||
true => Response::Result(super::Status::Ok),
|
||||
false => Response::InternalServerError(
|
||||
json!({ "error": "Failed to send email! Likely an issue with the backend API." }),
|
||||
),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Response::NotFound(json!({ "error": "Email not found or pending verification!" }))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Login {
|
||||
email: String,
|
||||
password: String,
|
||||
captcha: Option<String>,
|
||||
}
|
||||
|
||||
/// login to a Revolt account
|
||||
/// (1) find user by email
|
||||
/// (2) verify password
|
||||
/// (3) return access token
|
||||
#[post("/login", data = "<info>")]
|
||||
pub fn login(info: Json<Login>) -> Response {
|
||||
if let Err(error) = captcha::verify(&info.captcha) {
|
||||
return Response::BadRequest(
|
||||
json!({ "error": error })
|
||||
);
|
||||
}
|
||||
|
||||
let col = database::get_collection("users");
|
||||
|
||||
if let Some(u) = col
|
||||
.find_one(doc! { "email": info.email.clone() }, None)
|
||||
.expect("Failed user lookup")
|
||||
{
|
||||
let user: User = from_bson(Bson::Document(u)).expect("Failed to unwrap user.");
|
||||
|
||||
match verify(info.password.clone(), &user.password)
|
||||
.expect("Failed to check hash of password.")
|
||||
{
|
||||
true => {
|
||||
let token = match user.access_token {
|
||||
Some(t) => t.to_string(),
|
||||
None => {
|
||||
let token = gen_token(92);
|
||||
if col
|
||||
.update_one(
|
||||
doc! { "_id": &user.id },
|
||||
doc! { "$set": { "access_token": token.clone() } },
|
||||
None,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Response::InternalServerError(
|
||||
json!({ "error": "Failed database operation." }),
|
||||
);
|
||||
}
|
||||
|
||||
token
|
||||
}
|
||||
};
|
||||
|
||||
Response::Success(json!({ "access_token": token, "id": user.id }))
|
||||
}
|
||||
false => Response::Unauthorized(json!({ "error": "Invalid password." })),
|
||||
}
|
||||
} else {
|
||||
Response::NotFound(json!({ "error": "Email is not registered." }))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Token {
|
||||
token: String,
|
||||
}
|
||||
|
||||
/// login to a Revolt account via token
|
||||
#[post("/token", data = "<info>")]
|
||||
pub fn token(info: Json<Token>) -> Response {
|
||||
let col = database::get_collection("users");
|
||||
|
||||
if let Ok(result) = col.find_one(doc! { "access_token": info.token.clone() }, None) {
|
||||
if let Some(user) = result {
|
||||
Response::Success(json!({
|
||||
"id": user.get_str("_id").unwrap(),
|
||||
}))
|
||||
} else {
|
||||
Response::Unauthorized(json!({
|
||||
"error": "Invalid token!",
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
Response::InternalServerError(json!({
|
||||
"error": "Failed database query.",
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[options("/create")]
|
||||
pub fn create_preflight() -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/verify/<_code>")]
|
||||
pub fn verify_email_preflight(_code: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/resend")]
|
||||
pub fn resend_email_preflight() -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/login")]
|
||||
pub fn login_preflight() -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/token")]
|
||||
pub fn token_preflight() -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
65
src/routes/bots/create.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::util::regex::RE_USERNAME;
|
||||
|
||||
use nanoid::nanoid;
|
||||
use revolt_quark::{
|
||||
models::{user::BotInformation, Bot, User},
|
||||
variables::delta::MAX_BOT_COUNT,
|
||||
Db, Error, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Deserialize;
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
/// # Bot Details
|
||||
#[derive(Validate, Deserialize, JsonSchema)]
|
||||
pub struct DataCreateBot {
|
||||
/// Bot username
|
||||
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// # Create Bot
|
||||
///
|
||||
/// Create a new Revolt bot.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[post("/create", data = "<info>")]
|
||||
pub async fn create_bot(db: &Db, user: User, info: Json<DataCreateBot>) -> Result<Json<Bot>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let info = info.into_inner();
|
||||
info.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
if db.get_number_of_bots_by_user(&user.id).await? >= *MAX_BOT_COUNT {
|
||||
return Err(Error::ReachedMaximumBots);
|
||||
}
|
||||
|
||||
if db.is_username_taken(&info.name).await? {
|
||||
return Err(Error::UsernameTaken);
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
let bot_user = User {
|
||||
id: id.clone(),
|
||||
username: info.name.trim().to_string(),
|
||||
bot: Some(BotInformation {
|
||||
owner: user.id.clone(),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let bot = Bot {
|
||||
id,
|
||||
owner: user.id,
|
||||
token: nanoid!(64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_user(&bot_user).await?;
|
||||
db.insert_bot(&bot).await?;
|
||||
Ok(Json(bot))
|
||||
}
|
||||
19
src/routes/bots/delete.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use revolt_quark::{models::User, Db, EmptyResponse, Error, Ref, Result};
|
||||
|
||||
/// # Delete Bot
|
||||
///
|
||||
/// Delete a bot by its id.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[delete("/<target>")]
|
||||
pub async fn delete_bot(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let bot = target.as_bot(db).await?;
|
||||
if bot.owner != user.id {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
bot.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
107
src/routes/bots/edit.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::util::regex::RE_USERNAME;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
bot::{FieldsBot, PartialBot},
|
||||
Bot, User,
|
||||
},
|
||||
Db, Error, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Bot Details
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataEditBot {
|
||||
/// Bot username
|
||||
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
/// Whether the bot can be added by anyone
|
||||
public: Option<bool>,
|
||||
/// Whether analytics should be gathered for this bot
|
||||
///
|
||||
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
|
||||
analytics: Option<bool>,
|
||||
/// Interactions URL
|
||||
#[validate(length(min = 1, max = 2048))]
|
||||
interactions_url: Option<String>,
|
||||
/// Fields to remove from bot object
|
||||
#[validate(length(min = 1))]
|
||||
remove: Option<Vec<FieldsBot>>,
|
||||
}
|
||||
|
||||
/// # Edit Bot
|
||||
///
|
||||
/// Edit bot details by its id.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[patch("/<target>", data = "<data>")]
|
||||
pub async fn edit_bot(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataEditBot>,
|
||||
) -> Result<Json<Bot>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let mut bot = target.as_bot(db).await?;
|
||||
if bot.owner != user.id {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
if let Some(name) = data.name {
|
||||
if db.is_username_taken(&name).await? {
|
||||
return Err(Error::UsernameTaken);
|
||||
}
|
||||
|
||||
let mut user = db.fetch_user(&bot.id).await?;
|
||||
user.update_username(db, name).await?;
|
||||
}
|
||||
|
||||
if data.public.is_none()
|
||||
&& data.analytics.is_none()
|
||||
&& data.interactions_url.is_none()
|
||||
&& data.remove.is_none()
|
||||
{
|
||||
return Ok(Json(bot));
|
||||
}
|
||||
|
||||
let DataEditBot {
|
||||
public,
|
||||
analytics,
|
||||
interactions_url,
|
||||
remove,
|
||||
..
|
||||
} = data;
|
||||
|
||||
let mut partial = PartialBot {
|
||||
public,
|
||||
analytics,
|
||||
interactions_url,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(remove) = &remove {
|
||||
for field in remove {
|
||||
bot.remove(field);
|
||||
}
|
||||
|
||||
if remove.iter().any(|x| x == &FieldsBot::Token) {
|
||||
partial.token = Some(bot.token.clone());
|
||||
}
|
||||
}
|
||||
|
||||
db.update_bot(&bot.id, &partial, remove.unwrap_or_default())
|
||||
.await?;
|
||||
|
||||
bot.apply_options(partial);
|
||||
Ok(Json(bot))
|
||||
}
|
||||
36
src/routes/bots/fetch.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use revolt_quark::{
|
||||
models::{Bot, User},
|
||||
Db, Error, Ref, Result,
|
||||
};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
/// # Bot Response
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
pub struct BotResponse {
|
||||
/// Bot object
|
||||
bot: Bot,
|
||||
/// User object
|
||||
user: User,
|
||||
}
|
||||
|
||||
/// # Fetch Bot
|
||||
///
|
||||
/// Fetch details of a bot you own by its id.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[get("/<target>")]
|
||||
pub async fn fetch_bot(db: &Db, user: User, target: Ref) -> Result<Json<BotResponse>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let bot = target.as_bot(db).await?;
|
||||
if bot.owner != user.id {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
Ok(Json(BotResponse {
|
||||
user: db.fetch_user(&bot.id).await?.foreign(),
|
||||
bot,
|
||||
}))
|
||||
}
|
||||
42
src/routes/bots/fetch_owned.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use revolt_quark::{
|
||||
models::{Bot, User},
|
||||
Db, Error, Result,
|
||||
};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
/// # Owned Bots Response
|
||||
///
|
||||
/// Both lists are sorted by their IDs.
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
pub struct OwnedBotsResponse {
|
||||
/// Bot objects
|
||||
bots: Vec<Bot>,
|
||||
/// User objects
|
||||
users: Vec<User>,
|
||||
}
|
||||
|
||||
/// # Fetch Owned Bots
|
||||
///
|
||||
/// Fetch all of the bots that you have control over.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[get("/@me")]
|
||||
pub async fn fetch_owned_bots(db: &Db, user: User) -> Result<Json<OwnedBotsResponse>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let mut bots = db.fetch_bots_by_user(&user.id).await?;
|
||||
let user_ids = bots
|
||||
.iter()
|
||||
.map(|x| x.id.to_owned())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut users = db.fetch_users(&user_ids).await?;
|
||||
|
||||
// Ensure the lists match up exactly.
|
||||
bots.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
users.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
|
||||
Ok(Json(OwnedBotsResponse { users, bots }))
|
||||
}
|
||||
44
src/routes/bots/fetch_public.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use revolt_quark::{
|
||||
models::{File, User},
|
||||
Db, Error, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Public Bot
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PublicBot {
|
||||
/// Bot Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Bot Username
|
||||
username: String,
|
||||
/// Profile Avatar
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
avatar: Option<File>,
|
||||
/// Profile Description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
/// # Fetch Public Bot
|
||||
///
|
||||
/// Fetch details of a public (or owned) bot by its id.
|
||||
#[openapi(tag = "Bots")]
|
||||
#[get("/<target>/invite")]
|
||||
pub async fn fetch_public_bot(db: &Db, user: Option<User>, target: Ref) -> Result<Json<PublicBot>> {
|
||||
let bot = target.as_bot(db).await?;
|
||||
if !bot.public && user.map_or(true, |x| x.id != bot.owner) {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
let user = db.fetch_user(&bot.id).await?;
|
||||
|
||||
Ok(Json(PublicBot {
|
||||
id: bot.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
description: user.profile.and_then(|p| p.content),
|
||||
}))
|
||||
}
|
||||
71
src/routes/bots/invite.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// # Invite Destination
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum InviteBotDestination {
|
||||
/// Invite to a server
|
||||
Server {
|
||||
/// Server Id
|
||||
server: String,
|
||||
},
|
||||
/// Invite to a group
|
||||
Group {
|
||||
/// Group Id
|
||||
group: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// # Invite Bot
|
||||
///
|
||||
/// Invite a bot to a server or group by its id.`
|
||||
#[openapi(tag = "Bots")]
|
||||
#[post("/<target>/invite", data = "<dest>")]
|
||||
pub async fn invite_bot(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
dest: Json<InviteBotDestination>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let bot = target.as_bot(db).await?;
|
||||
if !bot.public && bot.owner != user.id {
|
||||
return Err(Error::BotIsPrivate);
|
||||
}
|
||||
|
||||
match dest.into_inner() {
|
||||
InviteBotDestination::Server { server } => {
|
||||
let server = db.fetch_server(&server).await?;
|
||||
|
||||
perms(&user)
|
||||
.server(&server)
|
||||
.throw_permission(db, Permission::ManageServer)
|
||||
.await?;
|
||||
|
||||
let user = db.fetch_user(&bot.id).await?;
|
||||
server
|
||||
.create_member(db, user, None)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
InviteBotDestination::Group { group } => {
|
||||
let mut channel = db.fetch_channel(&group).await?;
|
||||
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::InviteOthers)
|
||||
.await?;
|
||||
|
||||
channel
|
||||
.add_user_to_group(db, &bot.id, &user.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/routes/bots/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod create;
|
||||
mod delete;
|
||||
mod edit;
|
||||
mod fetch;
|
||||
mod fetch_owned;
|
||||
mod fetch_public;
|
||||
mod invite;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
create::create_bot,
|
||||
invite::invite_bot,
|
||||
fetch_public::fetch_public_bot,
|
||||
fetch::fetch_bot,
|
||||
fetch_owned::fetch_owned_bots,
|
||||
edit::edit_bot,
|
||||
delete::delete_bot,
|
||||
]
|
||||
}
|
||||
@@ -1,704 +0,0 @@
|
||||
use super::Response;
|
||||
use crate::database::{
|
||||
self, channel::Channel, get_relationship, get_relationship_internal, message::Message,
|
||||
Permission, PermissionCalculator, Relationship, user::User
|
||||
};
|
||||
use crate::notifications::{
|
||||
self,
|
||||
events::{groups::*, guilds::ChannelDelete, message::*, Notification},
|
||||
};
|
||||
use crate::util::vec_to_set;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use mongodb::bson::{doc, from_bson, Bson};
|
||||
use mongodb::options::FindOptions;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use rocket::request::Form;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
const MAXGROUPSIZE: usize = 50;
|
||||
|
||||
#[derive(Debug, TryFromPrimitive)]
|
||||
#[repr(usize)]
|
||||
pub enum ChannelType {
|
||||
DM = 0,
|
||||
GROUPDM = 1,
|
||||
GUILDCHANNEL = 2,
|
||||
}
|
||||
|
||||
macro_rules! with_permissions {
|
||||
($user: expr, $target: expr) => {{
|
||||
let permissions = PermissionCalculator::new($user.clone())
|
||||
.channel($target.clone())
|
||||
.fetch_data();
|
||||
|
||||
let value = permissions.as_permission();
|
||||
if !value.get_access() {
|
||||
return None;
|
||||
}
|
||||
|
||||
value
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateGroup {
|
||||
name: String,
|
||||
nonce: String,
|
||||
users: Vec<String>,
|
||||
}
|
||||
|
||||
/// create a new group
|
||||
#[post("/create", data = "<info>")]
|
||||
pub fn create_group(user: User, info: Json<CreateGroup>) -> Response {
|
||||
let name: String = info.name.chars().take(32).collect();
|
||||
let nonce: String = info.nonce.chars().take(32).collect();
|
||||
|
||||
let mut set = vec_to_set(&info.users);
|
||||
set.insert(user.id.clone());
|
||||
|
||||
if set.len() > MAXGROUPSIZE {
|
||||
return Response::BadRequest(json!({ "error": "Maximum group size is 50." }));
|
||||
}
|
||||
|
||||
let col = database::get_collection("channels");
|
||||
if let Some(_) = col.find_one(doc! { "nonce": nonce.clone() }, None).unwrap() {
|
||||
return Response::BadRequest(json!({ "error": "Group already created!" }));
|
||||
}
|
||||
|
||||
let mut query = vec![];
|
||||
for item in &set {
|
||||
if item == &user.id {
|
||||
continue;
|
||||
}
|
||||
|
||||
query.push(Bson::String(item.clone()));
|
||||
}
|
||||
|
||||
if let Ok(result) = database::get_collection("users").find(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": &query
|
||||
}
|
||||
},
|
||||
FindOptions::builder().limit(query.len() as i64).build(),
|
||||
) {
|
||||
if result.count() != query.len() {
|
||||
return Response::BadRequest(json!({ "error": "Specified non-existant user(s)." }));
|
||||
}
|
||||
|
||||
for item in set {
|
||||
if item == user.id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if get_relationship_internal(&user.id, &item, &user.relations) != Relationship::Friend {
|
||||
return Response::BadRequest(json!({ "error": "Not friends with user(s)." }));
|
||||
}
|
||||
}
|
||||
|
||||
query.push(Bson::String(user.id.clone()));
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
if col
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": id.clone(),
|
||||
"nonce": nonce,
|
||||
"type": ChannelType::GROUPDM as u32,
|
||||
"recipients": &query,
|
||||
"name": name,
|
||||
"owner": &user.id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
Response::Success(json!({ "id": id }))
|
||||
} else {
|
||||
Response::InternalServerError(json!({ "error": "Failed to create guild channel." }))
|
||||
}
|
||||
} else {
|
||||
Response::InternalServerError(json!({ "error": "Failed to validate users." }))
|
||||
}
|
||||
}
|
||||
|
||||
/// fetch channel information
|
||||
#[get("/<target>")]
|
||||
pub fn channel(user: User, target: Channel) -> Option<Response> {
|
||||
with_permissions!(user, target);
|
||||
Some(Response::Success(target.serialise()))
|
||||
}
|
||||
|
||||
/// [groups] add user to channel
|
||||
#[put("/<target>/recipients/<member>")]
|
||||
pub fn add_member(user: User, target: Channel, member: User) -> Option<Response> {
|
||||
if target.channel_type != 1 {
|
||||
return Some(Response::BadRequest(json!({ "error": "Not a group DM." })));
|
||||
}
|
||||
|
||||
with_permissions!(user, target);
|
||||
|
||||
let recp = target.recipients.as_ref().unwrap();
|
||||
if recp.len() == 50 {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "Maximum group size is 50." }),
|
||||
));
|
||||
}
|
||||
|
||||
let set = vec_to_set(&recp);
|
||||
if set.get(&member.id).is_some() {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "User already in group!" }),
|
||||
));
|
||||
}
|
||||
|
||||
match get_relationship(&user, &member) {
|
||||
Relationship::Friend => {
|
||||
if database::get_collection("channels")
|
||||
.update_one(
|
||||
doc! { "_id": target.id.clone() },
|
||||
doc! {
|
||||
"$push": {
|
||||
"recipients": &member.id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if (Message {
|
||||
id: Ulid::new().to_string(),
|
||||
nonce: None,
|
||||
channel: target.id.clone(),
|
||||
author: "system".to_string(),
|
||||
content: format!("<@{}> added <@{}> to the group.", &user.id, &member.id),
|
||||
edited: None,
|
||||
previous_content: vec![],
|
||||
})
|
||||
.send(&target)
|
||||
{
|
||||
notifications::send_message_given_channel(
|
||||
Notification::group_user_join(UserJoin {
|
||||
id: target.id.clone(),
|
||||
user: member.id.clone(),
|
||||
}),
|
||||
&target,
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::PartialStatus(
|
||||
json!({ "error": "Failed to send join message, but user has been added." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to add user to group." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Some(Response::BadRequest(
|
||||
json!({ "error": "Not friends with user." }),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// [groups] remove user from channel
|
||||
#[delete("/<target>/recipients/<member>")]
|
||||
pub fn remove_member(user: User, target: Channel, member: User) -> Option<Response> {
|
||||
if target.channel_type != 1 {
|
||||
return Some(Response::BadRequest(json!({ "error": "Not a group DM." })));
|
||||
}
|
||||
|
||||
if &user.id == &member.id {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "Cannot kick yourself, leave the channel instead." }),
|
||||
));
|
||||
}
|
||||
|
||||
let permissions = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_kick_members() {
|
||||
return Some(Response::LackingPermission(Permission::KickMembers));
|
||||
}
|
||||
|
||||
let set = vec_to_set(target.recipients.as_ref().unwrap());
|
||||
if set.get(&member.id).is_none() {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "User not in group!" }),
|
||||
));
|
||||
}
|
||||
|
||||
if database::get_collection("channels")
|
||||
.update_one(
|
||||
doc! { "_id": &target.id },
|
||||
doc! {
|
||||
"$pull": {
|
||||
"recipients": &member.id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if (Message {
|
||||
id: Ulid::new().to_string(),
|
||||
nonce: None,
|
||||
channel: target.id.clone(),
|
||||
author: "system".to_string(),
|
||||
content: format!("<@{}> removed <@{}> from the group.", &user.id, &member.id),
|
||||
edited: None,
|
||||
previous_content: vec![],
|
||||
})
|
||||
.send(&target)
|
||||
{
|
||||
notifications::send_message_given_channel(
|
||||
Notification::group_user_leave(UserLeave {
|
||||
id: target.id.clone(),
|
||||
user: member.id.clone(),
|
||||
}),
|
||||
&target,
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::PartialStatus(
|
||||
json!({ "error": "Failed to send join message, but user has been removed." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to add user to group." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// delete channel
|
||||
/// or leave group DM
|
||||
/// or close DM conversation
|
||||
#[delete("/<target>")]
|
||||
pub fn delete(user: User, target: Channel) -> Option<Response> {
|
||||
let permissions = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_manage_channels() {
|
||||
return Some(Response::LackingPermission(Permission::ManageChannels));
|
||||
}
|
||||
|
||||
let col = database::get_collection("channels");
|
||||
let target_id = target.id.clone();
|
||||
|
||||
let try_delete = || {
|
||||
let messages = database::get_collection("messages");
|
||||
|
||||
if messages
|
||||
.delete_many(doc! { "channel": &target_id }, None)
|
||||
.is_ok()
|
||||
{
|
||||
if col.delete_one(doc! { "_id": &target_id }, None).is_ok() {
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to delete channel." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to delete messages." }),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match target.channel_type {
|
||||
0 => {
|
||||
if col
|
||||
.update_one(
|
||||
doc! { "_id": &target_id },
|
||||
doc! { "$set": { "active": false } },
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to close channel." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let mut recipients = vec_to_set(
|
||||
target
|
||||
.recipients
|
||||
.as_ref()
|
||||
.expect("Missing recipients on Group DM."),
|
||||
);
|
||||
let owner = target.owner.as_ref().expect("Missing owner on Group DM.");
|
||||
|
||||
if recipients.len() == 1 {
|
||||
try_delete()
|
||||
} else {
|
||||
recipients.remove(&user.id);
|
||||
let new_owner = if owner == &user.id {
|
||||
recipients.iter().next().unwrap()
|
||||
} else {
|
||||
&owner
|
||||
};
|
||||
|
||||
if col
|
||||
.update_one(
|
||||
doc! { "_id": target_id },
|
||||
doc! {
|
||||
"$set": {
|
||||
"owner": new_owner,
|
||||
},
|
||||
"$pull": {
|
||||
"recipients": &user.id,
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if (Message {
|
||||
id: Ulid::new().to_string(),
|
||||
nonce: None,
|
||||
channel: target.id.clone(),
|
||||
author: "system".to_string(),
|
||||
content: format!("<@{}> left the group.", &user.id),
|
||||
edited: None,
|
||||
previous_content: vec![],
|
||||
})
|
||||
.send(&target)
|
||||
{
|
||||
notifications::send_message_given_channel(
|
||||
Notification::group_user_leave(UserLeave {
|
||||
id: target.id.clone(),
|
||||
user: user.id.clone(),
|
||||
}),
|
||||
&target,
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::PartialStatus(
|
||||
json!({ "error": "Failed to send leave message, but you have left the group." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to remove you from the group." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let guild_id = target.guild.unwrap();
|
||||
if database::get_collection("guilds")
|
||||
.update_one(
|
||||
doc! { "_id": &guild_id },
|
||||
doc! {
|
||||
"$pull": {
|
||||
"invites": {
|
||||
"channel": &target.id
|
||||
},
|
||||
"channels": &target.id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
notifications::send_message_threaded(
|
||||
None,
|
||||
guild_id.clone(),
|
||||
Notification::guild_channel_delete(ChannelDelete {
|
||||
id: guild_id.clone(),
|
||||
channel: target.id.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
try_delete()
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to remove invites." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
_ => Some(Response::InternalServerError(
|
||||
json!({ "error": "Unknown error has occurred." }),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromForm)]
|
||||
pub struct MessageFetchOptions {
|
||||
limit: Option<i64>,
|
||||
before: Option<String>,
|
||||
after: Option<String>,
|
||||
}
|
||||
|
||||
/// fetch channel messages
|
||||
#[get("/<target>/messages?<options..>")]
|
||||
pub fn messages(
|
||||
user: User,
|
||||
target: Channel,
|
||||
options: Form<MessageFetchOptions>,
|
||||
) -> Option<Response> {
|
||||
let permissions = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_read_messages() {
|
||||
return Some(Response::LackingPermission(Permission::ReadMessages));
|
||||
}
|
||||
|
||||
// ! FIXME: update wiki to reflect changes
|
||||
let mut query = doc! { "channel": target.id };
|
||||
|
||||
if let Some(before) = &options.before {
|
||||
query.insert("_id", doc! { "$lt": before });
|
||||
}
|
||||
|
||||
if let Some(after) = &options.after {
|
||||
query.insert("_id", doc! { "$gt": after });
|
||||
}
|
||||
|
||||
let limit = if let Some(limit) = options.limit {
|
||||
limit.min(100).max(0)
|
||||
} else {
|
||||
50
|
||||
};
|
||||
|
||||
let col = database::get_collection("messages");
|
||||
let result = col
|
||||
.find(
|
||||
query,
|
||||
FindOptions::builder()
|
||||
.limit(limit)
|
||||
.sort(doc! {
|
||||
"_id": -1
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut messages = Vec::new();
|
||||
for item in result {
|
||||
let message: Message =
|
||||
from_bson(Bson::Document(item.unwrap())).expect("Failed to unwrap message.");
|
||||
messages.push(json!({
|
||||
"id": message.id,
|
||||
"author": message.author,
|
||||
"content": message.content,
|
||||
"edited": if let Some(t) = message.edited { Some(t.timestamp()) } else { None }
|
||||
}));
|
||||
}
|
||||
|
||||
Some(Response::Success(json!(messages)))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SendMessage {
|
||||
content: String,
|
||||
nonce: String,
|
||||
}
|
||||
|
||||
/// send a message to a channel
|
||||
#[post("/<target>/messages", data = "<message>")]
|
||||
pub fn send_message(
|
||||
user: User,
|
||||
target: Channel,
|
||||
message: Json<SendMessage>,
|
||||
) -> Option<Response> {
|
||||
let permissions = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_send_messages() {
|
||||
if target.channel_type == 0 {
|
||||
return Some(Response::LackingPermission(Permission::SendDirectMessages));
|
||||
}
|
||||
|
||||
return Some(Response::LackingPermission(Permission::SendMessages));
|
||||
}
|
||||
|
||||
let content: String = message.content.chars().take(2000).collect();
|
||||
let nonce: String = message.nonce.chars().take(32).collect();
|
||||
|
||||
if content.len() == 0 {
|
||||
return Some(Response::NotAcceptable(
|
||||
json!({ "error": "No message content!" }),
|
||||
));
|
||||
}
|
||||
|
||||
let col = database::get_collection("messages");
|
||||
if col
|
||||
.find_one(doc! { "nonce": nonce.clone() }, None)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "Message already sent!" }),
|
||||
));
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
let message = Message {
|
||||
id: id.clone(),
|
||||
nonce: Some(nonce),
|
||||
channel: target.id.clone(),
|
||||
author: user.id,
|
||||
content,
|
||||
edited: None,
|
||||
previous_content: vec![],
|
||||
};
|
||||
|
||||
if message.send(&target) {
|
||||
Some(Response::Success(json!({ "id": id })))
|
||||
} else {
|
||||
Some(Response::BadRequest(
|
||||
json!({ "error": "Failed to send message." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// get a message
|
||||
#[get("/<target>/messages/<message>")]
|
||||
pub fn get_message(user: User, target: Channel, message: Message) -> Option<Response> {
|
||||
let permissions = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_read_messages() {
|
||||
return Some(Response::LackingPermission(Permission::ReadMessages));
|
||||
}
|
||||
|
||||
// ! CHECK IF USER HAS PERMISSION TO VIEW EDITS OF MESSAGES
|
||||
let mut entries = vec![];
|
||||
for entry in message.previous_content {
|
||||
entries.push(json!({
|
||||
"content": entry.content,
|
||||
"time": entry.time.timestamp(),
|
||||
}));
|
||||
}
|
||||
|
||||
Some(Response::Success(json!({
|
||||
"id": message.id,
|
||||
"author": message.author,
|
||||
"content": message.content,
|
||||
"edited": if let Some(t) = message.edited { Some(t.timestamp()) } else { None },
|
||||
"previous_content": entries,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct EditMessage {
|
||||
content: String,
|
||||
}
|
||||
|
||||
/// edit a message
|
||||
#[patch("/<target>/messages/<message>", data = "<edit>")]
|
||||
pub fn edit_message(
|
||||
user: User,
|
||||
target: Channel,
|
||||
message: Message,
|
||||
edit: Json<EditMessage>,
|
||||
) -> Option<Response> {
|
||||
with_permissions!(user, target);
|
||||
|
||||
if message.author != user.id {
|
||||
return Some(Response::Unauthorized(
|
||||
json!({ "error": "You did not send this message." }),
|
||||
));
|
||||
}
|
||||
|
||||
let col = database::get_collection("messages");
|
||||
let time = if let Some(edited) = message.edited {
|
||||
edited.0
|
||||
} else {
|
||||
Ulid::from_string(&message.id).unwrap().datetime()
|
||||
};
|
||||
|
||||
let edited = Utc::now();
|
||||
match col.update_one(
|
||||
doc! { "_id": message.id.clone() },
|
||||
doc! {
|
||||
"$set": {
|
||||
"content": &edit.content,
|
||||
"edited": Bson::DateTime(edited)
|
||||
},
|
||||
"$push": {
|
||||
"previous_content": {
|
||||
"content": &message.content,
|
||||
"time": time,
|
||||
}
|
||||
},
|
||||
},
|
||||
None,
|
||||
) {
|
||||
Ok(_) => {
|
||||
notifications::send_message_given_channel(
|
||||
Notification::message_edit(Edit {
|
||||
id: message.id.clone(),
|
||||
channel: target.id.clone(),
|
||||
author: message.author.clone(),
|
||||
content: edit.content.clone(),
|
||||
}),
|
||||
&target,
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
}
|
||||
Err(_) => Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to update message." }),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// delete a message
|
||||
#[delete("/<target>/messages/<message>")]
|
||||
pub fn delete_message(user: User, target: Channel, message: Message) -> Option<Response> {
|
||||
let permissions = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_manage_messages() {
|
||||
if message.author != user.id {
|
||||
return Some(Response::LackingPermission(Permission::ManageMessages));
|
||||
}
|
||||
}
|
||||
|
||||
let col = database::get_collection("messages");
|
||||
|
||||
match col.delete_one(doc! { "_id": &message.id }, None) {
|
||||
Ok(_) => {
|
||||
notifications::send_message_given_channel(
|
||||
Notification::message_delete(Delete {
|
||||
id: message.id.clone(),
|
||||
}),
|
||||
&target,
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
}
|
||||
Err(_) => Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to delete message." }),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[options("/create")]
|
||||
pub fn create_group_preflight() -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>")]
|
||||
pub fn channel_preflight(_target: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/recipients/<_member>")]
|
||||
pub fn member_preflight(_target: String, _member: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/messages")]
|
||||
pub fn messages_preflight(_target: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/messages/<_message>")]
|
||||
pub fn message_preflight(_target: String, _message: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
23
src/routes/channels/channel_ack.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
|
||||
|
||||
/// # Acknowledge Message
|
||||
///
|
||||
/// Lets the server and all other clients know that we've seen this message id in this channel.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[put("/<target>/ack/<message>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, message: Ref) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(db, Permission::ViewChannel)
|
||||
.await?;
|
||||
|
||||
channel
|
||||
.ack(&user.id, &message.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
41
src/routes/channels/channel_delete.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use revolt_quark::{
|
||||
models::{channel::PartialChannel, Channel, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Close Channel
|
||||
///
|
||||
/// Deletes a server channel, leaves a group or closes a group.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[delete("/<target>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
let mut perms = perms(&user).channel(&channel);
|
||||
perms.throw_permission(db, Permission::ViewChannel).await?;
|
||||
|
||||
match &channel {
|
||||
Channel::SavedMessages { .. } => Err(Error::NoEffect),
|
||||
Channel::DirectMessage { .. } => channel
|
||||
.update(
|
||||
db,
|
||||
PartialChannel {
|
||||
active: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::Group { .. } => channel
|
||||
.remove_user_from_group(db, &user.id, None)
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
|
||||
perms
|
||||
.throw_permission(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
|
||||
channel.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
173
src/routes/channels/channel_edit.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
channel::{Channel, FieldsChannel, PartialChannel},
|
||||
message::SystemMessage,
|
||||
File, User,
|
||||
},
|
||||
perms, Database, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::{serde::json::Json, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Channel Details
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataEditChannel {
|
||||
/// Channel name
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
/// Channel description
|
||||
#[validate(length(min = 0, max = 1024))]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
/// Icon
|
||||
///
|
||||
/// Provide an Autumn attachment Id.
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
icon: Option<String>,
|
||||
/// Whether this channel is age-restricted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
nsfw: Option<bool>,
|
||||
#[validate(length(min = 1))]
|
||||
remove: Option<Vec<FieldsChannel>>,
|
||||
}
|
||||
|
||||
/// # Edit Channel
|
||||
///
|
||||
/// Edit a channel object by its id.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[patch("/<target>", data = "<data>")]
|
||||
pub async fn req(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataEditChannel>,
|
||||
) -> Result<Json<Channel>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
|
||||
if data.name.is_none()
|
||||
&& data.description.is_none()
|
||||
&& data.icon.is_none()
|
||||
&& data.nsfw.is_none()
|
||||
&& data.remove.is_none()
|
||||
{
|
||||
return Ok(Json(channel));
|
||||
}
|
||||
|
||||
let mut partial: PartialChannel = Default::default();
|
||||
match &mut channel {
|
||||
Channel::Group {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
..
|
||||
}
|
||||
| Channel::TextChannel {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
..
|
||||
} => {
|
||||
if let Some(fields) = &data.remove {
|
||||
if fields.contains(&FieldsChannel::Icon) {
|
||||
if let Some(icon) = &icon {
|
||||
db.mark_attachment_as_deleted(&icon.id).await?;
|
||||
}
|
||||
}
|
||||
|
||||
for field in fields {
|
||||
match field {
|
||||
FieldsChannel::Description => {
|
||||
description.take();
|
||||
}
|
||||
FieldsChannel::Icon => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon_id) = data.icon {
|
||||
partial.icon = Some(File::use_icon(db, &icon_id, id).await?);
|
||||
*icon = partial.icon.clone();
|
||||
}
|
||||
|
||||
if let Some(new_name) = data.name {
|
||||
*name = new_name.clone();
|
||||
partial.name = Some(new_name);
|
||||
}
|
||||
|
||||
if let Some(new_description) = data.description {
|
||||
partial.description = Some(new_description);
|
||||
*description = partial.description.clone();
|
||||
}
|
||||
|
||||
if let Some(new_nsfw) = data.nsfw {
|
||||
*nsfw = new_nsfw;
|
||||
partial.nsfw = Some(new_nsfw);
|
||||
}
|
||||
|
||||
// Send out mutation system messages.
|
||||
if let Channel::Group { .. } = &channel {
|
||||
if let Some(name) = &partial.name {
|
||||
SystemMessage::ChannelRenamed {
|
||||
name: name.to_string(),
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if partial.description.is_some() {
|
||||
SystemMessage::ChannelDescriptionChanged {
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if partial.icon.is_some() {
|
||||
SystemMessage::ChannelIconChanged { by: user.id }
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
channel
|
||||
.update(db, partial, data.remove.unwrap_or_default())
|
||||
.await?;
|
||||
}
|
||||
_ => return Err(Error::InvalidOperation),
|
||||
};
|
||||
|
||||
Ok(Json(channel))
|
||||
}
|
||||
21
src/routes/channels/channel_fetch.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms, Database, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Fetch Channel
|
||||
///
|
||||
/// Fetch channel by its id.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[get("/<target>")]
|
||||
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<Channel>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(db, Permission::ViewChannel)
|
||||
.await?;
|
||||
|
||||
Ok(Json(channel))
|
||||
}
|
||||
32
src/routes/channels/group_add_member.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Add Member to Group
|
||||
///
|
||||
/// Adds another user to the group.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[put("/<target>/recipients/<member>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::InviteOthers)
|
||||
.await?;
|
||||
|
||||
match &channel {
|
||||
Channel::Group { .. } => {
|
||||
let member = member.as_user(db).await?;
|
||||
channel
|
||||
.add_user_to_group(db, &member.id, &user.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
_ => Err(Error::InvalidOperation),
|
||||
}
|
||||
}
|
||||
84
src/routes/channels/group_create.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use std::{collections::HashSet, iter::FromIterator};
|
||||
|
||||
use revolt_quark::{
|
||||
get_relationship,
|
||||
models::{user::RelationshipStatus, Channel, User},
|
||||
variables::delta::MAX_GROUP_SIZE,
|
||||
Db, Error, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
/// # Group Data
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataCreateGroup {
|
||||
/// Group name
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
name: String,
|
||||
/// Group description
|
||||
#[validate(length(min = 0, max = 1024))]
|
||||
description: Option<String>,
|
||||
/// Array of user IDs to add to the group
|
||||
///
|
||||
/// Must be friends with these users.
|
||||
#[validate(length(min = 0, max = 49))]
|
||||
users: Vec<String>,
|
||||
/// Whether this group is age-restricted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Create Group
|
||||
///
|
||||
/// Create a new group channel.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[post("/create", data = "<info>")]
|
||||
pub async fn req(db: &Db, user: User, info: Json<DataCreateGroup>) -> Result<Json<Channel>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let info = info.into_inner();
|
||||
info.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let mut set: HashSet<String> = HashSet::from_iter(info.users.into_iter());
|
||||
set.insert(user.id.clone());
|
||||
|
||||
if set.len() > *MAX_GROUP_SIZE {
|
||||
return Err(Error::GroupTooLarge {
|
||||
max: *MAX_GROUP_SIZE,
|
||||
});
|
||||
}
|
||||
|
||||
for target in &set {
|
||||
match get_relationship(&user, target) {
|
||||
RelationshipStatus::Friend | RelationshipStatus::User => {}
|
||||
_ => {
|
||||
return Err(Error::NotFriends);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let group = Channel::Group {
|
||||
id: Ulid::new().to_string(),
|
||||
|
||||
name: info.name,
|
||||
owner: user.id,
|
||||
description: info.description,
|
||||
recipients: set.into_iter().collect::<Vec<String>>(),
|
||||
|
||||
icon: None,
|
||||
last_message_id: None,
|
||||
|
||||
permissions: None,
|
||||
|
||||
nsfw: info.nsfw.unwrap_or(false),
|
||||
};
|
||||
|
||||
group.create(db).await?;
|
||||
Ok(Json(group))
|
||||
}
|
||||
42
src/routes/channels/group_remove_member.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Remove Member from Group
|
||||
///
|
||||
/// Removes a user from the group.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[delete("/<target>/recipients/<member>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
match &channel {
|
||||
Channel::Group {
|
||||
owner, recipients, ..
|
||||
} => {
|
||||
if &user.id != owner {
|
||||
return Error::from_permission(Permission::ManageChannel);
|
||||
}
|
||||
|
||||
let member = member.as_user(db).await?;
|
||||
if user.id == member.id {
|
||||
return Err(Error::CannotRemoveYourself);
|
||||
}
|
||||
|
||||
if !recipients.iter().any(|x| *x == member.id) {
|
||||
return Err(Error::NotInGroup);
|
||||
}
|
||||
|
||||
channel
|
||||
.remove_user_from_group(db, &member.id, Some(&user.id))
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
_ => Err(Error::InvalidOperation),
|
||||
}
|
||||
}
|
||||
27
src/routes/channels/invite_create.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use revolt_quark::{
|
||||
models::{Invite, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Create Invite
|
||||
///
|
||||
/// Creates an invite to this channel.
|
||||
///
|
||||
/// Channel must be a `TextChannel`.
|
||||
#[openapi(tag = "Channel Invites")]
|
||||
#[post("/<target>/invites")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Invite>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::InviteOthers)
|
||||
.await?;
|
||||
|
||||
Invite::create(db, &user, &channel).await.map(Json)
|
||||
}
|
||||
35
src/routes/channels/members_fetch.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Fetch Group Members
|
||||
///
|
||||
/// Retrieves all users who are part of this group.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[get("/<target>/members")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(db, Permission::ViewChannel)
|
||||
.await?;
|
||||
|
||||
if let Channel::Group { recipients, .. } = channel {
|
||||
Ok(Json(
|
||||
db.fetch_users(&recipients)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.with_relationship(&user))
|
||||
.collect::<Vec<User>>(),
|
||||
))
|
||||
} else {
|
||||
Err(Error::InvalidOperation)
|
||||
}
|
||||
}
|
||||
59
src/routes/channels/message_bulk_delete.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use chrono::Utc;
|
||||
use revolt_quark::{
|
||||
models::{Message, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Deserialize;
|
||||
use validator::Validate;
|
||||
|
||||
/// # Search Parameters
|
||||
#[derive(Validate, Deserialize, JsonSchema)]
|
||||
pub struct OptionsBulkDelete {
|
||||
/// Message IDs
|
||||
#[validate(length(min = 1, max = 100))]
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// # Bulk Delete Messages
|
||||
///
|
||||
/// Delete multiple messages you've sent or one you have permission to delete.
|
||||
///
|
||||
/// This will always require `ManageMessages` permission regardless of whether you own the message or not.
|
||||
///
|
||||
/// Messages must have been sent within the past 1 week.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: Json<OptionsBulkDelete>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let options = options.into_inner();
|
||||
options
|
||||
.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
for id in &options.ids {
|
||||
if ulid::Ulid::from_string(id)
|
||||
.map_err(|_| Error::InvalidOperation)?
|
||||
.datetime()
|
||||
.signed_duration_since(Utc::now())
|
||||
.num_days()
|
||||
.abs()
|
||||
> 7
|
||||
{
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
|
||||
perms(&user)
|
||||
.channel(&target.as_channel(db).await?)
|
||||
.throw_permission(db, Permission::ManageMessages)
|
||||
.await?;
|
||||
|
||||
Message::bulk_delete(db, &target.id, options.ids)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
22
src/routes/channels/message_delete.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
|
||||
|
||||
/// # Delete Message
|
||||
///
|
||||
/// Delete a message you've sent or one you have permission to delete.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[delete("/<target>/messages/<msg>", rank = 2)]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
|
||||
let message = msg.as_message(db).await?;
|
||||
if message.channel != target.id {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
if message.author != user.id {
|
||||
perms(&user)
|
||||
.channel(&target.as_channel(db).await?)
|
||||
.throw_permission(db, Permission::ManageMessages)
|
||||
.await?;
|
||||
}
|
||||
|
||||
message.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
93
src/routes/channels/message_edit.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use revolt_quark::{
|
||||
models::message::{PartialMessage, SendableEmbed},
|
||||
models::{Message, User},
|
||||
types::january::Embed,
|
||||
Db, Error, Ref, Result, Timestamp,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Message Details
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataEditMessage {
|
||||
/// New message content
|
||||
#[validate(length(min = 1, max = 2000))]
|
||||
content: Option<String>,
|
||||
/// Embeds to include in the message
|
||||
#[validate(length(min = 0, max = 10))]
|
||||
embeds: Option<Vec<SendableEmbed>>,
|
||||
}
|
||||
|
||||
/// # Edit Message
|
||||
///
|
||||
/// Edits a message that you've previously sent.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[patch("/<target>/messages/<msg>", data = "<edit>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: String,
|
||||
msg: Ref,
|
||||
edit: Json<DataEditMessage>,
|
||||
) -> Result<Json<Message>> {
|
||||
let edit = edit.into_inner();
|
||||
edit.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let mut message = msg.as_message(db).await?;
|
||||
if message.channel != target {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
if message.author != user.id {
|
||||
return Err(Error::CannotEditMessage);
|
||||
}
|
||||
|
||||
message.edited = Some(Timestamp::now_utc());
|
||||
let mut partial = PartialMessage {
|
||||
edited: message.edited,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// 1. Handle content update
|
||||
if let Some(content) = &edit.content {
|
||||
partial.content = Some(content.clone());
|
||||
}
|
||||
|
||||
// 2. Clear any auto generated embeds
|
||||
let mut new_embeds: Vec<Embed> = vec![];
|
||||
if let Some(embeds) = &message.embeds {
|
||||
for embed in embeds {
|
||||
if let Embed::Text(embed) = embed {
|
||||
new_embeds.push(Embed::Text(embed.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Replace if we are given new embeds
|
||||
if let Some(embeds) = edit.embeds {
|
||||
new_embeds.clear();
|
||||
|
||||
for embed in embeds {
|
||||
new_embeds.push(embed.clone().into_embed(db, message.id.clone()).await?);
|
||||
}
|
||||
}
|
||||
|
||||
partial.embeds = Some(new_embeds);
|
||||
|
||||
message.update(db, partial).await?;
|
||||
|
||||
// Queue up a task for processing embeds
|
||||
if let Some(content) = edit.content {
|
||||
revolt_quark::tasks::process_embeds::queue(
|
||||
message.channel.to_string(),
|
||||
message.id.to_string(),
|
||||
content,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(Json(message))
|
||||
}
|
||||
26
src/routes/channels/message_fetch.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use revolt_quark::{
|
||||
models::{Message, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Fetch Message
|
||||
///
|
||||
/// Retrieves a message by its id.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[get("/<target>/messages/<msg>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<Json<Message>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(db, Permission::ViewChannel)
|
||||
.await?;
|
||||
|
||||
let message = msg.as_message(db).await?;
|
||||
if message.channel != channel.as_id() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
|
||||
Ok(Json(message))
|
||||
}
|
||||
81
src/routes/channels/message_query.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
message::{BulkMessageResponse, MessageSort},
|
||||
User,
|
||||
},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
|
||||
pub struct OptionsQueryMessages {
|
||||
/// Maximum number of messages to fetch
|
||||
///
|
||||
/// For fetching nearby messages, this is \`(limit + 1)\`.
|
||||
#[validate(range(min = 1, max = 100))]
|
||||
limit: Option<i64>,
|
||||
/// Message id before which messages should be fetched
|
||||
#[validate(length(min = 26, max = 26))]
|
||||
before: Option<String>,
|
||||
/// Message id after which messages should be fetched
|
||||
#[validate(length(min = 26, max = 26))]
|
||||
after: Option<String>,
|
||||
/// Message sort direction
|
||||
sort: Option<MessageSort>,
|
||||
/// Message id to search around
|
||||
///
|
||||
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
|
||||
/// It will also take half of limit rounded as the limits to each side.
|
||||
/// It also fetches the message ID specified.
|
||||
#[validate(length(min = 26, max = 26))]
|
||||
nearby: Option<String>,
|
||||
/// Whether to include user (and member, if server channel) objects
|
||||
include_users: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Fetch Messages
|
||||
///
|
||||
/// Fetch multiple messages.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[get("/<target>/messages?<options..>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: OptionsQueryMessages,
|
||||
) -> Result<Json<BulkMessageResponse>> {
|
||||
options
|
||||
.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
if let Some(MessageSort::Relevance) = options.sort {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::ReadMessageHistory)
|
||||
.await?;
|
||||
|
||||
let OptionsQueryMessages {
|
||||
limit,
|
||||
before,
|
||||
after,
|
||||
sort,
|
||||
nearby,
|
||||
include_users,
|
||||
..
|
||||
} = options;
|
||||
|
||||
let messages = db
|
||||
.fetch_messages(channel.id(), limit, before, after, sort, nearby)
|
||||
.await?;
|
||||
|
||||
BulkMessageResponse::transform(db, &channel, messages, include_users)
|
||||
.await
|
||||
.map(Json)
|
||||
}
|
||||
26
src/routes/channels/message_query_stale.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use revolt_quark::{models::User, Ref, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct OptionsQueryStale {
|
||||
/// Array of message IDs
|
||||
#[validate(length(min = 0, max = 150))]
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// # Poll Message Changes
|
||||
///
|
||||
/// This route returns any changed message objects and tells you if any have been deleted.
|
||||
///
|
||||
/// Don't actually poll this route, instead use this to update your local database.
|
||||
///
|
||||
/// **DEPRECATED**
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[post("/<_target>/messages/stale", data = "<_data>")]
|
||||
pub async fn req(_user: User, _target: Ref, _data: Json<OptionsQueryStale>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
82
src/routes/channels/message_search.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
message::{BulkMessageResponse, MessageSort},
|
||||
User,
|
||||
},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Search Parameters
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct OptionsMessageSearch {
|
||||
/// Full-text search query
|
||||
///
|
||||
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
|
||||
#[validate(length(min = 1, max = 64))]
|
||||
query: String,
|
||||
|
||||
/// Maximum number of messages to fetch
|
||||
#[validate(range(min = 1, max = 100))]
|
||||
limit: Option<i64>,
|
||||
/// Message id before which messages should be fetched
|
||||
#[validate(length(min = 26, max = 26))]
|
||||
before: Option<String>,
|
||||
/// Message id after which messages should be fetched
|
||||
#[validate(length(min = 26, max = 26))]
|
||||
after: Option<String>,
|
||||
/// Message sort direction
|
||||
///
|
||||
/// By default, it will be sorted by relevance.
|
||||
#[serde(default = "MessageSort::default")]
|
||||
sort: MessageSort,
|
||||
/// Whether to include user (and member, if server channel) objects
|
||||
include_users: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Search for Messages
|
||||
///
|
||||
/// This route searches for messages within the given parameters.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[post("/<target>/search", data = "<options>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: Json<OptionsMessageSearch>,
|
||||
) -> Result<Json<BulkMessageResponse>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let options = options.into_inner();
|
||||
options
|
||||
.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::ReadMessageHistory)
|
||||
.await?;
|
||||
|
||||
let OptionsMessageSearch {
|
||||
query,
|
||||
limit,
|
||||
before,
|
||||
after,
|
||||
sort,
|
||||
include_users,
|
||||
} = options;
|
||||
|
||||
let messages = db
|
||||
.search_messages(channel.id(), &query, limit, before, after, sort)
|
||||
.await?;
|
||||
|
||||
BulkMessageResponse::transform(db, &channel, messages, include_users)
|
||||
.await
|
||||
.map(Json)
|
||||
}
|
||||
194
src/routes/channels/message_send.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
message::{Masquerade, Reply, SendableEmbed},
|
||||
Message, User,
|
||||
},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::util::idempotency::IdempotencyKey;
|
||||
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataMessageSend {
|
||||
/// Unique token to prevent duplicate message sending
|
||||
///
|
||||
/// **This is deprecated and replaced by `Idempotency-Key`!**
|
||||
nonce: Option<String>,
|
||||
|
||||
/// Message content to send
|
||||
#[validate(length(min = 0, max = 2000))]
|
||||
content: Option<String>,
|
||||
/// Attachments to include in message
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
attachments: Option<Vec<String>>,
|
||||
/// Messages to reply to
|
||||
replies: Option<Vec<Reply>>,
|
||||
/// Embeds to include in message
|
||||
#[validate(length(min = 1, max = 10))]
|
||||
embeds: Option<Vec<SendableEmbed>>,
|
||||
/// Masquerade to apply to this message
|
||||
#[validate]
|
||||
masquerade: Option<Masquerade>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
// ignoring I L O and U is intentional
|
||||
static ref RE_MENTION: Regex = Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap();
|
||||
}
|
||||
|
||||
/// # Send Message
|
||||
///
|
||||
/// Sends a message to the given channel.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[post("/<target>/messages", data = "<data>")]
|
||||
pub async fn message_send(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataMessageSend>,
|
||||
mut idempotency: IdempotencyKey,
|
||||
) -> Result<Json<Message>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
idempotency.consume_nonce(data.nonce).await?;
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
||||
.await?;
|
||||
|
||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
||||
{
|
||||
return Err(Error::EmptyMessage);
|
||||
}
|
||||
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: channel.id().to_string(),
|
||||
author: user.id.clone(),
|
||||
masquerade: data.masquerade,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// 1. Parse mentions in message.
|
||||
let mut mentions = HashSet::new();
|
||||
if let Some(content) = &data.content {
|
||||
for capture in RE_MENTION.captures_iter(content) {
|
||||
if let Some(mention) = capture.get(1) {
|
||||
mentions.insert(mention.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Verify permissions for masquerade.
|
||||
if message.masquerade.is_some() {
|
||||
permissions
|
||||
.throw_permission(db, Permission::Masquerade)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 3. Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > 5 {
|
||||
return Err(Error::TooManyReplies);
|
||||
}
|
||||
|
||||
for Reply { id, mention } in entries {
|
||||
let message = Ref::from_unchecked(id).as_message(db).await?;
|
||||
|
||||
replies.insert(message.id);
|
||||
|
||||
if mention {
|
||||
mentions.insert(message.author);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mentions.is_empty() {
|
||||
message.mentions.replace(
|
||||
mentions
|
||||
.into_iter()
|
||||
.filter(|id| !user.has_blocked(id))
|
||||
.collect::<Vec<String>>(),
|
||||
);
|
||||
}
|
||||
|
||||
if !replies.is_empty() {
|
||||
message
|
||||
.replies
|
||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
||||
}
|
||||
|
||||
// 4. Add attachments to message.
|
||||
let mut attachments = vec![];
|
||||
if let Some(ids) = &data.attachments {
|
||||
if !ids.is_empty() {
|
||||
permissions
|
||||
.throw_permission(db, Permission::UploadFiles)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// ! FIXME: move this to app config
|
||||
if ids.len() > 5 {
|
||||
return Err(Error::TooManyAttachments);
|
||||
}
|
||||
|
||||
for attachment_id in ids {
|
||||
attachments.push(
|
||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !attachments.is_empty() {
|
||||
message.attachments.replace(attachments);
|
||||
}
|
||||
|
||||
// 5. Process included embeds.
|
||||
let mut embeds = vec![];
|
||||
if let Some(sendable_embeds) = data.embeds {
|
||||
for sendable_embed in sendable_embeds {
|
||||
embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?)
|
||||
}
|
||||
}
|
||||
|
||||
if !embeds.is_empty() {
|
||||
message.embeds.replace(embeds);
|
||||
}
|
||||
|
||||
// 6. Set content
|
||||
message.content = data.content;
|
||||
|
||||
// 7. Pass-through nonce value for clients
|
||||
message.nonce = Some(idempotency.into_key());
|
||||
|
||||
message.create(db, &channel, Some(&user)).await?;
|
||||
|
||||
// Queue up a task for processing embeds
|
||||
if let Some(content) = &message.content {
|
||||
revolt_quark::tasks::process_embeds::queue(
|
||||
channel.id().to_string(),
|
||||
message.id.to_string(),
|
||||
content.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(Json(message))
|
||||
}
|
||||
48
src/routes/channels/mod.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod channel_ack;
|
||||
mod channel_delete;
|
||||
mod channel_edit;
|
||||
mod channel_fetch;
|
||||
mod group_add_member;
|
||||
mod group_create;
|
||||
mod group_remove_member;
|
||||
mod invite_create;
|
||||
mod members_fetch;
|
||||
mod message_bulk_delete;
|
||||
mod message_delete;
|
||||
mod message_edit;
|
||||
mod message_fetch;
|
||||
mod message_query;
|
||||
mod message_query_stale;
|
||||
mod message_search;
|
||||
mod message_send;
|
||||
mod permissions_set;
|
||||
mod permissions_set_default;
|
||||
mod voice_join;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
channel_ack::req,
|
||||
channel_fetch::req,
|
||||
members_fetch::req,
|
||||
channel_delete::req,
|
||||
channel_edit::req,
|
||||
invite_create::req,
|
||||
message_send::message_send,
|
||||
message_query::req,
|
||||
message_search::req,
|
||||
message_query_stale::req,
|
||||
message_fetch::req,
|
||||
message_edit::req,
|
||||
message_bulk_delete::req,
|
||||
message_delete::req,
|
||||
group_create::req,
|
||||
group_add_member::req,
|
||||
group_remove_member::req,
|
||||
voice_join::req,
|
||||
permissions_set::req,
|
||||
permissions_set_default::req,
|
||||
]
|
||||
}
|
||||
59
src/routes/channels/permissions_set.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Deserialize;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms, Db, Error, Override, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Permission Value
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct Data {
|
||||
/// Allow / deny values to set for this role
|
||||
permissions: Override,
|
||||
}
|
||||
|
||||
/// # Set Role Permission
|
||||
///
|
||||
/// Sets permissions for the specified role in this channel.
|
||||
///
|
||||
/// Channel must be a `TextChannel` or `VoiceChannel`.
|
||||
#[openapi(tag = "Channel Permissions")]
|
||||
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
role_id: String,
|
||||
data: Json<Data>,
|
||||
) -> Result<Json<Channel>> {
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::ManagePermissions)
|
||||
.await?;
|
||||
|
||||
if let Some(server) = permissions.server.get() {
|
||||
if let Some(role) = server.roles.get(&role_id) {
|
||||
if role.rank <= permissions.get_member_rank().unwrap_or(i64::MIN) {
|
||||
return Err(Error::NotElevated);
|
||||
}
|
||||
|
||||
let current_value: Override = role.permissions.into();
|
||||
permissions
|
||||
.throw_permission_override(db, current_value, data.permissions)
|
||||
.await?;
|
||||
|
||||
channel
|
||||
.set_role_permission(db, &role_id, data.permissions.into())
|
||||
.await?;
|
||||
|
||||
Ok(Json(channel))
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
} else {
|
||||
Err(Error::InvalidOperation)
|
||||
}
|
||||
}
|
||||
95
src/routes/channels/permissions_set_default.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Deserialize;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{channel::PartialChannel, Channel, User},
|
||||
perms, Db, Error, Override, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Permission Value
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum DataDefaultChannelPermissions {
|
||||
Value {
|
||||
/// Permission values to set for members in a `Group`
|
||||
permissions: u64,
|
||||
},
|
||||
Field {
|
||||
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
|
||||
permissions: Override,
|
||||
},
|
||||
}
|
||||
|
||||
/// # Set Default Permission
|
||||
///
|
||||
/// Sets permissions for the default role in this channel.
|
||||
///
|
||||
/// Channel must be a `Group`, `TextChannel` or `VoiceChannel`.
|
||||
#[openapi(tag = "Channel Permissions")]
|
||||
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataDefaultChannelPermissions>,
|
||||
) -> Result<Json<Channel>> {
|
||||
let data = data.into_inner();
|
||||
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
let mut perm = perms(&user).channel(&channel);
|
||||
|
||||
perm.throw_permission_and_view_channel(db, Permission::ManagePermissions)
|
||||
.await?;
|
||||
|
||||
match &channel {
|
||||
Channel::Group { .. } => {
|
||||
if let DataDefaultChannelPermissions::Value { permissions } = data {
|
||||
channel
|
||||
.update(
|
||||
db,
|
||||
PartialChannel {
|
||||
permissions: Some(permissions as i64),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
Channel::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
} => {
|
||||
if let DataDefaultChannelPermissions::Field { permissions } = data {
|
||||
perm.throw_permission_override(
|
||||
db,
|
||||
default_permissions.map(|x| x.into()),
|
||||
permissions,
|
||||
)
|
||||
.await?;
|
||||
|
||||
channel
|
||||
.update(
|
||||
db,
|
||||
PartialChannel {
|
||||
default_permissions: Some(permissions.into()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::InvalidOperation),
|
||||
}
|
||||
|
||||
Ok(Json(channel))
|
||||
}
|
||||
100
src/routes/channels/voice_join.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms,
|
||||
variables::delta::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL},
|
||||
Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Voice Server Token Response
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateVoiceUserResponse {
|
||||
/// Token for authenticating with the voice server
|
||||
token: String,
|
||||
}
|
||||
|
||||
/// # Join Call
|
||||
///
|
||||
/// Asks the voice server for a token to join the call.
|
||||
#[openapi(tag = "Voice")]
|
||||
#[post("/<target>/join_call")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUserResponse>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::Connect)
|
||||
.await?;
|
||||
|
||||
if !*USE_VOSO {
|
||||
return Err(Error::VosoUnavailable);
|
||||
}
|
||||
|
||||
match channel {
|
||||
Channel::SavedMessages { .. } | Channel::TextChannel { .. } => {
|
||||
return Err(Error::CannotJoinCall)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// To join a call:
|
||||
// - Check if the room exists.
|
||||
// - If not, create it.
|
||||
let client = reqwest::Client::new();
|
||||
let result = client
|
||||
.get(&format!("{}/room/{}", *VOSO_URL, channel.id()))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
VOSO_MANAGE_TOKEN.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Err(_) => return Err(Error::VosoUnavailable),
|
||||
Ok(result) => match result.status() {
|
||||
reqwest::StatusCode::OK => (),
|
||||
reqwest::StatusCode::NOT_FOUND => {
|
||||
if (client
|
||||
.post(&format!("{}/room/{}", *VOSO_URL, channel.id()))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
VOSO_MANAGE_TOKEN.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::VosoUnavailable);
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::VosoUnavailable),
|
||||
},
|
||||
}
|
||||
|
||||
// Then create a user for the room.
|
||||
if let Ok(response) = client
|
||||
.post(&format!(
|
||||
"{}/room/{}/user/{}",
|
||||
*VOSO_URL,
|
||||
channel.id(),
|
||||
user.id
|
||||
))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
VOSO_MANAGE_TOKEN.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|_| Error::InvalidOperation)
|
||||
.map(Json)
|
||||
} else {
|
||||
Err(Error::VosoUnavailable)
|
||||
}
|
||||
}
|
||||
@@ -1,836 +0,0 @@
|
||||
use super::channel::ChannelType;
|
||||
use super::Response;
|
||||
use crate::database::guild::{fetch_member as get_member, get_invite, Guild, MemberKey};
|
||||
use crate::database::{
|
||||
self, channel::fetch_channel, guild::serialise_guilds_with_channels, channel::Channel, Permission, PermissionCalculator, user::User
|
||||
};
|
||||
use crate::notifications::{
|
||||
self,
|
||||
events::{guilds::*, Notification},
|
||||
};
|
||||
use crate::util::gen_token;
|
||||
|
||||
use mongodb::bson::{doc, Bson};
|
||||
use mongodb::options::{FindOneOptions, FindOptions};
|
||||
use rocket::request::Form;
|
||||
use rocket_contrib::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
// ! FIXME: GET RID OF THIS
|
||||
macro_rules! with_permissions {
|
||||
($user: expr, $target: expr) => {{
|
||||
let permissions = PermissionCalculator::new($user.clone())
|
||||
.guild($target.clone())
|
||||
.fetch_data();
|
||||
|
||||
let value = permissions.as_permission();
|
||||
if !value.get_access() {
|
||||
return None;
|
||||
}
|
||||
|
||||
(value, permissions.member.unwrap())
|
||||
}};
|
||||
}
|
||||
|
||||
/// fetch your guilds
|
||||
#[get("/@me")]
|
||||
pub fn my_guilds(user: User) -> Response {
|
||||
if let Ok(gids) = user.find_guilds() {
|
||||
if let Ok(data) = serialise_guilds_with_channels(&gids) {
|
||||
Response::Success(json!(data))
|
||||
} else {
|
||||
Response::InternalServerError(json!({ "error": "Failed to fetch guilds." }))
|
||||
}
|
||||
} else {
|
||||
Response::InternalServerError(json!({ "error": "Failed to fetch memberships." }))
|
||||
}
|
||||
}
|
||||
|
||||
/// fetch a guild
|
||||
#[get("/<target>")]
|
||||
pub fn guild(user: User, target: Guild) -> Option<Response> {
|
||||
with_permissions!(user, target);
|
||||
|
||||
if let Ok(result) = target.seralise_with_channels() {
|
||||
Some(Response::Success(result))
|
||||
} else {
|
||||
Some(Response::InternalServerError(json!({ "error": "Failed to fetch channels!" })))
|
||||
}
|
||||
}
|
||||
|
||||
/// delete or leave a guild
|
||||
#[delete("/<target>")]
|
||||
pub fn remove_guild(user: User, target: Guild) -> Option<Response> {
|
||||
with_permissions!(user, target);
|
||||
|
||||
if user.id == target.owner {
|
||||
let channels = database::get_collection("channels");
|
||||
if let Ok(result) = channels.find(
|
||||
doc! {
|
||||
"type": 2,
|
||||
"guild": &target.id
|
||||
},
|
||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
||||
) {
|
||||
let mut values = vec![];
|
||||
for item in result {
|
||||
if let Ok(doc) = item {
|
||||
values.push(Bson::String(doc.get_str("_id").unwrap().to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
if database::get_collection("messages")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": {
|
||||
"$in": values
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if channels
|
||||
.delete_many(
|
||||
doc! {
|
||||
"type": 2,
|
||||
"guild": &target.id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if database::get_collection("members")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.guild": &target.id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if database::get_collection("guilds")
|
||||
.delete_one(
|
||||
doc! {
|
||||
"_id": &target.id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
notifications::send_message_threaded(
|
||||
None,
|
||||
target.id.clone(),
|
||||
Notification::guild_delete(Delete {
|
||||
id: target.id.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to delete guild." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to delete guild members." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to delete guild channels." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to delete guild messages." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Could not fetch channels." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
if database::get_collection("members")
|
||||
.delete_one(
|
||||
doc! {
|
||||
"_id.guild": &target.id,
|
||||
"_id.user": &user.id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
notifications::send_message_threaded(
|
||||
None,
|
||||
target.id.clone(),
|
||||
Notification::guild_user_leave(UserLeave {
|
||||
id: target.id.clone(),
|
||||
user: user.id.clone(),
|
||||
banned: false,
|
||||
}),
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to remove you from the guild." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateChannel {
|
||||
nonce: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
/// create a new channel
|
||||
#[post("/<target>/channels", data = "<info>")]
|
||||
pub fn create_channel(user: User, target: Guild, info: Json<CreateChannel>) -> Option<Response> {
|
||||
let (permissions, _) = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_manage_channels() {
|
||||
return Some(Response::LackingPermission(Permission::ManageChannels));
|
||||
}
|
||||
|
||||
let nonce: String = info.nonce.chars().take(32).collect();
|
||||
let name: String = info.name.chars().take(32).collect();
|
||||
let description: String = info
|
||||
.description
|
||||
.clone()
|
||||
.unwrap_or(String::new())
|
||||
.chars()
|
||||
.take(255)
|
||||
.collect();
|
||||
|
||||
if let Ok(result) =
|
||||
database::get_collection("channels").find_one(doc! { "nonce": &nonce }, None)
|
||||
{
|
||||
if result.is_some() {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "Channel already created." }),
|
||||
));
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
if database::get_collection("channels")
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": &id,
|
||||
"nonce": &nonce,
|
||||
"type": 2,
|
||||
"guild": &target.id,
|
||||
"name": &name,
|
||||
"description": &description,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
if database::get_collection("guilds")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &target.id
|
||||
},
|
||||
doc! {
|
||||
"$addToSet": {
|
||||
"channels": &id
|
||||
}
|
||||
},
|
||||
None
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
notifications::send_message_threaded(
|
||||
None,
|
||||
target.id.clone(),
|
||||
Notification::guild_channel_create(ChannelCreate {
|
||||
id: target.id.clone(),
|
||||
channel: id.clone(),
|
||||
name: name.clone(),
|
||||
description: description.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
Some(Response::Success(json!({ "id": &id })))
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Couldn't save channel list." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Couldn't create channel." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::BadRequest(
|
||||
json!({ "error": "Failed to check if channel was made." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct InviteOptions {
|
||||
// ? TODO: add options
|
||||
}
|
||||
|
||||
/// create a new invite
|
||||
#[post("/<target>/channels/<channel>/invite", data = "<_options>")]
|
||||
pub fn create_invite(
|
||||
user: User,
|
||||
target: Guild,
|
||||
channel: Channel,
|
||||
_options: Json<InviteOptions>,
|
||||
) -> Option<Response> {
|
||||
let (permissions, _) = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_create_invite() {
|
||||
return Some(Response::LackingPermission(Permission::CreateInvite));
|
||||
}
|
||||
|
||||
let code = gen_token(7);
|
||||
if database::get_collection("guilds")
|
||||
.update_one(
|
||||
doc! { "_id": target.id },
|
||||
doc! {
|
||||
"$push": {
|
||||
"invites": {
|
||||
"code": &code,
|
||||
"creator": user.id,
|
||||
"channel": channel.id,
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
Some(Response::Success(json!({ "code": code })))
|
||||
} else {
|
||||
Some(Response::BadRequest(
|
||||
json!({ "error": "Failed to create invite." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// remove an invite
|
||||
#[delete("/<target>/invites/<code>")]
|
||||
pub fn remove_invite(user: User, target: Guild, code: String) -> Option<Response> {
|
||||
let (permissions, _) = with_permissions!(user, target);
|
||||
|
||||
if let Some((guild_id, _, invite)) = get_invite(&code, None) {
|
||||
if invite.creator != user.id {
|
||||
if !permissions.get_manage_server() {
|
||||
return Some(Response::LackingPermission(Permission::ManageServer));
|
||||
}
|
||||
}
|
||||
|
||||
if database::get_collection("guilds")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &guild_id,
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"invites": {
|
||||
"code": &code
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::BadRequest(
|
||||
json!({ "error": "Failed to delete invite." }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::NotFound(
|
||||
json!({ "error": "Failed to fetch invite or code is invalid." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// fetch all guild invites
|
||||
#[get("/<target>/invites")]
|
||||
pub fn fetch_invites(user: User, target: Guild) -> Option<Response> {
|
||||
let (permissions, _) = with_permissions!(user, target);
|
||||
|
||||
if !permissions.get_manage_server() {
|
||||
return Some(Response::LackingPermission(Permission::ManageServer));
|
||||
}
|
||||
|
||||
Some(Response::Success(json!(target.invites)))
|
||||
}
|
||||
|
||||
/// view an invite before joining
|
||||
#[get("/join/<code>", rank = 1)]
|
||||
pub fn fetch_invite(user: User, code: String) -> Response {
|
||||
if let Some((guild_id, name, invite)) = get_invite(&code, user.id) {
|
||||
match fetch_channel(&invite.channel) {
|
||||
Ok(result) => {
|
||||
if let Some(channel) = result {
|
||||
Response::Success(json!({
|
||||
"guild": {
|
||||
"id": guild_id,
|
||||
"name": name,
|
||||
},
|
||||
"channel": {
|
||||
"id": channel.id,
|
||||
"name": channel.name,
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
Response::NotFound(json!({ "error": "Channel does not exist." }))
|
||||
}
|
||||
}
|
||||
Err(err) => Response::InternalServerError(json!({ "error": err })),
|
||||
}
|
||||
} else {
|
||||
Response::NotFound(json!({ "error": "Failed to fetch invite or code is invalid." }))
|
||||
}
|
||||
}
|
||||
|
||||
/// join a guild using an invite
|
||||
#[post("/join/<code>", rank = 1)]
|
||||
pub fn use_invite(user: User, code: String) -> Response {
|
||||
if let Some((guild_id, _, invite)) = get_invite(&code, Some(user.id.clone())) {
|
||||
if let Ok(result) = database::get_collection("members").find_one(
|
||||
doc! {
|
||||
"_id.guild": &guild_id,
|
||||
"_id.user": &user.id
|
||||
},
|
||||
FindOneOptions::builder()
|
||||
.projection(doc! { "_id": 1 })
|
||||
.build(),
|
||||
) {
|
||||
if result.is_none() {
|
||||
if database::get_collection("members")
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": {
|
||||
"guild": &guild_id,
|
||||
"user": &user.id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
notifications::send_message_threaded(
|
||||
None,
|
||||
guild_id.clone(),
|
||||
Notification::guild_user_join(UserJoin {
|
||||
id: guild_id.clone(),
|
||||
user: user.id.clone(),
|
||||
}),
|
||||
);
|
||||
|
||||
Response::Success(json!({
|
||||
"guild": &guild_id,
|
||||
"channel": &invite.channel,
|
||||
}))
|
||||
} else {
|
||||
Response::InternalServerError(
|
||||
json!({ "error": "Failed to add you to the guild." }),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Response::BadRequest(json!({ "error": "Already in the guild." }))
|
||||
}
|
||||
} else {
|
||||
Response::InternalServerError(
|
||||
json!({ "error": "Failed to check if you're in the guild." }),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Response::NotFound(json!({ "error": "Failed to fetch invite or code is invalid." }))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CreateGuild {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
nonce: String,
|
||||
}
|
||||
|
||||
/// create a new guild
|
||||
#[post("/create", data = "<info>")]
|
||||
pub fn create_guild(user: User, info: Json<CreateGuild>) -> Response {
|
||||
if !user.email_verification.verified {
|
||||
return Response::Unauthorized(json!({ "error": "Email not verified!" }));
|
||||
}
|
||||
|
||||
let name: String = info.name.chars().take(32).collect();
|
||||
let description: String = info
|
||||
.description
|
||||
.clone()
|
||||
.unwrap_or("No description.".to_string())
|
||||
.chars()
|
||||
.take(255)
|
||||
.collect();
|
||||
let nonce: String = info.nonce.chars().take(32).collect();
|
||||
|
||||
let channels = database::get_collection("channels");
|
||||
let col = database::get_collection("guilds");
|
||||
if col
|
||||
.find_one(doc! { "nonce": nonce.clone() }, None)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
{
|
||||
return Response::BadRequest(json!({ "error": "Guild already created!" }));
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
let channel_id = Ulid::new().to_string();
|
||||
if channels
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": channel_id.clone(),
|
||||
"type": ChannelType::GUILDCHANNEL as u32,
|
||||
"name": "general",
|
||||
"description": "",
|
||||
"guild": id.clone(),
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Response::InternalServerError(
|
||||
json!({ "error": "Failed to create guild channel." }),
|
||||
);
|
||||
}
|
||||
|
||||
if database::get_collection("members")
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": {
|
||||
"guild": &id,
|
||||
"user": &user.id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Response::InternalServerError(
|
||||
json!({ "error": "Failed to add you to members list." }),
|
||||
);
|
||||
}
|
||||
|
||||
if col
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": &id,
|
||||
"nonce": nonce,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"owner": &user.id,
|
||||
"channels": [ channel_id.clone() ],
|
||||
"invites": [],
|
||||
"bans": [],
|
||||
"default_permissions": 51,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
Response::Success(json!({ "id": id }))
|
||||
} else {
|
||||
channels
|
||||
.delete_one(doc! { "_id": channel_id }, None)
|
||||
.expect("Failed to delete the channel we just made.");
|
||||
|
||||
Response::InternalServerError(json!({ "error": "Failed to create guild." }))
|
||||
}
|
||||
}
|
||||
|
||||
/// fetch a guild's member
|
||||
#[get("/<target>/members")]
|
||||
pub fn fetch_members(user: User, target: Guild) -> Option<Response> {
|
||||
with_permissions!(user, target);
|
||||
|
||||
if let Ok(result) =
|
||||
database::get_collection("members").find(doc! { "_id.guild": target.id }, None)
|
||||
{
|
||||
let mut users = vec![];
|
||||
|
||||
for item in result {
|
||||
if let Ok(doc) = item {
|
||||
users.push(json!({
|
||||
"id": doc.get_document("_id").unwrap().get_str("user").unwrap(),
|
||||
"nickname": doc.get_str("nickname").ok(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Some(Response::Success(json!(users)))
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to fetch members." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// fetch a guild member
|
||||
#[get("/<target>/members/<other>")]
|
||||
pub fn fetch_member(user: User, target: Guild, other: String) -> Option<Response> {
|
||||
with_permissions!(user, target);
|
||||
|
||||
if let Ok(result) = get_member(MemberKey(target.id, other)) {
|
||||
if let Some(member) = result {
|
||||
Some(Response::Success(json!({
|
||||
"id": member.id.user,
|
||||
"nickname": member.nickname,
|
||||
})))
|
||||
} else {
|
||||
Some(Response::NotFound(
|
||||
json!({ "error": "Member does not exist!" }),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to fetch member." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// kick a guild member
|
||||
#[delete("/<target>/members/<other>")]
|
||||
pub fn kick_member(user: User, target: Guild, other: String) -> Option<Response> {
|
||||
let (permissions, _) = with_permissions!(user, target);
|
||||
|
||||
if user.id == other {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "Cannot kick yourself." }),
|
||||
));
|
||||
}
|
||||
|
||||
if !permissions.get_kick_members() {
|
||||
return Some(Response::LackingPermission(Permission::KickMembers));
|
||||
}
|
||||
|
||||
if let Ok(result) = get_member(MemberKey( target.id.clone(), other.clone() )) {
|
||||
if result.is_none() {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "User not part of guild." }),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Some(Response::InternalServerError(json!({ "error": "Failed to fetch member." })))
|
||||
}
|
||||
|
||||
if database::get_collection("members")
|
||||
.delete_one(
|
||||
doc! {
|
||||
"_id.guild": &target.id,
|
||||
"_id.user": &other,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
notifications::send_message_threaded(
|
||||
None,
|
||||
target.id.clone(),
|
||||
Notification::guild_user_leave(UserLeave {
|
||||
id: target.id.clone(),
|
||||
user: other.clone(),
|
||||
banned: false,
|
||||
}),
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to kick member." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, FromForm)]
|
||||
pub struct BanOptions {
|
||||
reason: Option<String>,
|
||||
}
|
||||
|
||||
/// ban a guild member
|
||||
#[put("/<target>/members/<other>/ban?<options..>")]
|
||||
pub fn ban_member(
|
||||
user: User,
|
||||
target: Guild,
|
||||
other: String,
|
||||
options: Form<BanOptions>,
|
||||
) -> Option<Response> {
|
||||
let (permissions, _) = with_permissions!(user, target);
|
||||
let reason: String = options
|
||||
.reason
|
||||
.clone()
|
||||
.unwrap_or("No reason specified.".to_string())
|
||||
.chars()
|
||||
.take(64)
|
||||
.collect();
|
||||
|
||||
if user.id == other {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "Cannot ban yourself." }),
|
||||
));
|
||||
}
|
||||
|
||||
if !permissions.get_ban_members() {
|
||||
return Some(Response::LackingPermission(Permission::BanMembers));
|
||||
}
|
||||
|
||||
if let Ok(result) = get_member(MemberKey( target.id.clone(), other.clone() )) {
|
||||
if result.is_none() {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "User not part of guild." }),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Some(Response::InternalServerError(json!({ "error": "Failed to fetch member." })))
|
||||
}
|
||||
|
||||
if database::get_collection("guilds")
|
||||
.update_one(
|
||||
doc! { "_id": &target.id },
|
||||
doc! {
|
||||
"$push": {
|
||||
"bans": {
|
||||
"id": &other,
|
||||
"reason": reason,
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "Failed to add ban to guild." }),
|
||||
));
|
||||
}
|
||||
|
||||
if database::get_collection("members")
|
||||
.delete_one(
|
||||
doc! {
|
||||
"_id.guild": &target.id,
|
||||
"_id.user": &other,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
notifications::send_message_threaded(
|
||||
None,
|
||||
target.id.clone(),
|
||||
Notification::guild_user_leave(UserLeave {
|
||||
id: target.id.clone(),
|
||||
user: other.clone(),
|
||||
banned: true,
|
||||
}),
|
||||
);
|
||||
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::InternalServerError(
|
||||
json!({ "error": "Failed to kick member after adding to ban list." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// unban a guild member
|
||||
#[delete("/<target>/members/<other>/ban")]
|
||||
pub fn unban_member(user: User, target: Guild, other: String) -> Option<Response> {
|
||||
let (permissions, _) = with_permissions!(user, target);
|
||||
|
||||
if user.id == other {
|
||||
return Some(Response::BadRequest(
|
||||
json!({ "error": "Cannot unban yourself (not checking if you're banned)." }),
|
||||
));
|
||||
}
|
||||
|
||||
if !permissions.get_ban_members() {
|
||||
return Some(Response::LackingPermission(Permission::BanMembers));
|
||||
}
|
||||
|
||||
if target.bans.iter().any(|v| v.id == other) {
|
||||
return Some(Response::BadRequest(json!({ "error": "User not banned." })));
|
||||
}
|
||||
|
||||
if database::get_collection("guilds")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &target.id
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"bans": {
|
||||
"$elemMatch": {
|
||||
"id": &other
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
Some(Response::Result(super::Status::Ok))
|
||||
} else {
|
||||
Some(Response::BadRequest(
|
||||
json!({ "error": "Failed to remove ban." }),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[options("/<_target>")]
|
||||
pub fn guild_preflight(_target: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/channels")]
|
||||
pub fn create_channel_preflight(_target: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/channels/<_channel>/invite")]
|
||||
pub fn create_invite_preflight(_target: String, _channel: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/invites/<_code>")]
|
||||
pub fn remove_invite_preflight(_target: String, _code: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/invites")]
|
||||
pub fn fetch_invites_preflight(_target: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/join/<_code>", rank = 1)]
|
||||
pub fn invite_preflight(_code: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/create")]
|
||||
pub fn create_guild_preflight() -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/members")]
|
||||
pub fn fetch_members_preflight(_target: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/members/<_other>")]
|
||||
pub fn fetch_member_preflight(_target: String, _other: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
#[options("/<_target>/members/<_other>/ban")]
|
||||
pub fn ban_member_preflight(_target: String, _other: String) -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
31
src/routes/invites/invite_delete.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use revolt_quark::{
|
||||
models::{Invite, User},
|
||||
perms, Db, EmptyResponse, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Delete Invite
|
||||
///
|
||||
/// Delete an invite by its id.
|
||||
#[openapi(tag = "Invites")]
|
||||
#[delete("/<target>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
|
||||
let invite = target.as_invite(db).await?;
|
||||
|
||||
if user.id == invite.creator() {
|
||||
db.delete_invite(invite.code()).await
|
||||
} else {
|
||||
match invite {
|
||||
Invite::Server { code, server, .. } => {
|
||||
let server = db.fetch_server(&server).await?;
|
||||
perms(&user)
|
||||
.server(&server)
|
||||
.throw_permission(db, Permission::ManageServer)
|
||||
.await?;
|
||||
|
||||
db.delete_invite(&code).await
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
131
src/routes/invites/invite_fetch.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use revolt_quark::models::{Channel, File, Invite};
|
||||
use revolt_quark::{Db, Ref, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
/// # Invite
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Serialize, Debug, Clone, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum InviteResponse {
|
||||
/// Server channel invite
|
||||
Server {
|
||||
/// Invite code
|
||||
code: String,
|
||||
/// Id of the server
|
||||
server_id: String,
|
||||
/// Name of the server
|
||||
server_name: String,
|
||||
/// Attachment for server icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_icon: Option<File>,
|
||||
/// Attachment for server banner
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_banner: Option<File>,
|
||||
/// Id of server channel
|
||||
channel_id: String,
|
||||
/// Name of server channel
|
||||
channel_name: String,
|
||||
/// Description of server channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_description: Option<String>,
|
||||
/// Name of user who created the invite
|
||||
user_name: String,
|
||||
/// Avatar of the user who created the invite
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_avatar: Option<File>,
|
||||
/// Number of members in this server
|
||||
member_count: i64,
|
||||
},
|
||||
/// Group channel invite
|
||||
Group {
|
||||
/// Invite code
|
||||
code: String,
|
||||
/// Id of group channel
|
||||
channel_id: String,
|
||||
/// Name of group channel
|
||||
channel_name: String,
|
||||
/// Description of group channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_description: Option<String>,
|
||||
/// Name of user who created the invite
|
||||
user_name: String,
|
||||
/// Avatar of the user who created the invite
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_avatar: Option<File>,
|
||||
},
|
||||
}
|
||||
|
||||
/// # Fetch Invite
|
||||
///
|
||||
/// Fetch an invite by its id.
|
||||
#[openapi(tag = "Invites")]
|
||||
#[get("/<target>")]
|
||||
pub async fn req(db: &Db, target: Ref) -> Result<Json<InviteResponse>> {
|
||||
Ok(Json(match target.as_invite(db).await? {
|
||||
Invite::Server {
|
||||
channel, creator, ..
|
||||
} => {
|
||||
let channel = db.fetch_channel(&channel).await?;
|
||||
let user = db.fetch_user(&creator).await?;
|
||||
|
||||
match channel {
|
||||
Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
..
|
||||
} => {
|
||||
let server = db.fetch_server(&server).await?;
|
||||
|
||||
InviteResponse::Server {
|
||||
code: target.id,
|
||||
member_count: db.fetch_member_count(&server.id).await? as i64,
|
||||
server_id: server.id,
|
||||
server_name: server.name,
|
||||
server_icon: server.icon,
|
||||
server_banner: server.banner,
|
||||
channel_id: id,
|
||||
channel_name: name,
|
||||
channel_description: description,
|
||||
user_name: user.username,
|
||||
user_avatar: user.avatar,
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
Invite::Group {
|
||||
channel, creator, ..
|
||||
} => {
|
||||
let channel = db.fetch_channel(&channel).await?;
|
||||
let user = db.fetch_user(&creator).await?;
|
||||
|
||||
match channel {
|
||||
Channel::Group {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
..
|
||||
} => InviteResponse::Group {
|
||||
code: target.id,
|
||||
channel_id: id,
|
||||
channel_name: name,
|
||||
channel_description: description,
|
||||
user_name: user.username,
|
||||
user_avatar: user.avatar,
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
47
src/routes/invites/invite_join.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, Invite, Server, User},
|
||||
variables::delta::MAX_SERVER_COUNT,
|
||||
Db, Error, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
/// # Join Response
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum InviteJoinResponse {
|
||||
Server {
|
||||
/// Channels in the server
|
||||
channels: Vec<Channel>,
|
||||
/// Server we are joining
|
||||
server: Server,
|
||||
},
|
||||
}
|
||||
|
||||
/// # Join Invite
|
||||
///
|
||||
/// Join an invite by its ID.
|
||||
#[openapi(tag = "Invites")]
|
||||
#[post("/<target>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<InviteJoinResponse>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
if !user.can_acquire_server(db).await? {
|
||||
return Err(Error::TooManyServers {
|
||||
max: *MAX_SERVER_COUNT,
|
||||
});
|
||||
}
|
||||
|
||||
let invite = target.as_invite(db).await?;
|
||||
match &invite {
|
||||
Invite::Server { server, .. } => {
|
||||
let server = db.fetch_server(server).await?;
|
||||
let channels = server.create_member(db, user, None).await?;
|
||||
Ok(Json(InviteJoinResponse::Server { channels, server }))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
10
src/routes/invites/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod invite_delete;
|
||||
mod invite_fetch;
|
||||
mod invite_join;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![invite_fetch::req, invite_join::req, invite_delete::req]
|
||||
}
|
||||
@@ -1,154 +1,267 @@
|
||||
pub use rocket::http::Status;
|
||||
pub use rocket::response::Redirect;
|
||||
use rocket::Rocket;
|
||||
use rocket_contrib::json::JsonValue;
|
||||
use rocket::{Build, Rocket};
|
||||
use rocket_okapi::{okapi::openapi3::OpenApi, settings::OpenApiSettings};
|
||||
|
||||
use crate::database::Permission;
|
||||
mod bots;
|
||||
mod channels;
|
||||
mod invites;
|
||||
mod onboard;
|
||||
mod push;
|
||||
mod root;
|
||||
mod servers;
|
||||
mod sync;
|
||||
mod users;
|
||||
|
||||
pub mod account;
|
||||
pub mod channel;
|
||||
pub mod guild;
|
||||
pub mod root;
|
||||
pub mod user;
|
||||
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
let settings = OpenApiSettings::default();
|
||||
|
||||
#[derive(Responder)]
|
||||
pub enum Response {
|
||||
#[response()]
|
||||
Result(Status),
|
||||
#[response()]
|
||||
Success(JsonValue),
|
||||
#[response()]
|
||||
Redirect(Redirect),
|
||||
#[response(status = 207)]
|
||||
PartialStatus(JsonValue),
|
||||
#[response(status = 400)]
|
||||
BadRequest(JsonValue),
|
||||
#[response(status = 401)]
|
||||
Unauthorized(JsonValue),
|
||||
#[response(status = 401)]
|
||||
LackingPermission(Permission),
|
||||
#[response(status = 404)]
|
||||
NotFound(JsonValue),
|
||||
#[response(status = 406)]
|
||||
NotAcceptable(JsonValue),
|
||||
#[response(status = 409)]
|
||||
Conflict(JsonValue),
|
||||
#[response(status = 410)]
|
||||
Gone(JsonValue),
|
||||
#[response(status = 418)]
|
||||
Teapot(JsonValue),
|
||||
#[response(status = 422)]
|
||||
UnprocessableEntity(JsonValue),
|
||||
#[response(status = 429)]
|
||||
TooManyRequests(JsonValue),
|
||||
#[response(status = 500)]
|
||||
InternalServerError(JsonValue),
|
||||
mount_endpoints_and_merged_docs! {
|
||||
rocket, "/".to_owned(), settings,
|
||||
"/" => (vec![], custom_openapi_spec()),
|
||||
"" => openapi_get_routes_spec![root::root, root::ping],
|
||||
"/users" => users::routes(),
|
||||
"/bots" => bots::routes(),
|
||||
"/channels" => channels::routes(),
|
||||
"/servers" => servers::routes(),
|
||||
"/invites" => invites::routes(),
|
||||
"/auth/account" => rauth::web::account::routes(),
|
||||
"/auth/session" => rauth::web::session::routes(),
|
||||
"/onboard" => onboard::routes(),
|
||||
"/push" => push::routes(),
|
||||
"/sync" => sync::routes(),
|
||||
};
|
||||
|
||||
rocket
|
||||
}
|
||||
|
||||
use rocket::http::ContentType;
|
||||
use rocket::request::Request;
|
||||
use std::io::Cursor;
|
||||
fn custom_openapi_spec() -> OpenApi {
|
||||
use rocket_okapi::okapi::openapi3::*;
|
||||
|
||||
impl<'a> rocket::response::Responder<'a> for Permission {
|
||||
fn respond_to(self, _: &Request) -> rocket::response::Result<'a> {
|
||||
rocket::response::Response::build()
|
||||
.header(ContentType::JSON)
|
||||
.sized_body(Cursor::new(format!(
|
||||
"{{\"error\":\"Lacking permission: {:?}.\",\"permission\":{}}}",
|
||||
self, self as u32,
|
||||
)))
|
||||
.ok()
|
||||
let mut extensions = schemars::Map::new();
|
||||
extensions.insert(
|
||||
"x-logo".to_owned(),
|
||||
json!({
|
||||
"url": "https://revolt.chat/header.png",
|
||||
"altText": "Revolt Header"
|
||||
}),
|
||||
);
|
||||
|
||||
extensions.insert(
|
||||
"x-tagGroups".to_owned(),
|
||||
json!([
|
||||
{
|
||||
"name": "Revolt",
|
||||
"tags": [
|
||||
"Core"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Users",
|
||||
"tags": [
|
||||
"User Information",
|
||||
"Direct Messaging",
|
||||
"Relationships"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Bots",
|
||||
"tags": [
|
||||
"Bots"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Channels",
|
||||
"tags": [
|
||||
"Channel Information",
|
||||
"Channel Invites",
|
||||
"Channel Permissions",
|
||||
"Messaging",
|
||||
"Groups",
|
||||
"Voice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Servers",
|
||||
"tags": [
|
||||
"Server Information",
|
||||
"Server Members",
|
||||
"Server Permissions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Invites",
|
||||
"tags": [
|
||||
"Invites"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Authentication",
|
||||
"tags": [
|
||||
"Account",
|
||||
"Session",
|
||||
"Onboarding"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Miscellaneous",
|
||||
"tags": [
|
||||
"Sync",
|
||||
"Web Push"
|
||||
]
|
||||
}
|
||||
]),
|
||||
);
|
||||
|
||||
OpenApi {
|
||||
openapi: OpenApi::default_version(),
|
||||
info: Info {
|
||||
title: "Revolt API".to_owned(),
|
||||
description: Some("User-first privacy focused chat platform.".to_owned()),
|
||||
terms_of_service: Some("https://revolt.chat/terms".to_owned()),
|
||||
contact: Some(Contact {
|
||||
name: Some("Revolt Support".to_owned()),
|
||||
url: Some("https://revolt.chat".to_owned()),
|
||||
email: Some("contact@revolt.chat".to_owned()),
|
||||
..Default::default()
|
||||
}),
|
||||
license: Some(License {
|
||||
name: "AGPLv3".to_owned(),
|
||||
url: Some("https://github.com/revoltchat/delta/blob/master/LICENSE".to_owned()),
|
||||
..Default::default()
|
||||
}),
|
||||
version: "0.5.3-rc.1".to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
servers: vec![
|
||||
Server {
|
||||
url: "https://api.revolt.chat".to_owned(),
|
||||
description: Some("Revolt API".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Server {
|
||||
url: "http://local.revolt.chat:8000".to_owned(),
|
||||
description: Some("Local Revolt Environment".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
external_docs: Some(ExternalDocs {
|
||||
url: "https://developers.revolt.chat".to_owned(),
|
||||
description: Some("Revolt Developer Documentation".to_owned()),
|
||||
..Default::default()
|
||||
}),
|
||||
extensions,
|
||||
tags: vec![
|
||||
Tag {
|
||||
name: "Core".to_owned(),
|
||||
description: Some(
|
||||
"Use in your applications to determine information about the Revolt node"
|
||||
.to_owned(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "User Information".to_owned(),
|
||||
description: Some("Query and fetch users on Revolt".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Direct Messaging".to_owned(),
|
||||
description: Some("Direct message other users on Revolt".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Relationships".to_owned(),
|
||||
description: Some(
|
||||
"Manage your friendships and block list on the platform".to_owned(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Bots".to_owned(),
|
||||
description: Some("Create and edit bots".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Channel Information".to_owned(),
|
||||
description: Some("Query and fetch channels on Revolt".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Channel Invites".to_owned(),
|
||||
description: Some("Create and manage invites for channels".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Channel Permissions".to_owned(),
|
||||
description: Some("Manage permissions for channels".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Messaging".to_owned(),
|
||||
description: Some("Send and manipulate messages".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Groups".to_owned(),
|
||||
description: Some("Create, invite users and manipulate groups".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Voice".to_owned(),
|
||||
description: Some("Join and talk with other users".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Server Information".to_owned(),
|
||||
description: Some("Query and fetch servers on Revolt".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Server Members".to_owned(),
|
||||
description: Some("Find and edit server members".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Server Permissions".to_owned(),
|
||||
description: Some("Manage permissions for servers".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Invites".to_owned(),
|
||||
description: Some("View, join and delete invites".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Account".to_owned(),
|
||||
description: Some("Manage your account".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Session".to_owned(),
|
||||
description: Some("Create and manage sessions".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Onboarding".to_owned(),
|
||||
description: Some(
|
||||
"After signing up to Revolt, users must pick a unique username".to_owned(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Sync".to_owned(),
|
||||
description: Some("Upload and retrieve any JSON data between clients".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
Tag {
|
||||
name: "Web Push".to_owned(),
|
||||
description: Some(
|
||||
"Subscribe to and receive Revolt push notifications while offline".to_owned(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mount(rocket: Rocket) -> Rocket {
|
||||
rocket
|
||||
.mount("/", routes![root::root, root::root_preflight, root::teapot])
|
||||
.mount(
|
||||
"/account",
|
||||
routes![
|
||||
account::create,
|
||||
account::verify_email,
|
||||
account::resend_email,
|
||||
account::login,
|
||||
account::token,
|
||||
account::create_preflight,
|
||||
account::verify_email_preflight,
|
||||
account::resend_email_preflight,
|
||||
account::login_preflight,
|
||||
account::token_preflight,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/users",
|
||||
routes![
|
||||
user::me,
|
||||
user::user,
|
||||
user::query,
|
||||
user::dms,
|
||||
user::dm,
|
||||
user::get_friends,
|
||||
user::get_friend,
|
||||
user::add_friend,
|
||||
user::remove_friend,
|
||||
user::block_user,
|
||||
user::unblock_user,
|
||||
user::user_preflight,
|
||||
user::query_preflight,
|
||||
user::dms_preflight,
|
||||
user::dm_preflight,
|
||||
user::friend_preflight,
|
||||
user::block_user_preflight,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/channels",
|
||||
routes![
|
||||
channel::create_group,
|
||||
channel::channel,
|
||||
channel::add_member,
|
||||
channel::remove_member,
|
||||
channel::delete,
|
||||
channel::messages,
|
||||
channel::get_message,
|
||||
channel::send_message,
|
||||
channel::edit_message,
|
||||
channel::delete_message,
|
||||
channel::create_group_preflight,
|
||||
channel::channel_preflight,
|
||||
channel::member_preflight,
|
||||
channel::messages_preflight,
|
||||
channel::message_preflight,
|
||||
],
|
||||
)
|
||||
.mount(
|
||||
"/guild",
|
||||
routes![
|
||||
guild::my_guilds,
|
||||
guild::guild,
|
||||
guild::remove_guild,
|
||||
guild::create_channel,
|
||||
guild::create_invite,
|
||||
guild::remove_invite,
|
||||
guild::fetch_invites,
|
||||
guild::fetch_invite,
|
||||
guild::use_invite,
|
||||
guild::create_guild,
|
||||
guild::fetch_members,
|
||||
guild::fetch_member,
|
||||
guild::kick_member,
|
||||
guild::ban_member,
|
||||
guild::unban_member,
|
||||
guild::guild_preflight,
|
||||
guild::create_channel_preflight,
|
||||
guild::create_invite_preflight,
|
||||
guild::remove_invite_preflight,
|
||||
guild::fetch_invites_preflight,
|
||||
guild::invite_preflight,
|
||||
guild::create_guild_preflight,
|
||||
guild::fetch_members_preflight,
|
||||
guild::fetch_member_preflight,
|
||||
guild::ban_member_preflight,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
47
src/routes/onboard/complete.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::util::regex::RE_USERNAME;
|
||||
use revolt_quark::{models::User, Database, EmptyResponse, Error, Result};
|
||||
|
||||
use rauth::entities::Session;
|
||||
use rocket::{serde::json::Json, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # New User Data
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataOnboard {
|
||||
/// New username which will be used to identify the user on the platform
|
||||
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
|
||||
username: String,
|
||||
}
|
||||
|
||||
/// # Complete Onboarding
|
||||
///
|
||||
/// This sets a new username, completes onboarding and allows a user to start using Revolt.
|
||||
#[openapi(tag = "Onboarding")]
|
||||
#[post("/complete", data = "<data>")]
|
||||
pub async fn req(
|
||||
db: &State<Database>,
|
||||
session: Session,
|
||||
user: Option<User>,
|
||||
data: Json<DataOnboard>,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.is_some() {
|
||||
return Err(Error::AlreadyOnboarded);
|
||||
}
|
||||
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
if db.is_username_taken(&data.username).await? {
|
||||
return Err(Error::UsernameTaken);
|
||||
}
|
||||
|
||||
let user = User {
|
||||
id: session.user_id,
|
||||
username: data.username,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_user(&user).await.map(|_| EmptyResponse)
|
||||
}
|
||||
22
src/routes/onboard/hello.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use rauth::entities::Session;
|
||||
use revolt_quark::models::User;
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
/// # Onboarding Status
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
pub struct DataHello {
|
||||
/// Whether onboarding is required
|
||||
onboarding: bool,
|
||||
}
|
||||
|
||||
/// # Check Onboarding Status
|
||||
///
|
||||
/// This will tell you whether the current account requires onboarding or whether you can continue to send requests as usual. You may skip calling this if you're restoring an existing session.
|
||||
#[openapi(tag = "Onboarding")]
|
||||
#[get("/hello")]
|
||||
pub async fn req(_session: Session, user: Option<User>) -> Json<DataHello> {
|
||||
Json(DataHello {
|
||||
onboarding: user.is_none(),
|
||||
})
|
||||
}
|
||||
9
src/routes/onboard/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod complete;
|
||||
mod hello;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![hello::req, complete::req]
|
||||
}
|
||||
9
src/routes/push/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod subscribe;
|
||||
mod unsubscribe;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![subscribe::req, unsubscribe::req]
|
||||
}
|
||||
30
src/routes/push/subscribe.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use revolt_quark::{EmptyResponse, Error, Result};
|
||||
|
||||
use rauth::{
|
||||
entities::{Model, Session, WebPushSubscription},
|
||||
logic::Auth,
|
||||
};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Push Subscribe
|
||||
///
|
||||
/// Create a new Web Push subscription.
|
||||
///
|
||||
/// If an existing subscription exists on this session, it will be removed.
|
||||
#[openapi(tag = "Web Push")]
|
||||
#[post("/subscribe", data = "<data>")]
|
||||
pub async fn req(
|
||||
auth: &State<Auth>,
|
||||
mut session: Session,
|
||||
data: Json<WebPushSubscription>,
|
||||
) -> Result<EmptyResponse> {
|
||||
session.subscription = Some(data.into_inner());
|
||||
session
|
||||
.save(&auth.db, None)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "save",
|
||||
with: "session",
|
||||
})
|
||||
}
|
||||
24
src/routes/push/unsubscribe.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use revolt_quark::{EmptyResponse, Error, Result};
|
||||
|
||||
use rauth::{
|
||||
entities::{Model, Session},
|
||||
logic::Auth,
|
||||
};
|
||||
use rocket::State;
|
||||
|
||||
/// # Unsubscribe
|
||||
///
|
||||
/// Remove the Web Push subscription associated with the current session.
|
||||
#[openapi(tag = "Web Push")]
|
||||
#[post("/unsubscribe")]
|
||||
pub async fn req(auth: &State<Auth>, mut session: Session) -> Result<EmptyResponse> {
|
||||
session.subscription = None;
|
||||
session
|
||||
.save(&auth.db, None)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "save",
|
||||
with: "session",
|
||||
})
|
||||
}
|
||||
@@ -1,34 +1,112 @@
|
||||
use super::Response;
|
||||
use revolt_quark::variables::delta::{
|
||||
APP_URL, AUTUMN_URL, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, JANUARY_URL, USE_AUTUMN,
|
||||
USE_EMAIL, USE_HCAPTCHA, USE_JANUARY, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL, VOSO_WS_HOST,
|
||||
};
|
||||
use revolt_quark::Result;
|
||||
|
||||
use mongodb::bson::doc;
|
||||
use std::env;
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
/// root
|
||||
/// # hCaptcha Configuration
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct CaptchaFeature {
|
||||
/// Whether captcha is enabled
|
||||
pub enabled: bool,
|
||||
/// Client key used for solving captcha
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
/// # Generic Service Configuration
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct Feature {
|
||||
/// Whether the service is enabled
|
||||
pub enabled: bool,
|
||||
/// URL pointing to the service
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// # Voice Server Configuration
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct VoiceFeature {
|
||||
/// Whether voice is enabled
|
||||
pub enabled: bool,
|
||||
/// URL pointing to the voice API
|
||||
pub url: String,
|
||||
/// URL pointing to the voice WebSocket server
|
||||
pub ws: String,
|
||||
}
|
||||
|
||||
/// # Feature Configuration
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct RevoltFeatures {
|
||||
/// hCaptcha configuration
|
||||
pub captcha: CaptchaFeature,
|
||||
/// Whether email verification is enabled
|
||||
pub email: bool,
|
||||
/// Whether this server is invite only
|
||||
pub invite_only: bool,
|
||||
/// File server service configuration
|
||||
pub autumn: Feature,
|
||||
/// Proxy service configuration
|
||||
pub january: Feature,
|
||||
/// Voice server configuration
|
||||
pub voso: VoiceFeature,
|
||||
}
|
||||
|
||||
/// # Server Configuration
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct RevoltConfig {
|
||||
/// Revolt API Version
|
||||
pub revolt: String,
|
||||
/// Features enabled on this Revolt node
|
||||
pub features: RevoltFeatures,
|
||||
/// WebSocket URL
|
||||
pub ws: String,
|
||||
/// URL pointing to the client serving this node
|
||||
pub app: String,
|
||||
/// Web Push VAPID public key
|
||||
pub vapid: String,
|
||||
}
|
||||
|
||||
/// # Query Node
|
||||
///
|
||||
/// Fetch the server configuration for this Revolt instance.
|
||||
#[openapi(tag = "Core")]
|
||||
#[get("/")]
|
||||
pub fn root() -> Response {
|
||||
Response::Success(json!({
|
||||
"revolt": "0.2.9",
|
||||
"version": {
|
||||
"major": 0,
|
||||
"minor": 2,
|
||||
"patch": 9
|
||||
pub async fn root() -> Result<Json<RevoltConfig>> {
|
||||
Ok(Json(RevoltConfig {
|
||||
revolt: crate::version::VERSION.to_string(),
|
||||
features: RevoltFeatures {
|
||||
captcha: CaptchaFeature {
|
||||
enabled: *USE_HCAPTCHA,
|
||||
key: HCAPTCHA_SITEKEY.to_string(),
|
||||
},
|
||||
email: *USE_EMAIL,
|
||||
invite_only: *INVITE_ONLY,
|
||||
autumn: Feature {
|
||||
enabled: *USE_AUTUMN,
|
||||
url: AUTUMN_URL.to_string(),
|
||||
},
|
||||
january: Feature {
|
||||
enabled: *USE_JANUARY,
|
||||
url: JANUARY_URL.to_string(),
|
||||
},
|
||||
voso: VoiceFeature {
|
||||
enabled: *USE_VOSO,
|
||||
url: VOSO_URL.to_string(),
|
||||
ws: VOSO_WS_HOST.to_string(),
|
||||
},
|
||||
},
|
||||
"features": {
|
||||
"captcha": env::var("HCAPTCHA_KEY").is_ok()
|
||||
}
|
||||
ws: EXTERNAL_WS_URL.to_string(),
|
||||
app: APP_URL.to_string(),
|
||||
vapid: VAPID_PUBLIC_KEY.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
#[options("/")]
|
||||
pub fn root_preflight() -> Response {
|
||||
Response::Result(super::Status::Ok)
|
||||
}
|
||||
|
||||
/// I'm a teapot.
|
||||
#[delete("/")]
|
||||
pub fn teapot() -> Response {
|
||||
Response::Teapot(json!({
|
||||
"teapot": true,
|
||||
"can_delete": false
|
||||
}))
|
||||
/// Example endpoint.
|
||||
#[openapi(skip)]
|
||||
#[get("/ping")]
|
||||
pub async fn ping(/*_limitguard: Ratelimiter*/) -> Status {
|
||||
Status::Ok
|
||||
}
|
||||
|
||||
59
src/routes/servers/ban_create.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use revolt_quark::{
|
||||
models::{ServerBan, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Ban Information
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataBanCreate {
|
||||
/// Ban reason
|
||||
#[validate(length(min = 1, max = 1024))]
|
||||
reason: Option<String>,
|
||||
}
|
||||
|
||||
/// # Ban User
|
||||
///
|
||||
/// Ban a user by their id.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[put("/<server>/bans/<target>", data = "<data>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
server: Ref,
|
||||
target: Ref,
|
||||
data: Json<DataBanCreate>,
|
||||
) -> Result<Json<ServerBan>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let server = server.as_server(db).await?;
|
||||
|
||||
if target.id == user.id {
|
||||
return Err(Error::CannotRemoveYourself);
|
||||
}
|
||||
|
||||
if target.id == server.owner {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
|
||||
let mut permissions = perms(&user).server(&server);
|
||||
|
||||
permissions
|
||||
.throw_permission(db, Permission::BanMembers)
|
||||
.await?;
|
||||
|
||||
let member = target.as_member(db, &server.id).await?;
|
||||
|
||||
if member.get_ranking(permissions.server.get().unwrap())
|
||||
<= permissions.get_member_rank().unwrap_or(i64::MIN)
|
||||
{
|
||||
return Err(Error::NotElevated);
|
||||
}
|
||||
|
||||
server.ban_member(db, member, data.reason).await.map(Json)
|
||||
}
|
||||
67
src/routes/servers/ban_list.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use revolt_quark::models::{File, ServerBan, User};
|
||||
use revolt_quark::{perms, Db, Permission, Ref, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Banned User
|
||||
///
|
||||
/// Just enoguh user information to list bans.
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
struct BannedUser {
|
||||
/// Id of the banned user
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Username of the banned user
|
||||
pub username: String,
|
||||
/// Avatar of the banned user
|
||||
pub avatar: Option<File>,
|
||||
}
|
||||
|
||||
/// # Ban List Result
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BanListResult {
|
||||
/// Users objects
|
||||
users: Vec<BannedUser>,
|
||||
/// Ban objects
|
||||
bans: Vec<ServerBan>,
|
||||
}
|
||||
|
||||
impl From<User> for BannedUser {
|
||||
fn from(user: User) -> Self {
|
||||
BannedUser {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// # Fetch Bans
|
||||
///
|
||||
/// Fetch all bans on a server.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[get("/<target>/bans")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<BanListResult>> {
|
||||
let server = target.as_server(db).await?;
|
||||
perms(&user)
|
||||
.server(&server)
|
||||
.throw_permission(db, Permission::BanMembers)
|
||||
.await?;
|
||||
|
||||
let bans = db.fetch_bans(&server.id).await?;
|
||||
let users = db
|
||||
.fetch_users(
|
||||
&bans
|
||||
.iter()
|
||||
.map(|x| &x.id.user)
|
||||
.cloned()
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.into())
|
||||
.collect();
|
||||
|
||||
Ok(Json(BanListResult { users, bans }))
|
||||
}
|
||||