mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-06 11:16:00 +00:00
Compare commits
428 Commits
0.5.0
...
20230129-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdab997d17 | ||
|
|
8e0addc3e0 | ||
|
|
0da4baf777 | ||
|
|
c03fcda85b | ||
|
|
0afbcc065f | ||
|
|
a406e5504d | ||
|
|
e8c233f60b | ||
|
|
72595f30d4 | ||
|
|
39e345af74 | ||
|
|
e0b918771d | ||
|
|
7a6bd70dcd | ||
|
|
c0ef3d295a | ||
|
|
f5c95f9d12 | ||
|
|
fed1da72fd | ||
|
|
cf9c0bc5e1 | ||
|
|
cc2054838a | ||
|
|
d9d2735efa | ||
|
|
94b0472934 | ||
|
|
4a7917e9ea | ||
|
|
ce6b454874 | ||
|
|
fc32996b51 | ||
|
|
32d3683013 | ||
|
|
0cebb69e02 | ||
|
|
117958d8cd | ||
|
|
c5b823ad13 | ||
|
|
27b4699299 | ||
|
|
ee1c4cc2d5 | ||
|
|
0e0daf86f7 | ||
|
|
572e08c733 | ||
|
|
9011939344 | ||
|
|
f4dcd091cc | ||
|
|
86ad72b426 | ||
|
|
044d82d566 | ||
|
|
deffe663cb | ||
|
|
e7f6433d77 | ||
|
|
345e4d5f13 | ||
|
|
81fc3e7760 | ||
|
|
ca8d2e6118 | ||
|
|
c1227e8e33 | ||
|
|
0e0c6f2c3a | ||
|
|
fa82be74b5 | ||
|
|
8491ced13d | ||
|
|
229d4e2e1d | ||
|
|
7bdd2d69d6 | ||
|
|
ab2af9b5e5 | ||
|
|
5aebda2d16 | ||
|
|
a7273d0556 | ||
|
|
72aed64e5b | ||
|
|
00ad4d54e7 | ||
|
|
cba8c4ef3b | ||
|
|
b6e395fe0d | ||
|
|
8d07457c58 | ||
|
|
f1171e5358 | ||
|
|
d96c9f62c4 | ||
|
|
4f73e43a03 | ||
|
|
741b8ee8fd | ||
|
|
692081b7f0 | ||
|
|
943b1f08f0 | ||
|
|
0e5c8ed59b | ||
|
|
a7e0c42ee4 | ||
|
|
386f027a5a | ||
|
|
30c7f553d8 | ||
|
|
e291eef364 | ||
|
|
73443ce883 | ||
|
|
47297e243f | ||
|
|
4188b6d2f2 | ||
|
|
165380c7b4 | ||
|
|
49ca24ac9f | ||
|
|
35f956ce7d | ||
|
|
4baab5d5d5 | ||
|
|
7fc4fb2df7 | ||
|
|
ef757aa2fb | ||
|
|
0585dd0c20 | ||
|
|
f96541efab | ||
|
|
c6414338b6 | ||
|
|
4c4eb60cdb | ||
|
|
11d89b3bf0 | ||
|
|
64bb171cc8 | ||
|
|
6de5ad15c5 | ||
|
|
b5ab16d66f | ||
|
|
b9aad6d38c | ||
|
|
ed5c8159e9 | ||
|
|
2e6e1fdd44 | ||
|
|
cb2e6fb2f7 | ||
|
|
d069acd5a5 | ||
|
|
f8b5c25c17 | ||
|
|
80af18761f | ||
|
|
98e10c3478 | ||
|
|
2051c8ce45 | ||
|
|
0abc12e5fa | ||
|
|
aaceb5bdee | ||
|
|
3de7ee4c7b | ||
|
|
d660127c14 | ||
|
|
7390b3c087 | ||
|
|
020f2a1b1a | ||
|
|
3e55ff3577 | ||
|
|
bb70294546 | ||
|
|
9c93017b8a | ||
|
|
57592c0862 | ||
|
|
f09981695e | ||
|
|
ad76e85c8d | ||
|
|
a100b7aefa | ||
|
|
a06ef2ba1a | ||
|
|
6336b254c5 | ||
|
|
f1dd7d2c78 | ||
|
|
ae3ed7c124 | ||
|
|
5237ecbb90 | ||
|
|
2ce610e1e7 | ||
|
|
5d8432e267 | ||
|
|
44311c87be | ||
|
|
ee9622251a | ||
|
|
d13573eb45 | ||
|
|
20e93b9838 | ||
|
|
dc6ce96aa2 | ||
|
|
de5428348c | ||
|
|
d7527d9131 | ||
|
|
c59c4146c4 | ||
|
|
bd455b59fb | ||
|
|
d03cb44071 | ||
|
|
ac4cedd014 | ||
|
|
09e918d8e0 | ||
|
|
97fef56bad | ||
|
|
408743476b | ||
|
|
fad53ee5f0 | ||
|
|
be9a5dba5b | ||
|
|
c33d387486 | ||
|
|
0651a501f6 | ||
|
|
278d795d50 | ||
|
|
6281418ecb | ||
|
|
ae0e5cd7bf | ||
|
|
45a3b4ba15 | ||
|
|
8b72e2fa0e | ||
|
|
e5af59d307 | ||
|
|
6a7e7e9501 | ||
|
|
247c059f29 | ||
|
|
621dd86845 | ||
|
|
2b016c34f7 | ||
|
|
e1169c5335 | ||
|
|
fad540a87a | ||
|
|
00cb132d29 | ||
|
|
7757769f79 | ||
|
|
2d10180f08 | ||
|
|
180ac67056 | ||
|
|
350bd9c0be | ||
|
|
367c4e3260 | ||
|
|
02ba8c7978 | ||
|
|
d7839f1790 | ||
|
|
9c71f09a84 | ||
|
|
b32503d29f | ||
|
|
535665ca1b | ||
|
|
da08a7d722 | ||
|
|
5ce0c133e8 | ||
|
|
3adbf29060 | ||
|
|
3d98f5dfbb | ||
|
|
54b6cf67ab | ||
|
|
b4a2e8c366 | ||
|
|
3e135a1a9c | ||
|
|
b351ef0332 | ||
|
|
87391a49a5 | ||
|
|
d2f0fb3414 | ||
|
|
7ea5146e58 | ||
|
|
3d4c40dac0 | ||
|
|
c3a04d02c5 | ||
|
|
1dbd66a45c | ||
|
|
10645638a2 | ||
|
|
24def22613 | ||
|
|
a2911e42c6 | ||
|
|
558e4b86f1 | ||
|
|
e245b272fa | ||
|
|
b4d825b9a1 | ||
|
|
aee6c751c6 | ||
|
|
0232d721db | ||
|
|
8dd9acdb59 | ||
|
|
43c475eabb | ||
|
|
25a52cbbb1 | ||
|
|
14e17b10c0 | ||
|
|
a0926eea32 | ||
|
|
3ae23af275 | ||
|
|
927556041b | ||
|
|
bbb368217c | ||
|
|
f568838ff9 | ||
|
|
3c5e494216 | ||
|
|
465d766254 | ||
|
|
8d8bcff31f | ||
|
|
3df869c21d | ||
|
|
a495a8dfd7 | ||
|
|
0553ce5587 | ||
|
|
fcbaab7bcb | ||
|
|
437a620274 | ||
|
|
30af92bee4 | ||
|
|
0ecce98056 | ||
|
|
04c2f8f293 | ||
|
|
d45b32bce6 | ||
|
|
23ec914033 | ||
|
|
71c01e0817 | ||
|
|
7b1a3dc7dd | ||
|
|
64d1126af9 | ||
|
|
4c40c6c2cc | ||
|
|
7befd77c5b | ||
|
|
aab85fa143 | ||
|
|
d7be1db755 | ||
|
|
2580cec272 | ||
|
|
489c2bdd87 | ||
|
|
061af1f2c3 | ||
|
|
50d9f50c8f | ||
|
|
28db08ba88 | ||
|
|
9da51b2d45 | ||
|
|
6a871fa944 | ||
|
|
4801b51cef | ||
|
|
7a9adb45f7 | ||
|
|
f0eb228b19 | ||
|
|
5aee381fd6 | ||
|
|
249b1161f9 | ||
|
|
1988a9501b | ||
|
|
6d230e57f5 | ||
|
|
9f1eda4af0 | ||
|
|
734dc2bb59 | ||
|
|
e22d991995 | ||
|
|
37ffde23ce | ||
|
|
f676071641 | ||
|
|
a8d5fcec9e | ||
|
|
d4aff072a8 | ||
|
|
57d114fee9 | ||
|
|
a4cbbfea51 | ||
|
|
a6cec559b1 | ||
|
|
8b4541feb0 | ||
|
|
88273021a2 | ||
|
|
e367c42dca | ||
|
|
88f811761b | ||
|
|
b58755dbeb | ||
|
|
780b88eb2f | ||
|
|
1aafa51033 | ||
|
|
63064cc9bb | ||
|
|
1d5800a3b8 | ||
|
|
c1cd4bfa66 | ||
|
|
c39a9917df | ||
|
|
0fdb749199 | ||
|
|
ab4c3f913e | ||
|
|
8be51dc4f0 | ||
|
|
96fd18f679 | ||
|
|
2db54cf8de | ||
|
|
c3f092aaef | ||
|
|
ce68f7eeed | ||
|
|
139e704746 | ||
|
|
345c4c627c | ||
|
|
03ca611553 | ||
|
|
0fe63e3955 | ||
|
|
4474061103 | ||
|
|
788580a64f | ||
|
|
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 |
@@ -1,3 +1,4 @@
|
||||
Dockerfile
|
||||
target
|
||||
.mongo
|
||||
.env
|
||||
79
.env.example
Normal file
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
|
||||
36
.github/workflows/cla.yml
vendored
Normal file
36
.github/workflows/cla.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened,closed,synchronize]
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.1.3-beta
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PAT }}
|
||||
with:
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-document: 'https://github.com/revoltchat/cla/blob/master/CLA.md' # e.g. a CLA or a DCO document
|
||||
# branch should not be protected
|
||||
branch: 'master'
|
||||
allowlist: insertish,bot*
|
||||
|
||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||
remote-organization-name: revoltchat
|
||||
remote-repository-name: cla
|
||||
create-file-commit-message: 'cla(create): creating file for storing CLA Signatures'
|
||||
signed-commit-message: 'cla(sign): $contributorName has signed the CLA in #$pullRequestNo'
|
||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
116
.github/workflows/docker.yaml
vendored
Normal file
116
.github/workflows/docker.yaml
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
name: Docker Test & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
base:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build base image (amd64)
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# Authenticate with GHCR
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Build all projects and cache
|
||||
- name: Build Base Image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/revoltchat/base:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
publish_amd64:
|
||||
needs: [base]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
strategy:
|
||||
matrix:
|
||||
project: [delta, bonfire]
|
||||
name: Build ${{ matrix.project }} image (amd64)
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# Authenticate with Docker Hub and GHCR
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
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 }}
|
||||
|
||||
# Resolve the correct project
|
||||
- uses: kanga333/variable-mapper@master
|
||||
id: export
|
||||
with:
|
||||
key: "${{ matrix.project }}"
|
||||
map: |
|
||||
{
|
||||
"delta": {
|
||||
"path": "crates/delta",
|
||||
"tag": "revoltchat/server"
|
||||
},
|
||||
"bonfire": {
|
||||
"path": "crates/bonfire",
|
||||
"tag": "revoltchat/bonfire"
|
||||
}
|
||||
}
|
||||
export_to: output
|
||||
|
||||
# Configure metadata
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ${{ steps.export.outputs.tag }}, ghcr.io/${{ steps.export.outputs.tag }}
|
||||
|
||||
# Build crate image
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
file: ${{ steps.export.outputs.path }}/Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
75
.github/workflows/rust.yaml
vendored
Normal file
75
.github/workflows/rust.yaml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Rust build, test, and generate specification
|
||||
|
||||
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 stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
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
|
||||
|
||||
- name: Copy .env.example
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Run services in background
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
docker-compose -f docker-compose.db.yml up -d
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
cargo run --bin revolt-delta &
|
||||
|
||||
- name: Wait for API to go up
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: nev7n/wait_for_response@v1
|
||||
with:
|
||||
url: "http://localhost:8000/"
|
||||
|
||||
- name: Checkout API repository
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: revoltchat/api
|
||||
path: api
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request'
|
||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
with:
|
||||
cwd: "api"
|
||||
add: "*.json"
|
||||
message: "chore: generate OpenAPI specification"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
54
.github/workflows/triage_issue.yml
vendored
Normal file
54
.github/workflows/triage_issue.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Add Issue to Board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
79
.github/workflows/triage_pr.yml
vendored
Normal file
79
.github/workflows/triage_pr.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation (
|
||||
$project: ID!
|
||||
$item: ID!
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: {
|
||||
singleSelectOptionId: $status_value
|
||||
}
|
||||
}) {
|
||||
projectV2Item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
Rocket.toml
|
||||
/target
|
||||
/target_backup
|
||||
**/*.rs.bk
|
||||
.mongo
|
||||
target
|
||||
.data
|
||||
.env
|
||||
avatar.png
|
||||
|
||||
.vercel
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
stages:
|
||||
- build
|
||||
|
||||
# Rocket does not currently compile on stable Rust.
|
||||
# Once it does, we can uncomment this, and instead
|
||||
# put `allow-failure: true` on the nightly build.
|
||||
#
|
||||
# rust-latest:
|
||||
# stage: build
|
||||
# image: rust:latest
|
||||
# script:
|
||||
# - cargo build --verbose
|
||||
# - cargo test --verbose
|
||||
|
||||
rust-nightly:
|
||||
stage: build
|
||||
image: rustlang/rust:nightly
|
||||
script:
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"rust-analyzer.diagnostics.disabled": [
|
||||
"unresolved-macro-call"
|
||||
]
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy"
|
||||
}
|
||||
3312
Cargo.lock
generated
3312
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
50
Cargo.toml
50
Cargo.toml
@@ -1,48 +1,2 @@
|
||||
[package]
|
||||
name = "revolt"
|
||||
# To help optimise CI and Docker builds.
|
||||
# Version here is left as 0.0.0, please
|
||||
# adjust and run ./set_version.sh instead.
|
||||
version = "0.0.0"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
regex = "1"
|
||||
md5 = "0.7.0"
|
||||
log = "0.4.11"
|
||||
ulid = "0.4.1"
|
||||
rand = "0.7.3"
|
||||
time = "0.2.16"
|
||||
nanoid = "0.4.0"
|
||||
base64 = "0.13.0"
|
||||
linkify = "0.6.0"
|
||||
dotenv = "0.15.0"
|
||||
futures = "0.3.8"
|
||||
chrono = "0.4.15"
|
||||
num_enum = "0.5.1"
|
||||
impl_ops = "0.1.1"
|
||||
web-push = "0.7.2"
|
||||
once_cell = "1.4.1"
|
||||
bitfield = "0.13.2"
|
||||
env_logger = "0.7.1"
|
||||
serde_json = "1.0.57"
|
||||
lazy_static = "1.4.0"
|
||||
urlencoding = "1.1.1"
|
||||
many-to-many = "0.1.2"
|
||||
lettre = "0.10.0-alpha.1"
|
||||
reqwest = { version = "0.10.8", features = ["json"] }
|
||||
serde = { version = "1.0.115", features = ["derive"] }
|
||||
validator = { version = "0.11", features = ["derive"] }
|
||||
ctrlc = { version = "3.0", features = ["termination"] }
|
||||
hive_pubsub = { version = "0.4.4", features = ["mongo"] }
|
||||
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
|
||||
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
|
||||
rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" }
|
||||
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
|
||||
rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "1db825a0059639144ea8802f6d51b5d27c5b986e" }
|
||||
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
|
||||
rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
|
||||
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false }
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
|
||||
27
Dockerfile
27
Dockerfile
@@ -1,25 +1,12 @@
|
||||
# Build Stage
|
||||
FROM ekidd/rust-musl-builder:nightly-2021-01-01 AS builder
|
||||
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
|
||||
# Install build requirements
|
||||
RUN apt-get update && apt-get install -y libssl-dev pkg-config
|
||||
|
||||
# Build all crates
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src/bin/dummy.rs ./src/bin/dummy.rs
|
||||
RUN cargo build --release --bin dummy
|
||||
|
||||
COPY assets/templates ./assets/templates
|
||||
COPY src ./src
|
||||
RUN cargo build --release
|
||||
|
||||
# Bundle Stage
|
||||
FROM alpine:latest
|
||||
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
|
||||
COPY --from=builder /home/rust/src/revolt/target/x86_64-unknown-linux-musl/release/revolt ./
|
||||
COPY assets ./assets
|
||||
EXPOSE 8000
|
||||
EXPOSE 9000
|
||||
ENV ROCKET_ADDRESS 0.0.0.0
|
||||
ENV ROCKET_PORT 8000
|
||||
CMD ["./revolt"]
|
||||
COPY crates ./crates
|
||||
RUN cargo build --locked --release
|
||||
|
||||
10
LICENSE
10
LICENSE
@@ -1,7 +1,7 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
@@ -629,8 +629,8 @@ to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
Server
|
||||
Copyright (C) 2021 REVOLT
|
||||
Revolt Project
|
||||
Copyright (C) 2022 Pawel Makles
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
@@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
@@ -658,4 +658,4 @@ specific requirements.
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
41
README.md
41
README.md
@@ -1,20 +1,31 @@
|
||||

|
||||
# Revolt Backend
|
||||
|
||||
Delta is a **blazing fast API server** built with Rust for the REVOLT platform.
|
||||
This is a monorepo for the Revolt backend.
|
||||
|
||||
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.
|
||||
| Crate | Path | Description |
|
||||
| ---------------- | ---------------------------------------------- | ------------------------------------ |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
|
||||
| `quark` | [crates/quark](crates/quark) | Models and logic |
|
||||
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer |
|
||||
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer |
|
||||
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |-->
|
||||
|
||||
## Docker Helper Scripts
|
||||
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
||||
|
||||
If you have Docker installed, you can use the helper scripts to deploy Revolt in your development environment.
|
||||
## Resources
|
||||
|
||||
```bash
|
||||
./build.sh # build Docker image
|
||||
./run.sh # run Docker container
|
||||
./monitor.sh # view container logs
|
||||
./remove.sh # kill and remove container
|
||||
```
|
||||
### 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)
|
||||
|
||||
## 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
|
||||
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). Please check individual crates for further license information.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,62 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-GB">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Reset your password.</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
|
||||
<style type="text/css">
|
||||
a[x-apple-data-detectors] {color: inherit !important;}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;">
|
||||
<table role="presentation" 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 style="margin: 0;">
|
||||
Reset your password by <a href="{{url}}">clicking here</a>.
|
||||
</p>
|
||||
<p>
|
||||
Or by manually navigating to the URL: {{url}}
|
||||
</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. · <a style="color: white;" href="https://revolt.chat">Website</a> · <a style="color: white;" href="https://gitlab.insrt.uk/revolt">Source Code</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,62 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-GB">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Verify your account.</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
|
||||
<style type="text/css">
|
||||
a[x-apple-data-detectors] {color: inherit !important;}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;">
|
||||
<table role="presentation" 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;">Verify your account!</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 style="margin: 0;">
|
||||
Please verify your account by <a href="{{url}}">clicking here</a>.
|
||||
</p>
|
||||
<p>
|
||||
Or by manually navigating to the URL: {{url}}
|
||||
</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. · <a style="color: white;" href="https://revolt.chat">Website</a> · <a style="color: white;" href="https://gitlab.insrt.uk/revolt">Source Code</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
2
crates/bonfire/.github/FUNDING.yml
vendored
Normal file
2
crates/bonfire/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
ko_fi: insertish
|
||||
custom: https://insrt.uk/donate
|
||||
98
crates/bonfire/.github/workflows/docker.yml
vendored
Normal file
98
crates/bonfire/.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
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: ghcr.io/revoltchat/bonfire
|
||||
- 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
crates/bonfire/.github/workflows/rust.yaml
vendored
Normal file
33
crates/bonfire/.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
crates/bonfire/.github/workflows/triage_issue.yml
vendored
Normal file
49
crates/bonfire/.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
crates/bonfire/.github/workflows/triage_pr.yml
vendored
Normal file
72
crates/bonfire/.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
|
||||
1
crates/bonfire/.gitignore
vendored
Normal file
1
crates/bonfire/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
28
crates/bonfire/Cargo.toml
Normal file
28
crates/bonfire/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.5.7"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# util
|
||||
log = "*"
|
||||
once_cell = "1.9.0"
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# serde
|
||||
serde_json = "1.0.79"
|
||||
rmp-serde = "1.0.0"
|
||||
serde = "1.0.136"
|
||||
|
||||
# async
|
||||
futures = "0.3.21"
|
||||
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
|
||||
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
|
||||
10
crates/bonfire/Dockerfile
Normal file
10
crates/bonfire/Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
# Build Stage
|
||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||
RUN cargo install --locked --path crates/bonfire
|
||||
|
||||
# Bundle Stage
|
||||
FROM debian:buster-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
COPY --from=builder /usr/local/cargo/bin/revolt-bonfire ./
|
||||
EXPOSE 9000
|
||||
CMD ["./revolt-bonfire"]
|
||||
1
crates/bonfire/LICENSE
Symbolic link
1
crates/bonfire/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE
|
||||
150
crates/bonfire/src/config.rs
Normal file
150
crates/bonfire/src/config.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_quark::{Error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Enumeration of supported protocol formats
|
||||
#[derive(Debug)]
|
||||
pub enum ProtocolFormat {
|
||||
Json,
|
||||
Msgpack,
|
||||
}
|
||||
|
||||
/// User-provided protocol configuration
|
||||
#[derive(Debug)]
|
||||
pub struct ProtocolConfiguration {
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
}
|
||||
|
||||
impl ProtocolConfiguration {
|
||||
/// Create a new protocol configuration object from provided data
|
||||
pub fn from(
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode some WebSocket message into a T: Deserialize using the client's specified protocol format
|
||||
pub fn decode<'a, T: Deserialize<'a>>(&self, msg: &'a Message) -> Result<T> {
|
||||
match self.format {
|
||||
ProtocolFormat::Json => {
|
||||
if let Message::Text(text) = msg {
|
||||
serde_json::from_str(text).map_err(|_| Error::InternalError)
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
}
|
||||
}
|
||||
ProtocolFormat::Msgpack => {
|
||||
if let Message::Binary(buf) = msg {
|
||||
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError)
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode T: Serialize into a WebSocket message using the client's specified protocol format
|
||||
pub fn encode<T: Serialize>(&self, data: &T) -> Message {
|
||||
match self.format {
|
||||
ProtocolFormat::Json => {
|
||||
Message::Text(serde_json::to_string(data).expect("Failed to serialise (as json)."))
|
||||
}
|
||||
ProtocolFormat::Msgpack => Message::Binary(
|
||||
rmp_serde::to_vec_named(data).expect("Failed to serialise (as msgpack)."),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current session token
|
||||
pub fn set_session_token(&mut self, token: String) {
|
||||
self.session_token.replace(token);
|
||||
}
|
||||
|
||||
/// Get the current session token
|
||||
pub fn get_session_token(&self) -> &Option<String> {
|
||||
&self.session_token
|
||||
}
|
||||
|
||||
/// Get the protocol version specified
|
||||
pub fn get_protocol_version(&self) -> i32 {
|
||||
self.protocol_version
|
||||
}
|
||||
|
||||
/// Get the protocol format specified
|
||||
pub fn get_protocol_format(&self) -> &ProtocolFormat {
|
||||
&self.format
|
||||
}
|
||||
}
|
||||
|
||||
/// Object holding one side of a channel for receiving the parsed information
|
||||
pub struct WebsocketHandshakeCallback {
|
||||
sender: Sender<ProtocolConfiguration>,
|
||||
}
|
||||
|
||||
impl WebsocketHandshakeCallback {
|
||||
/// Create a callback using a given sender
|
||||
pub fn from(sender: Sender<ProtocolConfiguration>) -> Self {
|
||||
Self { sender }
|
||||
}
|
||||
}
|
||||
|
||||
impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
/// Handle request to create a new WebSocket connection
|
||||
fn on_request(
|
||||
self,
|
||||
request: &handshake::server::Request,
|
||||
response: handshake::server::Response,
|
||||
) -> Result<handshake::server::Response, handshake::server::ErrorResponse> {
|
||||
// Take and parse query parameters from the URI.
|
||||
let query = request.uri().query().unwrap_or_default();
|
||||
let params = querystring::querify(query);
|
||||
|
||||
// Set default values for the protocol.
|
||||
let mut protocol_version = 1;
|
||||
let mut format = ProtocolFormat::Json;
|
||||
let mut session_token = None;
|
||||
|
||||
// Parse and map parameters from key-value to known variables.
|
||||
for (key, value) in params {
|
||||
match key {
|
||||
"version" => {
|
||||
if let Ok(version) = value.parse() {
|
||||
protocol_version = version;
|
||||
}
|
||||
}
|
||||
"format" => match value {
|
||||
"json" => format = ProtocolFormat::Json,
|
||||
"msgpack" => format = ProtocolFormat::Msgpack,
|
||||
_ => {}
|
||||
},
|
||||
"token" => session_token = Some(value.into()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Send configuration information back from this callback.
|
||||
// We have to use a channel as this function does not borrow mutably.
|
||||
if self
|
||||
.sender
|
||||
.send(ProtocolConfiguration {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(handshake::server::ErrorResponse::new(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
19
crates/bonfire/src/database.rs
Normal file
19
crates/bonfire/src/database.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use revolt_quark::{Database, DatabaseInfo};
|
||||
|
||||
static DBCONN: OnceCell<Database> = OnceCell::new();
|
||||
|
||||
/// Connect Bonfire to the database.
|
||||
pub async fn connect() {
|
||||
let database = DatabaseInfo::Auto
|
||||
.connect()
|
||||
.await
|
||||
.expect("Failed to connect to the database.");
|
||||
|
||||
DBCONN.set(database).expect("Setting `Database`");
|
||||
}
|
||||
|
||||
/// Get a reference to the current database.
|
||||
pub fn get_db() -> &'static Database {
|
||||
DBCONN.get().expect("Valid `Database`")
|
||||
}
|
||||
34
crates/bonfire/src/main.rs
Normal file
34
crates/bonfire/src/main.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::env;
|
||||
|
||||
use async_std::net::TcpListener;
|
||||
use revolt_quark::presence::presence_clear_region;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod config;
|
||||
|
||||
mod database;
|
||||
mod websocket;
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
// Configure requirements for Bonfire.
|
||||
revolt_quark::configure!();
|
||||
database::connect().await;
|
||||
|
||||
// Clean up the current region information.
|
||||
presence_clear_region(None).await;
|
||||
|
||||
// Setup a TCP listener to accept WebSocket connections on.
|
||||
// By default, we bind to port 9000 on all interfaces.
|
||||
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:9000".into());
|
||||
info!("Listening on host {bind}");
|
||||
let try_socket = TcpListener::bind(bind).await;
|
||||
let listener = try_socket.expect("Failed to bind");
|
||||
|
||||
// Start accepting new connections and spawn a client for each connection.
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
websocket::spawn_client(database::get_db(), stream, addr);
|
||||
}
|
||||
}
|
||||
253
crates/bonfire/src/websocket.rs
Normal file
253
crates/bonfire/src/websocket.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use revolt_quark::{
|
||||
events::{
|
||||
client::EventV1,
|
||||
server::ClientMessage,
|
||||
state::{State, SubscriptionStateChange},
|
||||
},
|
||||
models::{user::UserHint, User},
|
||||
presence::{presence_create_session, presence_delete_session},
|
||||
redis_kiss, Database,
|
||||
};
|
||||
|
||||
use async_std::{net::TcpStream, sync::Mutex, task};
|
||||
|
||||
use crate::config::WebsocketHandshakeCallback;
|
||||
|
||||
/// Spawn a new WebSocket client worker given access to the database,
|
||||
/// the relevant TCP stream and the remote address of the client.
|
||||
pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
|
||||
// Spawn a new Async task to work on.
|
||||
task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
|
||||
// Upgrade the TCP connection to a WebSocket connection.
|
||||
// In this process, we also parse any additional parameters given.
|
||||
// e.g. wss://example.com?format=json&version=1
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
if let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
|
||||
stream,
|
||||
WebsocketHandshakeCallback::from(sender),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
if let Ok(mut config) = receiver.await {
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (write, mut read) = ws.split();
|
||||
let write = Mutex::new(write);
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
'outer: while let Ok(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
config.set_session_token(token);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
if let Some(token) = config.get_session_token().as_ref() {
|
||||
match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => {
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) =
|
||||
presence_create_session(&user_id, 0).await;
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
if let Ok(ready_payload) = state.generate_ready_payload(db).await {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&ready_payload))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = async {
|
||||
if let Ok(mut conn) = redis_kiss::open_pubsub_connection().await
|
||||
{
|
||||
loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state() {
|
||||
SubscriptionStateChange::Reset => {
|
||||
for id in state.iter_subscriptions() {
|
||||
conn.subscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} has reset their subscriptions");
|
||||
}
|
||||
SubscriptionStateChange::Change { add, remove } => {
|
||||
for id in remove {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
conn.unsubscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
conn.subscribe(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
// * Debug logging of current subscriptions.
|
||||
/*#[cfg(debug_assertions)]
|
||||
info!(
|
||||
"User {addr:?} is subscribed to {:?}",
|
||||
state
|
||||
.iter_subscriptions()
|
||||
.collect::<Vec<&String>>()
|
||||
);*/
|
||||
|
||||
// Handle incoming events.
|
||||
match conn.on_message().next().await.map(|item| {
|
||||
(
|
||||
item.get_channel_name().to_string(),
|
||||
redis_kiss::decode_payload::<EventV1>(&item),
|
||||
)
|
||||
}) {
|
||||
Some((channel, item)) => {
|
||||
if let Ok(mut event) = item {
|
||||
if state
|
||||
.handle_incoming_event_v1(
|
||||
db, &mut event,
|
||||
)
|
||||
.await
|
||||
&& write.lock().await
|
||||
.send(config.encode(&event))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
warn!("Failed to deserialise an event for {channel}!");
|
||||
}
|
||||
}
|
||||
// No more data, assume we disconnected or otherwise
|
||||
// something bad occurred, so disconnect user.
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker =
|
||||
async {
|
||||
while let Ok(Some(msg)) = read.try_next().await {
|
||||
if let Ok(payload) = config.decode(&msg) {
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(
|
||||
&EventV1::Pong { data },
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
|
||||
// Wait for either disconnect or for listener to die.
|
||||
select!(
|
||||
() = listener => {},
|
||||
() = worker => {}
|
||||
);
|
||||
|
||||
// * Combine the streams back once we are ready to disconnect.
|
||||
/* ws = read.reunite(write).unwrap(); */
|
||||
}
|
||||
|
||||
// Clean up presence session.
|
||||
let last_session = presence_delete_session(&user_id, session_id).await;
|
||||
|
||||
// If this was the last session, notify other users that we just went offline.
|
||||
if last_session {
|
||||
state.broadcast_presence_change(false).await;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
write.lock().await.send(config.encode(&err)).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Disconnect the WebSocket if it isn't already.
|
||||
/*ws.close(Some(CloseFrame {
|
||||
code: CloseCode::Normal,
|
||||
reason: std::borrow::Cow::from(""),
|
||||
}))
|
||||
.await
|
||||
.unwrap();*/
|
||||
}
|
||||
|
||||
info!("User disconnected from {addr:?}");
|
||||
});
|
||||
}
|
||||
65
crates/delta/Cargo.toml
Normal file
65
crates/delta/Cargo.toml
Normal file
@@ -0,0 +1,65 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.5.7"
|
||||
license = "AGPL-3.0-or-later"
|
||||
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]
|
||||
# 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"
|
||||
|
||||
# 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"
|
||||
|
||||
# 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.2", default-features = false, features = ["json"] }
|
||||
rocket_empty = { version = "0.1.1", features = ["schema"] }
|
||||
rocket_authifier = { version = "1.0.7" }
|
||||
|
||||
# spec generation
|
||||
schemars = "0.8.8"
|
||||
revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
[build-dependencies]
|
||||
vergen = "7.5.0"
|
||||
13
crates/delta/Dockerfile
Normal file
13
crates/delta/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
# Build Stage
|
||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||
RUN cargo install --locked --path crates/delta
|
||||
|
||||
# Bundle Stage
|
||||
FROM debian:buster-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
COPY --from=builder /usr/local/cargo/bin/revolt-delta ./
|
||||
|
||||
EXPOSE 8000
|
||||
ENV ROCKET_ADDRESS 0.0.0.0
|
||||
ENV ROCKET_PORT 8000
|
||||
CMD ["./revolt-delta"]
|
||||
1
crates/delta/LICENSE
Symbolic link
1
crates/delta/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE
|
||||
16
crates/delta/build.rs
Normal file
16
crates/delta/build.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
if let Ok(output) = Command::new("git")
|
||||
.args(["config", "--get", "remote.origin.url"])
|
||||
.output()
|
||||
{
|
||||
if let Ok(git_origin) = String::from_utf8(output.stdout) {
|
||||
println!("cargo:rustc-env=GIT_ORIGIN_URL={}", git_origin);
|
||||
}
|
||||
}
|
||||
|
||||
vergen(Config::default()).ok();
|
||||
}
|
||||
73
crates/delta/src/main.rs
Normal file
73
crates/delta/src/main.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate revolt_rocket_okapi;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
pub mod routes;
|
||||
pub mod util;
|
||||
|
||||
use async_std::channel::unbounded;
|
||||
use revolt_quark::authifier::{Authifier, AuthifierEvent};
|
||||
use revolt_quark::events::client::EventV1;
|
||||
use revolt_quark::DatabaseInfo;
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
// Configure logging and environment
|
||||
revolt_quark::configure!();
|
||||
|
||||
// Ensure environment variables are present
|
||||
revolt_quark::variables::delta::preflight_checks();
|
||||
|
||||
// Setup database
|
||||
let db = DatabaseInfo::Auto.connect().await.unwrap();
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Setup Authifier event channel
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
// Setup Authifier
|
||||
let authifier = Authifier {
|
||||
database: db.clone().into(),
|
||||
config: revolt_quark::util::authifier::config(),
|
||||
event_channel: Some(sender),
|
||||
};
|
||||
|
||||
// Launch a listener for Authifier events
|
||||
async_std::task::spawn(async move {
|
||||
while let Ok(event) = receiver.recv().await {
|
||||
match &event {
|
||||
AuthifierEvent::CreateSession { .. } | AuthifierEvent::CreateAccount { .. } => {
|
||||
EventV1::Auth(event).global().await
|
||||
}
|
||||
AuthifierEvent::DeleteSession { user_id, .. }
|
||||
| AuthifierEvent::DeleteAllSessions { user_id, .. } => {
|
||||
let id = user_id.to_string();
|
||||
EventV1::Auth(event).private(id).await
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Launch background task workers
|
||||
async_std::task::spawn(revolt_quark::tasks::start_workers(db.clone()));
|
||||
|
||||
// Configure CORS
|
||||
let cors = revolt_quark::web::cors::new();
|
||||
|
||||
// Configure Rocket
|
||||
let rocket = rocket::build();
|
||||
routes::mount(rocket)
|
||||
.mount("/", revolt_quark::web::cors::catch_all_options_routes())
|
||||
.mount("/", revolt_quark::web::ratelimiter::routes())
|
||||
.mount("/swagger/", revolt_quark::web::swagger::routes())
|
||||
.manage(authifier)
|
||||
.manage(db)
|
||||
.manage(cors.clone())
|
||||
.attach(revolt_quark::web::ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
}
|
||||
65
crates/delta/src/routes/bots/create.rs
Normal file
65
crates/delta/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
crates/delta/src/routes/bots/delete.rs
Normal file
19
crates/delta/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
crates/delta/src/routes/bots/edit.rs
Normal file
107
crates/delta/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
crates/delta/src/routes/bots/fetch.rs
Normal file
36
crates/delta/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
crates/delta/src/routes/bots/fetch_owned.rs
Normal file
42
crates/delta/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
crates/delta/src/routes/bots/fetch_public.rs
Normal file
44
crates/delta/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
crates/delta/src/routes/bots/invite.rs
Normal file
71
crates/delta/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
crates/delta/src/routes/bots/mod.rs
Normal file
22
crates/delta/src/routes/bots/mod.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
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,
|
||||
]
|
||||
}
|
||||
23
crates/delta/src/routes/channels/channel_ack.rs
Normal file
23
crates/delta/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)
|
||||
}
|
||||
60
crates/delta/src/routes/channels/channel_delete.rs
Normal file
60
crates/delta/src/routes/channels/channel_delete.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use revolt_quark::{
|
||||
models::{channel::PartialChannel, Channel, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
|
||||
pub struct OptionsChannelDelete {
|
||||
/// Whether to not send a leave message
|
||||
leave_silently: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Close Channel
|
||||
///
|
||||
/// Deletes a server channel, leaves a group or closes a group.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[delete("/<target>?<options..>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: OptionsChannelDelete,
|
||||
) -> 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,
|
||||
options.leave_silently.unwrap_or_default(),
|
||||
)
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
|
||||
perms
|
||||
.throw_permission(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
|
||||
channel.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
210
crates/delta/src/routes/channels/channel_edit.rs
Normal file
210
crates/delta/src/routes/channels/channel_edit.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
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))]
|
||||
name: Option<String>,
|
||||
/// Channel description
|
||||
#[validate(length(min = 0, max = 1024))]
|
||||
description: Option<String>,
|
||||
/// Group owner
|
||||
owner: Option<String>,
|
||||
/// Icon
|
||||
///
|
||||
/// Provide an Autumn attachment Id.
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
icon: Option<String>,
|
||||
/// Whether this channel is age-restricted
|
||||
nsfw: Option<bool>,
|
||||
/// Whether this channel is archived
|
||||
archived: 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.owner.is_none()
|
||||
&& data.remove.is_none()
|
||||
{
|
||||
return Ok(Json(channel));
|
||||
}
|
||||
|
||||
let mut partial: PartialChannel = Default::default();
|
||||
|
||||
// Transfer group ownership
|
||||
if let Some(new_owner) = data.owner {
|
||||
if let Channel::Group {
|
||||
owner, recipients, ..
|
||||
} = &mut channel
|
||||
{
|
||||
// Make sure we are the owner of this group
|
||||
if owner != &user.id {
|
||||
return Err(Error::NotOwner);
|
||||
}
|
||||
|
||||
// Ensure user is part of group
|
||||
if !recipients.contains(&new_owner) {
|
||||
return Err(Error::NotInGroup);
|
||||
}
|
||||
|
||||
// Transfer ownership
|
||||
partial.owner = Some(new_owner.to_string());
|
||||
let old_owner = std::mem::replace(owner, new_owner.to_string());
|
||||
|
||||
// Notify clients
|
||||
SystemMessage::ChannelOwnershipChanged {
|
||||
from: old_owner,
|
||||
to: new_owner,
|
||||
}
|
||||
} else {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
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
crates/delta/src/routes/channels/channel_fetch.rs
Normal file
21
crates/delta/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
crates/delta/src/routes/channels/group_add_member.rs
Normal file
32
crates/delta/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
crates/delta/src/routes/channels/group_create.rs
Normal file
84
crates/delta/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
crates/delta/src/routes/channels/group_remove_member.rs
Normal file
42
crates/delta/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), false)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
_ => Err(Error::InvalidOperation),
|
||||
}
|
||||
}
|
||||
27
crates/delta/src/routes/channels/invite_create.rs
Normal file
27
crates/delta/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
crates/delta/src/routes/channels/members_fetch.rs
Normal file
35
crates/delta/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
crates/delta/src/routes/channels/message_bulk_delete.rs
Normal file
59
crates/delta/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)
|
||||
}
|
||||
34
crates/delta/src/routes/channels/message_clear_reactions.rs
Normal file
34
crates/delta/src/routes/channels/message_clear_reactions.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use revolt_quark::{
|
||||
models::{message::PartialMessage, User},
|
||||
perms, Db, EmptyResponse, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Remove All Reactions from Message
|
||||
///
|
||||
/// Remove your own, someone else's or all of a given reaction.
|
||||
///
|
||||
/// Requires `ManageMessages` permission.
|
||||
#[openapi(tag = "Interactions")]
|
||||
#[delete("/<target>/messages/<msg>/reactions")]
|
||||
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::ManageMessages)
|
||||
.await?;
|
||||
|
||||
// Fetch relevant message
|
||||
let mut message = msg.as_message_in(db, channel.id()).await?;
|
||||
|
||||
// Clear reactions
|
||||
message
|
||||
.update(
|
||||
db,
|
||||
PartialMessage {
|
||||
reactions: Some(Default::default()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
22
crates/delta/src/routes/channels/message_delete.rs
Normal file
22
crates/delta/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)
|
||||
}
|
||||
95
crates/delta/src/routes/channels/message_edit.rs
Normal file
95
crates/delta/src/routes/channels/message_edit.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
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 })?;
|
||||
|
||||
Message::validate_sum(&edit.content, &edit.embeds)?;
|
||||
|
||||
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
crates/delta/src/routes/channels/message_fetch.rs
Normal file
26
crates/delta/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
crates/delta/src/routes/channels/message_query.rs
Normal file
81
crates/delta/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
crates/delta/src/routes/channels/message_query_stale.rs
Normal file
26
crates/delta/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(())
|
||||
}
|
||||
29
crates/delta/src/routes/channels/message_react.rs
Normal file
29
crates/delta/src/routes/channels/message_react.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
|
||||
|
||||
/// # Add Reaction to Message
|
||||
///
|
||||
/// React to a given message.
|
||||
#[openapi(tag = "Interactions")]
|
||||
#[put("/<target>/messages/<msg>/reactions/<emoji>")]
|
||||
pub async fn react_message(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
msg: Ref,
|
||||
emoji: Ref,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::React)
|
||||
.await?;
|
||||
|
||||
// Fetch relevant message
|
||||
let message = msg.as_message_in(db, channel.id()).await?;
|
||||
|
||||
// Add the reaction
|
||||
message
|
||||
.add_reaction(db, &user, &emoji.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
82
crates/delta/src/routes/channels/message_search.rs
Normal file
82
crates/delta/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)
|
||||
}
|
||||
231
crates/delta/src/routes/channels/message_send.rs
Normal file
231
crates/delta/src/routes/channels/message_send.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
message::{Interactions, Masquerade, Reply, SendableEmbed},
|
||||
Message, User,
|
||||
},
|
||||
perms,
|
||||
web::idempotency::IdempotencyKey,
|
||||
Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataMessageSend {
|
||||
/// Unique token to prevent duplicate message sending
|
||||
///
|
||||
/// **This is deprecated and replaced by `Idempotency-Key`!**
|
||||
#[validate(length(min = 1, max = 64))]
|
||||
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
|
||||
///
|
||||
/// Text embed content contributes to the content length cap
|
||||
#[validate(length(min = 1, max = 10))]
|
||||
embeds: Option<Vec<SendableEmbed>>,
|
||||
/// Masquerade to apply to this message
|
||||
#[validate]
|
||||
masquerade: Option<Masquerade>,
|
||||
/// Information about how this message should be interacted with
|
||||
interactions: Option<Interactions>,
|
||||
}
|
||||
|
||||
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 })?;
|
||||
|
||||
// Validate Message is within reasonable length limits
|
||||
Message::validate_sum(&data.content, &data.embeds)?;
|
||||
|
||||
// Ensure the request is unique
|
||||
idempotency.consume_nonce(data.nonce).await?;
|
||||
|
||||
// Ensure we have permissions to send a message
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
||||
.await?;
|
||||
|
||||
// Check the message is not empty
|
||||
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);
|
||||
}
|
||||
|
||||
// Ensure restrict_reactions is not specified without reactions list
|
||||
if let Some(interactions) = &data.interactions {
|
||||
if interactions.restrict_reactions {
|
||||
let disallowed = if let Some(list) = &interactions.reactions {
|
||||
list.is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if disallowed {
|
||||
return Err(Error::InvalidProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start constructing the message
|
||||
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,
|
||||
interactions: data.interactions.unwrap_or_default(),
|
||||
..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 let Some(masq) = &message.masquerade {
|
||||
permissions
|
||||
.throw_permission(db, Permission::Masquerade)
|
||||
.await?;
|
||||
|
||||
if masq.colour.is_some() {
|
||||
permissions
|
||||
.throw_permission(db, Permission::ManageRole)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Ensure interactions information is correct
|
||||
message.interactions.validate(db, &mut permissions).await?;
|
||||
|
||||
// 4. 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>>());
|
||||
}
|
||||
|
||||
// 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. 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);
|
||||
}
|
||||
|
||||
// 7. Set content
|
||||
message.content = data.content;
|
||||
|
||||
// 8. 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))
|
||||
}
|
||||
58
crates/delta/src/routes/channels/message_unreact.rs
Normal file
58
crates/delta/src/routes/channels/message_unreact.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Serialize, Deserialize, JsonSchema, FromForm)]
|
||||
pub struct OptionsUnreact {
|
||||
/// Remove a specific user's reaction
|
||||
user_id: Option<String>,
|
||||
/// Remove all reactions
|
||||
remove_all: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Remove Reaction(s) to Message
|
||||
///
|
||||
/// Remove your own, someone else's or all of a given reaction.
|
||||
///
|
||||
/// Requires `ManageMessages` if changing others' reactions.
|
||||
#[openapi(tag = "Interactions")]
|
||||
#[delete("/<target>/messages/<msg>/reactions/<emoji>?<options..>")]
|
||||
pub async fn unreact_message(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
msg: Ref,
|
||||
emoji: Ref,
|
||||
options: OptionsUnreact,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::React)
|
||||
.await?;
|
||||
|
||||
// Check if we need to escalate permissions
|
||||
let remove_all = options.remove_all.unwrap_or_default();
|
||||
if options.user_id.is_some() || remove_all {
|
||||
permissions
|
||||
.throw_permission(db, Permission::ManageMessages)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Fetch relevant message
|
||||
let message = msg.as_message_in(db, channel.id()).await?;
|
||||
|
||||
// Check if we should wipe all of this reaction
|
||||
if remove_all {
|
||||
return message
|
||||
.clear_reaction(db, &emoji.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse);
|
||||
}
|
||||
|
||||
// Remove the reaction
|
||||
message
|
||||
.remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), &emoji.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
54
crates/delta/src/routes/channels/mod.rs
Normal file
54
crates/delta/src/routes/channels/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
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_clear_reactions;
|
||||
mod message_delete;
|
||||
mod message_edit;
|
||||
mod message_fetch;
|
||||
mod message_query;
|
||||
mod message_query_stale;
|
||||
mod message_react;
|
||||
mod message_search;
|
||||
mod message_send;
|
||||
mod message_unreact;
|
||||
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,
|
||||
message_react::react_message,
|
||||
message_unreact::unreact_message,
|
||||
message_clear_reactions::clear_reactions
|
||||
]
|
||||
}
|
||||
59
crates/delta/src/routes/channels/permissions_set.rs
Normal file
59
crates/delta/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
crates/delta/src/routes/channels/permissions_set_default.rs
Normal file
95
crates/delta/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))
|
||||
}
|
||||
@@ -1,44 +1,50 @@
|
||||
use crate::database::*;
|
||||
use crate::util::result::{Error, Result};
|
||||
use crate::util::variables::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL};
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms,
|
||||
variables::delta::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL},
|
||||
Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket_contrib::json::JsonValue;
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CreateUserResponse {
|
||||
/// # 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(user: User, target: Ref) -> Result<JsonValue> {
|
||||
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);
|
||||
}
|
||||
|
||||
let target = target.fetch_channel().await?;
|
||||
match target {
|
||||
match channel {
|
||||
Channel::SavedMessages { .. } | Channel::TextChannel { .. } => {
|
||||
return Err(Error::CannotJoinCall)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let perm = permissions::PermissionCalculator::new(&user)
|
||||
.with_channel(&target)
|
||||
.for_channel()
|
||||
.await?;
|
||||
|
||||
if !perm.get_voice_call() {
|
||||
return Err(Error::MissingPermission);
|
||||
}
|
||||
|
||||
// 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, target.id()))
|
||||
.get(&format!("{}/room/{}", *VOSO_URL, channel.id()))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
VOSO_MANAGE_TOKEN.to_string(),
|
||||
@@ -51,14 +57,15 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||
Ok(result) => match result.status() {
|
||||
reqwest::StatusCode::OK => (),
|
||||
reqwest::StatusCode::NOT_FOUND => {
|
||||
if let Err(_) = client
|
||||
.post(&format!("{}/room/{}", *VOSO_URL, target.id()))
|
||||
if (client
|
||||
.post(&format!("{}/room/{}", *VOSO_URL, channel.id()))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
VOSO_MANAGE_TOKEN.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.await)
|
||||
.is_err()
|
||||
{
|
||||
return Err(Error::VosoUnavailable);
|
||||
}
|
||||
@@ -72,7 +79,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||
.post(&format!(
|
||||
"{}/room/{}/user/{}",
|
||||
*VOSO_URL,
|
||||
target.id(),
|
||||
channel.id(),
|
||||
user.id
|
||||
))
|
||||
.header(
|
||||
@@ -82,9 +89,11 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
let res: CreateUserResponse = response.json().await.map_err(|_| Error::InvalidOperation)?;
|
||||
|
||||
Ok(json!(res))
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|_| Error::InvalidOperation)
|
||||
.map(Json)
|
||||
} else {
|
||||
Err(Error::VosoUnavailable)
|
||||
}
|
||||
81
crates/delta/src/routes/customisation/emoji_create.rs
Normal file
81
crates/delta/src/routes/customisation/emoji_create.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use revolt_quark::models::emoji::EmojiParent;
|
||||
use revolt_quark::models::{Emoji, File, User};
|
||||
use revolt_quark::{perms, Db, Error, Permission, Result};
|
||||
use serde::Deserialize;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::util::regex::RE_EMOJI;
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Emoji Data
|
||||
#[derive(Validate, Deserialize, JsonSchema)]
|
||||
pub struct DataCreateEmoji {
|
||||
/// Server name
|
||||
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
|
||||
name: String,
|
||||
/// Parent information
|
||||
parent: EmojiParent,
|
||||
/// Whether the emoji is mature
|
||||
#[serde(default)]
|
||||
nsfw: bool,
|
||||
}
|
||||
|
||||
/// # Create New Emoji
|
||||
///
|
||||
/// Create an emoji by its Autumn upload id.
|
||||
#[openapi(tag = "Emojis")]
|
||||
#[put("/emoji/<id>", data = "<data>")]
|
||||
pub async fn create_emoji(
|
||||
db: &Db,
|
||||
user: User,
|
||||
id: String,
|
||||
data: Json<DataCreateEmoji>,
|
||||
) -> Result<Json<Emoji>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
// Bots cannot manage emojis
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
// Validate we have permission to write into parent
|
||||
match &data.parent {
|
||||
EmojiParent::Server { id } => {
|
||||
let server = db.fetch_server(id).await?;
|
||||
|
||||
// Check for permission
|
||||
perms(&user)
|
||||
.server(&server)
|
||||
.throw_permission(db, Permission::ManageCustomisation)
|
||||
.await?;
|
||||
|
||||
// Check that there are no more than 100 emoji
|
||||
// ! FIXME: hardcoded upper limit
|
||||
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
|
||||
if emojis.len() > 99 {
|
||||
return Err(Error::TooManyEmoji);
|
||||
}
|
||||
}
|
||||
EmojiParent::Detached => return Err(Error::InvalidOperation),
|
||||
};
|
||||
|
||||
// Find the relevant attachment
|
||||
let attachment = File::use_emoji(db, &id, &id).await?;
|
||||
|
||||
// Create the emoji object
|
||||
let emoji = Emoji {
|
||||
id,
|
||||
parent: data.parent,
|
||||
creator_id: user.id,
|
||||
name: data.name,
|
||||
animated: "image/gif" == &attachment.content_type,
|
||||
nsfw: data.nsfw,
|
||||
};
|
||||
|
||||
// Save emoji
|
||||
emoji.create(db).await?;
|
||||
Ok(Json(emoji))
|
||||
}
|
||||
37
crates/delta/src/routes/customisation/emoji_delete.rs
Normal file
37
crates/delta/src/routes/customisation/emoji_delete.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use revolt_quark::models::{emoji::EmojiParent, User};
|
||||
use revolt_quark::{perms, Db, EmptyResponse, Error, Permission, Ref, Result};
|
||||
|
||||
/// # Delete Emoji
|
||||
///
|
||||
/// Delete an emoji by its id.
|
||||
#[openapi(tag = "Emojis")]
|
||||
#[delete("/emoji/<id>")]
|
||||
pub async fn delete_emoji(db: &Db, user: User, id: Ref) -> Result<EmptyResponse> {
|
||||
// Bots cannot manage emoji
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
// Fetch the emoji
|
||||
let emoji = id.as_emoji(db).await?;
|
||||
|
||||
// If we uploaded the emoji, then we have permission to delete it
|
||||
if emoji.creator_id != user.id {
|
||||
// Otherwise, validate we have permission to delete from parent
|
||||
match &emoji.parent {
|
||||
EmojiParent::Server { id } => {
|
||||
let server = db.fetch_server(id).await?;
|
||||
|
||||
// Check for permission
|
||||
perms(&user)
|
||||
.server(&server)
|
||||
.throw_permission(db, Permission::ManageCustomisation)
|
||||
.await?;
|
||||
}
|
||||
EmojiParent::Detached => return Ok(EmptyResponse),
|
||||
};
|
||||
}
|
||||
|
||||
// Delete the emoji
|
||||
emoji.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
13
crates/delta/src/routes/customisation/emoji_fetch.rs
Normal file
13
crates/delta/src/routes/customisation/emoji_fetch.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use revolt_quark::models::{Emoji, User};
|
||||
use revolt_quark::{Db, Ref, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Fetch Emoji
|
||||
///
|
||||
/// Fetch an emoji by its id.
|
||||
#[openapi(tag = "Emojis")]
|
||||
#[get("/emoji/<id>")]
|
||||
pub async fn fetch_emoji(db: &Db, _user: User, id: Ref) -> Result<Json<Emoji>> {
|
||||
id.as_emoji(db).await.map(Json)
|
||||
}
|
||||
14
crates/delta/src/routes/customisation/mod.rs
Normal file
14
crates/delta/src/routes/customisation/mod.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
mod emoji_create;
|
||||
mod emoji_delete;
|
||||
mod emoji_fetch;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
emoji_create::create_emoji,
|
||||
emoji_delete::delete_emoji,
|
||||
emoji_fetch::fetch_emoji
|
||||
]
|
||||
}
|
||||
31
crates/delta/src/routes/invites/invite_delete.rs
Normal file
31
crates/delta/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)
|
||||
}
|
||||
135
crates/delta/src/routes/invites/invite_fetch.rs
Normal file
135
crates/delta/src/routes/invites/invite_fetch.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
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>,
|
||||
/// Enum of server flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_flags: Option<i32>,
|
||||
/// 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,
|
||||
server_flags: server.flags,
|
||||
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
crates/delta/src/routes/invites/invite_join.rs
Normal file
47
crates/delta/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
crates/delta/src/routes/invites/mod.rs
Normal file
10
crates/delta/src/routes/invites/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
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]
|
||||
}
|
||||
283
crates/delta/src/routes/mod.rs
Normal file
283
crates/delta/src/routes/mod.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use revolt_rocket_okapi::{revolt_okapi::openapi3::OpenApi, settings::OpenApiSettings};
|
||||
pub use rocket::http::Status;
|
||||
pub use rocket::response::Redirect;
|
||||
use rocket::{Build, Rocket};
|
||||
|
||||
mod bots;
|
||||
mod channels;
|
||||
mod customisation;
|
||||
mod invites;
|
||||
mod onboard;
|
||||
mod push;
|
||||
mod root;
|
||||
mod servers;
|
||||
mod sync;
|
||||
mod users;
|
||||
|
||||
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
let settings = OpenApiSettings::default();
|
||||
|
||||
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(),
|
||||
"/custom" => customisation::routes(),
|
||||
"/auth/account" => rocket_authifier::routes::account::routes(),
|
||||
"/auth/session" => rocket_authifier::routes::session::routes(),
|
||||
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
|
||||
"/onboard" => onboard::routes(),
|
||||
"/push" => push::routes(),
|
||||
"/sync" => sync::routes(),
|
||||
};
|
||||
|
||||
rocket
|
||||
}
|
||||
|
||||
fn custom_openapi_spec() -> OpenApi {
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::*;
|
||||
|
||||
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",
|
||||
"Interactions",
|
||||
"Groups",
|
||||
"Voice"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Servers",
|
||||
"tags": [
|
||||
"Server Information",
|
||||
"Server Members",
|
||||
"Server Permissions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Invites",
|
||||
"tags": [
|
||||
"Invites"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Customisation",
|
||||
"tags": [
|
||||
"Emojis"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Authentication",
|
||||
"tags": [
|
||||
"Account",
|
||||
"Session",
|
||||
"Onboarding",
|
||||
"MFA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Miscellaneous",
|
||||
"tags": [
|
||||
"Sync",
|
||||
"Web Push"
|
||||
]
|
||||
}
|
||||
]),
|
||||
);
|
||||
|
||||
OpenApi {
|
||||
openapi: OpenApi::default_version(),
|
||||
info: Info {
|
||||
title: "Revolt API".to_owned(),
|
||||
description: Some("Open source user-first 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: env!("CARGO_PKG_VERSION").to_string(),
|
||||
..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: "MFA".to_owned(),
|
||||
description: Some("Multi-factor Authentication".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()
|
||||
}
|
||||
}
|
||||
45
crates/delta/src/routes/onboard/complete.rs
Normal file
45
crates/delta/src/routes/onboard/complete.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use crate::util::regex::RE_USERNAME;
|
||||
use revolt_quark::{
|
||||
authifier::models::Session, models::User, Database, EmptyResponse, Error, Result,
|
||||
};
|
||||
|
||||
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 })?;
|
||||
|
||||
let username = User::validate_username(db, data.username).await?;
|
||||
let user = User {
|
||||
id: session.user_id,
|
||||
username,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_user(&user).await.map(|_| EmptyResponse)
|
||||
}
|
||||
21
crates/delta/src/routes/onboard/hello.rs
Normal file
21
crates/delta/src/routes/onboard/hello.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use revolt_quark::{authifier::models::Session, 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
crates/delta/src/routes/onboard/mod.rs
Normal file
9
crates/delta/src/routes/onboard/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
mod complete;
|
||||
mod hello;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![hello::req, complete::req]
|
||||
}
|
||||
9
crates/delta/src/routes/push/mod.rs
Normal file
9
crates/delta/src/routes/push/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
mod subscribe;
|
||||
mod unsubscribe;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![subscribe::req, unsubscribe::req]
|
||||
}
|
||||
32
crates/delta/src/routes/push/subscribe.rs
Normal file
32
crates/delta/src/routes/push/subscribe.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use revolt_quark::{
|
||||
authifier::{
|
||||
models::{Session, WebPushSubscription},
|
||||
Authifier,
|
||||
},
|
||||
EmptyResponse, Error, Result,
|
||||
};
|
||||
|
||||
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(
|
||||
authifier: &State<Authifier>,
|
||||
mut session: Session,
|
||||
data: Json<WebPushSubscription>,
|
||||
) -> Result<EmptyResponse> {
|
||||
session.subscription = Some(data.into_inner());
|
||||
session
|
||||
.save(authifier)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "save",
|
||||
with: "session",
|
||||
})
|
||||
}
|
||||
23
crates/delta/src/routes/push/unsubscribe.rs
Normal file
23
crates/delta/src/routes/push/unsubscribe.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use revolt_quark::{
|
||||
authifier::{models::Session, Authifier},
|
||||
EmptyResponse, Error, Result,
|
||||
};
|
||||
|
||||
use rocket::State;
|
||||
|
||||
/// # Unsubscribe
|
||||
///
|
||||
/// Remove the Web Push subscription associated with the current session.
|
||||
#[openapi(tag = "Web Push")]
|
||||
#[post("/unsubscribe")]
|
||||
pub async fn req(authifier: &State<Authifier>, mut session: Session) -> Result<EmptyResponse> {
|
||||
session.subscription = None;
|
||||
session
|
||||
.save(authifier)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "save",
|
||||
with: "session",
|
||||
})
|
||||
}
|
||||
146
crates/delta/src/routes/root.rs
Normal file
146
crates/delta/src/routes/root.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
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 rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
/// # 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,
|
||||
}
|
||||
|
||||
/// # Build Information
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct BuildInformation {
|
||||
/// Commit Hash
|
||||
pub commit_sha: String,
|
||||
/// Commit Timestamp
|
||||
pub commit_timestamp: String,
|
||||
/// Git Semver
|
||||
pub semver: String,
|
||||
/// Git Origin URL
|
||||
pub origin_url: String,
|
||||
/// Build Timestamp
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
/// # 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,
|
||||
/// Build information
|
||||
pub build: BuildInformation,
|
||||
}
|
||||
|
||||
/// # Query Node
|
||||
///
|
||||
/// Fetch the server configuration for this Revolt instance.
|
||||
#[openapi(tag = "Core")]
|
||||
#[get("/")]
|
||||
pub async fn root() -> Result<Json<RevoltConfig>> {
|
||||
Ok(Json(RevoltConfig {
|
||||
revolt: env!("CARGO_PKG_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(),
|
||||
},
|
||||
},
|
||||
ws: EXTERNAL_WS_URL.to_string(),
|
||||
app: APP_URL.to_string(),
|
||||
vapid: VAPID_PUBLIC_KEY.to_string(),
|
||||
build: BuildInformation {
|
||||
commit_sha: option_env!("VERGEN_GIT_SHA")
|
||||
.unwrap_or_else(|| "<failed to generate>")
|
||||
.to_string(),
|
||||
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
|
||||
.unwrap_or_else(|| "<failed to generate>")
|
||||
.to_string(),
|
||||
semver: option_env!("VERGEN_GIT_SEMVER")
|
||||
.unwrap_or_else(|| "<failed to generate>")
|
||||
.to_string(),
|
||||
origin_url: option_env!("GIT_ORIGIN_URL")
|
||||
.unwrap_or_else(|| "<failed to generate>")
|
||||
.to_string(),
|
||||
timestamp: option_env!("VERGEN_BUILD_TIMESTAMP")
|
||||
.unwrap_or_else(|| "<failed to generate>")
|
||||
.to_string(),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
/// Example endpoint.
|
||||
#[openapi(skip)]
|
||||
#[get("/ping")]
|
||||
pub async fn ping(/*_limitguard: Ratelimiter*/) -> Status {
|
||||
Status::Ok
|
||||
}
|
||||
73
crates/delta/src/routes/servers/ban_create.rs
Normal file
73
crates/delta/src/routes/servers/ban_create.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use revolt_quark::{
|
||||
models::{server_member::MemberCompositeKey, 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?;
|
||||
|
||||
// If member exists, check privileges against them
|
||||
if let Ok(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)
|
||||
} else {
|
||||
let server_id = server.id.to_string();
|
||||
server
|
||||
.ban_user(
|
||||
db,
|
||||
MemberCompositeKey {
|
||||
server: server_id,
|
||||
user: target.id,
|
||||
},
|
||||
data.reason,
|
||||
)
|
||||
.await
|
||||
.map(Json)
|
||||
}
|
||||
}
|
||||
67
crates/delta/src/routes/servers/ban_list.rs
Normal file
67
crates/delta/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 }))
|
||||
}
|
||||
17
crates/delta/src/routes/servers/ban_remove.rs
Normal file
17
crates/delta/src/routes/servers/ban_remove.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
|
||||
|
||||
/// # Unban user
|
||||
///
|
||||
/// Remove a user's ban.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[delete("/<server>/bans/<target>")]
|
||||
pub async fn req(db: &Db, user: User, server: Ref, target: Ref) -> Result<EmptyResponse> {
|
||||
let server = server.as_server(db).await?;
|
||||
perms(&user)
|
||||
.server(&server)
|
||||
.throw_permission(db, Permission::BanMembers)
|
||||
.await?;
|
||||
|
||||
let ban = target.as_ban(db, &server.id).await?;
|
||||
db.delete_ban(&ban.id).await.map(|_| EmptyResponse)
|
||||
}
|
||||
120
crates/delta/src/routes/servers/channel_create.rs
Normal file
120
crates/delta/src/routes/servers/channel_create.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{server::PartialServer, Channel, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
/// # Channel Type
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
enum ChannelType {
|
||||
/// Text Channel
|
||||
Text,
|
||||
/// Voice Channel
|
||||
Voice,
|
||||
}
|
||||
|
||||
impl Default for ChannelType {
|
||||
fn default() -> Self {
|
||||
ChannelType::Text
|
||||
}
|
||||
}
|
||||
|
||||
/// # Channel Data
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataCreateChannel {
|
||||
/// Channel type
|
||||
#[serde(rename = "type", default = "ChannelType::default")]
|
||||
channel_type: ChannelType,
|
||||
/// Channel name
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
name: String,
|
||||
/// Channel description
|
||||
#[validate(length(min = 0, max = 1024))]
|
||||
description: Option<String>,
|
||||
/// Whether this channel is age restricted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Create Channel
|
||||
///
|
||||
/// Create a new Text or Voice channel.
|
||||
#[openapi(tag = "Server Information")]
|
||||
#[post("/<target>/channels", data = "<info>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
info: Json<DataCreateChannel>,
|
||||
) -> Result<Json<Channel>> {
|
||||
let info = info.into_inner();
|
||||
info.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let mut server = target.as_server(db).await?;
|
||||
perms(&user)
|
||||
.server(&server)
|
||||
.throw_permission(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
let mut channels = server.channels.clone();
|
||||
channels.push(id.clone());
|
||||
|
||||
let DataCreateChannel {
|
||||
name,
|
||||
description,
|
||||
nsfw,
|
||||
channel_type,
|
||||
} = info;
|
||||
let channel = match channel_type {
|
||||
ChannelType::Text => Channel::TextChannel {
|
||||
id,
|
||||
server: server.id.clone(),
|
||||
|
||||
name,
|
||||
description,
|
||||
|
||||
icon: None,
|
||||
last_message_id: None,
|
||||
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
|
||||
nsfw: nsfw.unwrap_or(false),
|
||||
},
|
||||
ChannelType::Voice => Channel::VoiceChannel {
|
||||
id,
|
||||
server: server.id.clone(),
|
||||
|
||||
name,
|
||||
description,
|
||||
icon: None,
|
||||
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
|
||||
nsfw: nsfw.unwrap_or(false),
|
||||
},
|
||||
};
|
||||
|
||||
channel.create(db).await?;
|
||||
server
|
||||
.update(
|
||||
db,
|
||||
PartialServer {
|
||||
channels: Some(channels),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(channel))
|
||||
}
|
||||
17
crates/delta/src/routes/servers/emoji_list.rs
Normal file
17
crates/delta/src/routes/servers/emoji_list.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use revolt_quark::models::{Emoji, User};
|
||||
use revolt_quark::{perms, Db, Ref, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Fetch Server Emoji
|
||||
///
|
||||
/// Fetch all emoji on a server.
|
||||
#[openapi(tag = "Server Customisation")]
|
||||
#[get("/<target>/emojis")]
|
||||
pub async fn list_emoji(db: &Db, user: User, target: Ref) -> Result<Json<Vec<Emoji>>> {
|
||||
let server = target.as_server(db).await?;
|
||||
perms(&user).server(&server).calc(db).await?;
|
||||
|
||||
// Fetch all emoji from server if we can view it
|
||||
db.fetch_emoji_by_parent_id(&server.id).await.map(Json)
|
||||
}
|
||||
25
crates/delta/src/routes/servers/invites_fetch.rs
Normal file
25
crates/delta/src/routes/servers/invites_fetch.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use revolt_quark::{
|
||||
models::{Invite, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Fetch Invites
|
||||
///
|
||||
/// Fetch all server invites.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[get("/<target>/invites")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<Invite>>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
}
|
||||
|
||||
let server = target.as_server(db).await?;
|
||||
perms(&user)
|
||||
.server(&server)
|
||||
.throw_permission(db, Permission::ManageServer)
|
||||
.await?;
|
||||
|
||||
db.fetch_invites_for_server(&server.id).await.map(Json)
|
||||
}
|
||||
171
crates/delta/src/routes/servers/member_edit.rs
Normal file
171
crates/delta/src/routes/servers/member_edit.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
server_member::{FieldsMember, PartialMember},
|
||||
File, Member, User,
|
||||
},
|
||||
perms, Db, Error, Permission, Ref, Result, Timestamp,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Member Data
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataMemberEdit {
|
||||
/// Member nickname
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
nickname: Option<String>,
|
||||
/// Attachment Id to set for avatar
|
||||
avatar: Option<String>,
|
||||
/// Array of role ids
|
||||
roles: Option<Vec<String>>,
|
||||
/// Timestamp this member is timed out until
|
||||
timeout: Option<Timestamp>,
|
||||
/// Fields to remove from channel object
|
||||
#[validate(length(min = 1))]
|
||||
remove: Option<Vec<FieldsMember>>,
|
||||
}
|
||||
|
||||
/// # Edit Member
|
||||
///
|
||||
/// Edit a member by their id.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[patch("/<server>/members/<target>", data = "<data>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
server: Ref,
|
||||
target: Ref,
|
||||
data: Json<DataMemberEdit>,
|
||||
) -> Result<Json<Member>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
// Fetch server, target member and current permissions
|
||||
let mut server = server.as_server(db).await?;
|
||||
let mut member = target.as_member(db, &server.id).await?;
|
||||
let mut permissions = perms(&user).server(&server);
|
||||
|
||||
// Check permissions in server
|
||||
let mut required = vec![];
|
||||
|
||||
if data.nickname.is_some()
|
||||
|| data
|
||||
.remove
|
||||
.as_ref()
|
||||
.map(|x| x.contains(&FieldsMember::Nickname))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if user.id == member.id.user {
|
||||
required.push(Permission::ChangeNickname);
|
||||
} else {
|
||||
required.push(Permission::ManageNicknames);
|
||||
}
|
||||
}
|
||||
|
||||
if data.avatar.is_some()
|
||||
|| data
|
||||
.remove
|
||||
.as_ref()
|
||||
.map(|x| x.contains(&FieldsMember::Avatar))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if user.id == member.id.user {
|
||||
required.push(Permission::ChangeAvatar);
|
||||
} else {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
}
|
||||
|
||||
if data.roles.is_some()
|
||||
|| data
|
||||
.remove
|
||||
.as_ref()
|
||||
.map(|x| x.contains(&FieldsMember::Roles))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
required.push(Permission::AssignRoles);
|
||||
}
|
||||
|
||||
if data.timeout.is_some()
|
||||
|| data
|
||||
.remove
|
||||
.as_ref()
|
||||
.map(|x| x.contains(&FieldsMember::Timeout))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
required.push(Permission::TimeoutMembers);
|
||||
}
|
||||
|
||||
for permission in required {
|
||||
permissions.throw_permission(db, permission).await?;
|
||||
}
|
||||
|
||||
// Resolve our ranking
|
||||
let our_ranking = permissions.get_member_rank().unwrap_or(i64::MIN);
|
||||
|
||||
// Check that we have permissions to act against this member
|
||||
if member.id.user != user.id
|
||||
&& member.get_ranking(permissions.server.get().unwrap()) <= our_ranking
|
||||
{
|
||||
return Err(Error::NotElevated);
|
||||
}
|
||||
|
||||
// Check permissions against roles in diff
|
||||
if let Some(roles) = &data.roles {
|
||||
let current_roles = member.roles.iter().collect::<HashSet<&String>>();
|
||||
|
||||
let new_roles = roles.iter().collect::<HashSet<&String>>();
|
||||
let added_roles: Vec<&&String> = new_roles.difference(¤t_roles).collect();
|
||||
|
||||
for role_id in added_roles {
|
||||
if let Some(role) = server.roles.remove(*role_id) {
|
||||
if role.rank <= our_ranking {
|
||||
return Err(Error::NotElevated);
|
||||
}
|
||||
} else {
|
||||
return Err(Error::InvalidRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply edits to the member object
|
||||
let DataMemberEdit {
|
||||
nickname,
|
||||
avatar,
|
||||
roles,
|
||||
timeout,
|
||||
remove,
|
||||
} = data;
|
||||
|
||||
let mut partial = PartialMember {
|
||||
nickname,
|
||||
roles,
|
||||
timeout,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// 1. Remove fields from object
|
||||
if let Some(fields) = &remove {
|
||||
if fields.contains(&FieldsMember::Avatar) {
|
||||
if let Some(avatar) = &member.avatar {
|
||||
db.mark_attachment_as_deleted(&avatar.id).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Apply new avatar
|
||||
if let Some(avatar) = avatar {
|
||||
partial.avatar = Some(File::use_avatar(db, &avatar, &user.id).await?);
|
||||
}
|
||||
|
||||
member
|
||||
.update(db, partial, remove.unwrap_or_default())
|
||||
.await?;
|
||||
|
||||
Ok(Json(member))
|
||||
}
|
||||
17
crates/delta/src/routes/servers/member_fetch.rs
Normal file
17
crates/delta/src/routes/servers/member_fetch.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use revolt_quark::{
|
||||
models::{Member, User},
|
||||
perms, Db, Ref, Result,
|
||||
};
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Fetch Member
|
||||
///
|
||||
/// Retrieve a member.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[get("/<target>/members/<member>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<Json<Member>> {
|
||||
let server = target.as_server(db).await?;
|
||||
perms(&user).server(&server).calc(db).await?;
|
||||
|
||||
member.as_member(db, &server.id).await.map(Json)
|
||||
}
|
||||
62
crates/delta/src/routes/servers/member_fetch_all.rs
Normal file
62
crates/delta/src/routes/servers/member_fetch_all.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use revolt_quark::{
|
||||
models::{Member, User},
|
||||
perms, Db, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Deserialize, JsonSchema, FromForm)]
|
||||
pub struct OptionsFetchAllMembers {
|
||||
/// Whether to exclude offline users
|
||||
exclude_offline: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Member List
|
||||
///
|
||||
/// Both lists are sorted by ID.
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
pub struct AllMemberResponse {
|
||||
/// List of members
|
||||
members: Vec<Member>,
|
||||
/// List of users
|
||||
users: Vec<User>,
|
||||
}
|
||||
|
||||
/// # Fetch Members
|
||||
///
|
||||
/// Fetch all server members.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[get("/<target>/members?<options..>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: OptionsFetchAllMembers,
|
||||
) -> Result<Json<AllMemberResponse>> {
|
||||
let server = target.as_server(db).await?;
|
||||
perms(&user).server(&server).calc(db).await?;
|
||||
|
||||
let mut members = db.fetch_all_members(&server.id).await?;
|
||||
|
||||
let mut user_ids = vec![];
|
||||
for member in &members {
|
||||
user_ids.push(member.id.user.clone());
|
||||
}
|
||||
|
||||
let mut users = User::fetch_foreign_users(db, &user_ids).await?;
|
||||
|
||||
// Ensure the lists match up exactly.
|
||||
members.sort_by(|a, b| a.id.user.cmp(&b.id.user));
|
||||
users.sort_by(|a, b| a.id.cmp(&b.id));
|
||||
|
||||
// Optionally, remove all offline user entries.
|
||||
if let Some(true) = options.exclude_offline {
|
||||
let mut iter = users.iter();
|
||||
members.retain(|_| iter.next().unwrap().online.unwrap_or(false));
|
||||
users.retain(|user| user.online.unwrap_or(false));
|
||||
}
|
||||
|
||||
Ok(Json(AllMemberResponse { members, users }))
|
||||
}
|
||||
40
crates/delta/src/routes/servers/member_remove.rs
Normal file
40
crates/delta/src/routes/servers/member_remove.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use revolt_quark::{
|
||||
models::{server_member::RemovalIntention, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Kick Member
|
||||
///
|
||||
/// Removes a member from the server.
|
||||
#[openapi(tag = "Server Members")]
|
||||
#[delete("/<target>/members/<member>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
|
||||
let server = target.as_server(db).await?;
|
||||
|
||||
if member.id == user.id {
|
||||
return Err(Error::CannotRemoveYourself);
|
||||
}
|
||||
|
||||
if member.id == server.owner {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
|
||||
let mut permissions = perms(&user).server(&server);
|
||||
|
||||
permissions
|
||||
.throw_permission(db, Permission::KickMembers)
|
||||
.await?;
|
||||
|
||||
let member = member.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
|
||||
.remove_member(db, member, RemovalIntention::Kick, false)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
@@ -1,29 +1,34 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
mod ban_create;
|
||||
mod ban_list;
|
||||
mod ban_remove;
|
||||
mod channel_create;
|
||||
mod emoji_list;
|
||||
mod invites_fetch;
|
||||
mod member_edit;
|
||||
mod member_fetch;
|
||||
mod member_fetch_all;
|
||||
mod member_remove;
|
||||
mod permissions_set;
|
||||
mod permissions_set_default;
|
||||
mod roles_create;
|
||||
mod roles_delete;
|
||||
mod roles_edit;
|
||||
mod server_ack;
|
||||
mod server_create;
|
||||
mod server_delete;
|
||||
mod server_edit;
|
||||
mod server_fetch;
|
||||
|
||||
mod channel_create;
|
||||
|
||||
mod member_edit;
|
||||
mod member_fetch;
|
||||
mod member_fetch_all;
|
||||
mod member_remove;
|
||||
|
||||
mod ban_create;
|
||||
mod ban_list;
|
||||
mod ban_remove;
|
||||
|
||||
mod invites_fetch;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
server_create::req,
|
||||
server_delete::req,
|
||||
server_fetch::req,
|
||||
server_edit::req,
|
||||
server_ack::req,
|
||||
channel_create::req,
|
||||
member_fetch_all::req,
|
||||
member_remove::req,
|
||||
@@ -32,6 +37,12 @@ pub fn routes() -> Vec<Route> {
|
||||
ban_create::req,
|
||||
ban_remove::req,
|
||||
ban_list::req,
|
||||
invites_fetch::req
|
||||
invites_fetch::req,
|
||||
roles_create::req,
|
||||
roles_edit::req,
|
||||
roles_delete::req,
|
||||
permissions_set::req,
|
||||
permissions_set_default::req,
|
||||
emoji_list::list_emoji
|
||||
]
|
||||
}
|
||||
56
crates/delta/src/routes/servers/permissions_set.rs
Normal file
56
crates/delta/src/routes/servers/permissions_set.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Deserialize;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{Server, User},
|
||||
perms, Db, Error, Override, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Permission Value
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct DataSetServerRolePermission {
|
||||
/// Allow / deny values for the role in this server.
|
||||
permissions: Override,
|
||||
}
|
||||
|
||||
/// # Set Role Permission
|
||||
///
|
||||
/// Sets permissions for the specified role in the server.
|
||||
#[openapi(tag = "Server 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<DataSetServerRolePermission>,
|
||||
) -> Result<Json<Server>> {
|
||||
let data = data.into_inner();
|
||||
|
||||
let mut server = target.as_server(db).await?;
|
||||
if let Some((current_value, rank)) = server.roles.get(&role_id).map(|x| (x.permissions, x.rank))
|
||||
{
|
||||
let mut permissions = perms(&user).server(&server);
|
||||
|
||||
permissions
|
||||
.throw_permission(db, Permission::ManagePermissions)
|
||||
.await?;
|
||||
|
||||
if rank <= permissions.get_member_rank().unwrap_or(i64::MIN) {
|
||||
return Err(Error::NotElevated);
|
||||
}
|
||||
|
||||
let current_value: Override = current_value.into();
|
||||
permissions
|
||||
.throw_permission_override(db, current_value, data.permissions)
|
||||
.await?;
|
||||
|
||||
server
|
||||
.set_role_permission(db, &role_id, data.permissions.into())
|
||||
.await?;
|
||||
|
||||
Ok(Json(server))
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user