forked from jmug/stoatchat
Compare commits
221 Commits
0.5.3-alph
...
20220718-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,3 +1,4 @@
|
||||
Dockerfile
|
||||
target
|
||||
.mongo
|
||||
.env
|
||||
79
.env
Normal file
79
.env
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
|
||||
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
|
||||
141
.github/workflows/docker.yml
vendored
141
.github/workflows/docker.yml
vendored
@@ -1,141 +0,0 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- ".gitignore"
|
||||
- ".gitlab-ci.yml"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [linux/amd64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.architecture }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
|
||||
publish_amd64:
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache amd64 Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache/linux/amd64
|
||||
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: revoltchat/server, ghcr.io/revoltchat/server
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and publish
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
||||
publish_arm64:
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request' && startsWith(github.ref, 'refs/tags')
|
||||
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: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: revoltchat/server, ghcr.io/revoltchat/server
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and publish
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
49
.github/workflows/triage_issue.yml
vendored
Normal file
49
.github/workflows/triage_issue.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Add Issue to Board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
72
.github/workflows/triage_pr.yml
vendored
Normal file
72
.github/workflows/triage_pr.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation (
|
||||
$project: ID!
|
||||
$item: ID!
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectNextItemField(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: $status_value
|
||||
}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,7 +1,3 @@
|
||||
Rocket.toml
|
||||
/target
|
||||
/target_backup
|
||||
**/*.rs.bk
|
||||
.mongo
|
||||
.env
|
||||
avatar.png
|
||||
target
|
||||
.data
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
2708
Cargo.lock
generated
2708
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
67
Cargo.toml
67
Cargo.toml
@@ -1,65 +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]
|
||||
# Utility
|
||||
lru = "0.7.0"
|
||||
url = "2.2.2"
|
||||
log = "0.4.11"
|
||||
dotenv = "0.15.0"
|
||||
linkify = "0.6.0"
|
||||
once_cell = "1.4.1"
|
||||
env_logger = "0.7.1"
|
||||
lazy_static = "1.4.0"
|
||||
ctrlc = { version = "3.0", features = ["termination"] }
|
||||
|
||||
# Lang. Utilities
|
||||
regex = "1"
|
||||
num_enum = "0.5.1"
|
||||
impl_ops = "0.1.1"
|
||||
bitfield = "0.13.2"
|
||||
phf = { version = "0.9.0", features = ["macros"] }
|
||||
|
||||
# ID / key generation
|
||||
ulid = "0.4.1"
|
||||
nanoid = "0.4.0"
|
||||
base64 = "0.13.0"
|
||||
|
||||
# serde
|
||||
serde_json = "1.0.57"
|
||||
serde = { version = "1.0.115", features = ["derive"] }
|
||||
validator = { version = "0.11", features = ["derive"] }
|
||||
rmp-serde = { git = "https://github.com/3Hren/msgpack-rust", rev = "5bf2c24203ad422233cf35b7b7bfad9f7e811814" }
|
||||
|
||||
# async
|
||||
futures = "0.3.8"
|
||||
chrono = "0.4.15"
|
||||
async-channel = "1.6.1"
|
||||
reqwest = { version = "0.11.4", features = ["json"] }
|
||||
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
|
||||
|
||||
# internal util
|
||||
web-push = "0.7.2"
|
||||
many-to-many = "0.1.2"
|
||||
lettre = "0.10.0-alpha.4"
|
||||
rauth = { git = "https://github.com/insertish/rauth", rev = "775a6f3ffb9ffc0e382af32fac2924e9a707e890" }
|
||||
hive_pubsub = { git = "https://gitlab.insrt.uk/insert/hive", rev = "b0f3db9d33990530d7640d4bbb309c8d9eb5c0cf", features = ["redis-backend"] }
|
||||
|
||||
# redis
|
||||
redis = { version = "0.21.2", features = ["async-std-comp"] }
|
||||
mobc = { version = "0.7.3" }
|
||||
mobc-redis = { version = "0.7.0", default-features = false, features = ["async-std-comp"] }
|
||||
|
||||
# web
|
||||
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
|
||||
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] }
|
||||
mongodb = { version = "1.2.2", features = ["tokio-runtime"], default-features = false }
|
||||
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
|
||||
25
Dockerfile
25
Dockerfile
@@ -3,23 +3,10 @@ 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 apt-get update && apt-get install -y libssl-dev pkg-config && cargo build --release --bin dummy
|
||||
|
||||
COPY assets/templates ./assets/templates
|
||||
COPY src ./src
|
||||
RUN cargo install --locked --path .
|
||||
|
||||
# Bundle Stage
|
||||
FROM debian:buster-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
COPY --from=builder /usr/local/cargo/bin/revolt ./
|
||||
COPY assets ./assets
|
||||
EXPOSE 8000
|
||||
EXPOSE 9000
|
||||
ENV ROCKET_ADDRESS 0.0.0.0
|
||||
ENV ROCKET_PORT 8000
|
||||
CMD ["./revolt"]
|
||||
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/>.
|
||||
|
||||
38
README.md
38
README.md
@@ -1,21 +1,17 @@
|
||||
# Delta
|
||||
# Revolt Backend
|
||||
|
||||
## Description
|
||||
This is a monorepo for the Revolt backend.
|
||||
|
||||
Delta is a blazing fast API server built with Rust for Revolt.
|
||||
| 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 |-->
|
||||
|
||||
**Features:**
|
||||
|
||||
- Robust and efficient API routes for running a chat platform.
|
||||
- Distributed notification system, allowing any node to be seamlessly connected.
|
||||
- Simple deployment, based mostly on pure Rust code and libraries.
|
||||
- Hooks up to a MongoDB deployment, provide URI and no extra work needed.
|
||||
|
||||
## Stack
|
||||
|
||||
- [Rocket](https://rocket.rs/) (REST)
|
||||
- [Async Tungstenite](https://github.com/sdroege/async-tungstenite) (WebSockets)
|
||||
- [MongoDB](https://mongodb.com/)
|
||||
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
||||
|
||||
## Resources
|
||||
|
||||
@@ -25,16 +21,6 @@ Delta is a blazing fast API server built with Rust for Revolt.
|
||||
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
|
||||
- [Contribution Guide](https://developers.revolt.chat/contributing)
|
||||
|
||||
## CLI Commands
|
||||
|
||||
| Command | Description |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------- |
|
||||
| `./publish.sh` | Publish a Docker Image. |
|
||||
| `./set_version.sh` | Update the version. **Not intended for PR use.** |
|
||||
| `cargo build` | Build/compile Delta. |
|
||||
| `cargo run` | Run Delta. |
|
||||
| `cargo fmt` | Format Delta. Not intended for PR use to avoid accidentally formatting unformatted files. |
|
||||
|
||||
## Contributing
|
||||
|
||||
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
|
||||
@@ -42,4 +28,4 @@ Please note that a pull request should only take care of one issue so that we ca
|
||||
|
||||
## License
|
||||
|
||||
Delta is licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/delta/blob/master/LICENSE).
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). Please check individual crates for further license information.
|
||||
|
||||
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 |
7
build.sh
Executable file
7
build.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# Build base image
|
||||
docker build -t revolt.chat/base:latest -f Dockerfile .
|
||||
|
||||
# Build crates
|
||||
docker build -t revolt.chat/delta:latest -f crates/delta/Dockerfile .
|
||||
docker build -t revolt.chat/bonfire:latest -f crates/bonfire/Dockerfile .
|
||||
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.5"
|
||||
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:?}");
|
||||
});
|
||||
}
|
||||
63
crates/delta/Cargo.toml
Normal file
63
crates/delta/Cargo.toml
Normal file
@@ -0,0 +1,63 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.5.5"
|
||||
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 = { git = "https://github.com/insertish/rocket_empty", branch = "master" }
|
||||
rocket_rauth = { git = "https://github.com/insertish/rauth", rev = "9c3f9bb58a549a51546bd748274130ac4a667cb4" }
|
||||
|
||||
# spec generation
|
||||
schemars = "0.8.8"
|
||||
# rocket_okapi = "0.8.0-rc.1"
|
||||
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "a1048d0c8cd771e424ec97d33d825c32e06aa120", features = [ "swagger" ] }
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
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
|
||||
51
crates/delta/src/main.rs
Normal file
51
crates/delta/src/main.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate rocket_okapi;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
pub mod routes;
|
||||
pub mod util;
|
||||
|
||||
use revolt_quark::rauth::RAuth;
|
||||
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 rAuth
|
||||
let rauth = RAuth {
|
||||
database: db.clone().into(),
|
||||
config: revolt_quark::util::rauth::config(),
|
||||
};
|
||||
|
||||
// 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(rauth)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod create;
|
||||
mod invite;
|
||||
mod fetch_public;
|
||||
mod delete;
|
||||
mod edit;
|
||||
mod fetch;
|
||||
mod fetch_owned;
|
||||
mod edit;
|
||||
mod delete;
|
||||
mod fetch_public;
|
||||
mod invite;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
create::create_bot,
|
||||
invite::invite_bot,
|
||||
fetch_public::fetch_public_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)
|
||||
}
|
||||
41
crates/delta/src/routes/channels/channel_delete.rs
Normal file
41
crates/delta/src/routes/channels/channel_delete.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use revolt_quark::{
|
||||
models::{channel::PartialChannel, Channel, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Close Channel
|
||||
///
|
||||
/// Deletes a server channel, leaves a group or closes a group.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[delete("/<target>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
let mut perms = perms(&user).channel(&channel);
|
||||
perms.throw_permission(db, Permission::ViewChannel).await?;
|
||||
|
||||
match &channel {
|
||||
Channel::SavedMessages { .. } => Err(Error::NoEffect),
|
||||
Channel::DirectMessage { .. } => channel
|
||||
.update(
|
||||
db,
|
||||
PartialChannel {
|
||||
active: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::Group { .. } => channel
|
||||
.remove_user_from_group(db, &user.id, None)
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
|
||||
perms
|
||||
.throw_permission(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
|
||||
channel.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
207
crates/delta/src/routes/channels/channel_edit.rs
Normal file
207
crates/delta/src/routes/channels/channel_edit.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
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>,
|
||||
#[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
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,57 @@
|
||||
use crate::database::*;
|
||||
use crate::util::idempotency::IdempotencyKey;
|
||||
use crate::util::result::{Error, Result};
|
||||
use crate::util::variables::MAX_GROUP_SIZE;
|
||||
use std::{collections::HashSet, iter::FromIterator};
|
||||
|
||||
use mongodb::bson::doc;
|
||||
use rocket::serde::json::{Json, Value};
|
||||
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 std::collections::HashSet;
|
||||
use std::iter::FromIterator;
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
#[derive(Validate, Serialize, Deserialize)]
|
||||
pub struct Data {
|
||||
/// # 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>
|
||||
nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
/// # Create Group
|
||||
///
|
||||
/// Create a new group channel.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[post("/create", data = "<info>")]
|
||||
pub async fn req(_idempotency: IdempotencyKey, user: User, info: Json<Data>) -> Result<Value> {
|
||||
pub async fn req(db: &Db, user: User, info: Json<DataCreateGroup>) -> Result<Json<Channel>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot)
|
||||
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.iter().cloned());
|
||||
let mut set: HashSet<String> = HashSet::from_iter(info.users.into_iter());
|
||||
set.insert(user.id.clone());
|
||||
|
||||
if set.len() > *MAX_GROUP_SIZE {
|
||||
Err(Error::GroupTooLarge {
|
||||
return Err(Error::GroupTooLarge {
|
||||
max: *MAX_GROUP_SIZE,
|
||||
})?
|
||||
});
|
||||
}
|
||||
|
||||
for target in &set {
|
||||
@@ -50,20 +63,22 @@ pub async fn req(_idempotency: IdempotencyKey, user: User, info: Json<Data>) ->
|
||||
}
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
let channel = Channel::Group {
|
||||
id,
|
||||
let group = Channel::Group {
|
||||
id: Ulid::new().to_string(),
|
||||
|
||||
name: info.name,
|
||||
description: info.description,
|
||||
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_default()
|
||||
|
||||
nsfw: info.nsfw.unwrap_or(false),
|
||||
};
|
||||
|
||||
channel.clone().publish().await?;
|
||||
|
||||
Ok(json!(channel))
|
||||
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))
|
||||
.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)
|
||||
}
|
||||
211
crates/delta/src/routes/channels/message_send.rs
Normal file
211
crates/delta/src/routes/channels/message_send.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
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 })?;
|
||||
|
||||
Message::validate_sum(&data.content, &data.embeds)?;
|
||||
|
||||
idempotency.consume_nonce(data.nonce).await?;
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
||||
.await?;
|
||||
|
||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
||||
{
|
||||
return Err(Error::EmptyMessage);
|
||||
}
|
||||
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: channel.id().to_string(),
|
||||
author: user.id.clone(),
|
||||
masquerade: data.masquerade,
|
||||
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).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)
|
||||
}
|
||||
@@ -1,27 +1,32 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod channel_ack;
|
||||
mod channel_delete;
|
||||
mod channel_edit;
|
||||
mod channel_fetch;
|
||||
mod members_fetch;
|
||||
mod group_add_member;
|
||||
mod group_create;
|
||||
mod group_remove_member;
|
||||
mod invite_create;
|
||||
mod voice_join;
|
||||
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_search;
|
||||
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> {
|
||||
routes![
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
channel_ack::req,
|
||||
channel_fetch::req,
|
||||
members_fetch::req,
|
||||
@@ -34,6 +39,7 @@ pub fn routes() -> Vec<Route> {
|
||||
message_query_stale::req,
|
||||
message_fetch::req,
|
||||
message_edit::req,
|
||||
message_bulk_delete::req,
|
||||
message_delete::req,
|
||||
group_create::req,
|
||||
group_add_member::req,
|
||||
@@ -41,5 +47,8 @@ pub fn routes() -> Vec<Route> {
|
||||
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::serde::json::Value;
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
.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<Value> {
|
||||
.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() > 100 {
|
||||
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 rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
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)
|
||||
}
|
||||
131
crates/delta/src/routes/invites/invite_fetch.rs
Normal file
131
crates/delta/src/routes/invites/invite_fetch.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use revolt_quark::models::{Channel, File, Invite};
|
||||
use revolt_quark::{Db, Ref, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Serialize;
|
||||
|
||||
/// # Invite
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Serialize, Debug, Clone, JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum InviteResponse {
|
||||
/// Server channel invite
|
||||
Server {
|
||||
/// Invite code
|
||||
code: String,
|
||||
/// Id of the server
|
||||
server_id: String,
|
||||
/// Name of the server
|
||||
server_name: String,
|
||||
/// Attachment for server icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_icon: Option<File>,
|
||||
/// Attachment for server banner
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_banner: Option<File>,
|
||||
/// Id of server channel
|
||||
channel_id: String,
|
||||
/// Name of server channel
|
||||
channel_name: String,
|
||||
/// Description of server channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_description: Option<String>,
|
||||
/// Name of user who created the invite
|
||||
user_name: String,
|
||||
/// Avatar of the user who created the invite
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_avatar: Option<File>,
|
||||
/// Number of members in this server
|
||||
member_count: i64,
|
||||
},
|
||||
/// Group channel invite
|
||||
Group {
|
||||
/// Invite code
|
||||
code: String,
|
||||
/// Id of group channel
|
||||
channel_id: String,
|
||||
/// Name of group channel
|
||||
channel_name: String,
|
||||
/// Description of group channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_description: Option<String>,
|
||||
/// Name of user who created the invite
|
||||
user_name: String,
|
||||
/// Avatar of the user who created the invite
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_avatar: Option<File>,
|
||||
},
|
||||
}
|
||||
|
||||
/// # Fetch Invite
|
||||
///
|
||||
/// Fetch an invite by its id.
|
||||
#[openapi(tag = "Invites")]
|
||||
#[get("/<target>")]
|
||||
pub async fn req(db: &Db, target: Ref) -> Result<Json<InviteResponse>> {
|
||||
Ok(Json(match target.as_invite(db).await? {
|
||||
Invite::Server {
|
||||
channel, creator, ..
|
||||
} => {
|
||||
let channel = db.fetch_channel(&channel).await?;
|
||||
let user = db.fetch_user(&creator).await?;
|
||||
|
||||
match channel {
|
||||
Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
..
|
||||
} => {
|
||||
let server = db.fetch_server(&server).await?;
|
||||
|
||||
InviteResponse::Server {
|
||||
code: target.id,
|
||||
member_count: db.fetch_member_count(&server.id).await? as i64,
|
||||
server_id: server.id,
|
||||
server_name: server.name,
|
||||
server_icon: server.icon,
|
||||
server_banner: server.banner,
|
||||
channel_id: id,
|
||||
channel_name: name,
|
||||
channel_description: description,
|
||||
user_name: user.username,
|
||||
user_avatar: user.avatar,
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
Invite::Group {
|
||||
channel, creator, ..
|
||||
} => {
|
||||
let channel = db.fetch_channel(&channel).await?;
|
||||
let user = db.fetch_user(&creator).await?;
|
||||
|
||||
match channel {
|
||||
Channel::Group {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
..
|
||||
} => InviteResponse::Group {
|
||||
code: target.id,
|
||||
channel_id: id,
|
||||
channel_name: name,
|
||||
channel_description: description,
|
||||
user_name: user.username,
|
||||
user_avatar: user.avatar,
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
47
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 rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod invite_delete;
|
||||
mod invite_fetch;
|
||||
mod invite_join;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![invite_fetch::req, invite_join::req, invite_delete::req]
|
||||
}
|
||||
283
crates/delta/src/routes/mod.rs
Normal file
283
crates/delta/src/routes/mod.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
pub use rocket::http::Status;
|
||||
pub use rocket::response::Redirect;
|
||||
use rocket::{Build, Rocket};
|
||||
use rocket_okapi::{okapi::openapi3::OpenApi, settings::OpenApiSettings};
|
||||
|
||||
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_rauth::routes::account::routes(),
|
||||
"/auth/session" => rocket_rauth::routes::session::routes(),
|
||||
"/auth/mfa" => rocket_rauth::routes::mfa::routes(),
|
||||
"/onboard" => onboard::routes(),
|
||||
"/push" => push::routes(),
|
||||
"/sync" => sync::routes(),
|
||||
};
|
||||
|
||||
rocket
|
||||
}
|
||||
|
||||
fn custom_openapi_spec() -> OpenApi {
|
||||
use rocket_okapi::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()
|
||||
}
|
||||
}
|
||||
43
crates/delta/src/routes/onboard/complete.rs
Normal file
43
crates/delta/src/routes/onboard/complete.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::util::regex::RE_USERNAME;
|
||||
use revolt_quark::{models::User, rauth::models::Session, 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::{models::User, rauth::models::Session};
|
||||
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 rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod complete;
|
||||
mod hello;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![hello::req, complete::req]
|
||||
}
|
||||
9
crates/delta/src/routes/push/mod.rs
Normal file
9
crates/delta/src/routes/push/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use rocket::Route;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod subscribe;
|
||||
mod unsubscribe;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![subscribe::req, unsubscribe::req]
|
||||
}
|
||||
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::{
|
||||
rauth::{
|
||||
models::{Session, WebPushSubscription},
|
||||
RAuth,
|
||||
},
|
||||
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(
|
||||
rauth: &State<RAuth>,
|
||||
mut session: Session,
|
||||
data: Json<WebPushSubscription>,
|
||||
) -> Result<EmptyResponse> {
|
||||
session.subscription = Some(data.into_inner());
|
||||
session
|
||||
.save(&rauth)
|
||||
.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::{
|
||||
rauth::{models::Session, RAuth},
|
||||
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(rauth: &State<RAuth>, mut session: Session) -> Result<EmptyResponse> {
|
||||
session.subscription = None;
|
||||
session
|
||||
.save(&rauth)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "save",
|
||||
with: "session",
|
||||
})
|
||||
}
|
||||
112
crates/delta/src/routes/root.rs
Normal file
112
crates/delta/src/routes/root.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
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,
|
||||
}
|
||||
|
||||
/// # Server Configuration
|
||||
#[derive(Serialize, JsonSchema, Debug)]
|
||||
pub struct RevoltConfig {
|
||||
/// Revolt API Version
|
||||
pub revolt: String,
|
||||
/// Features enabled on this Revolt node
|
||||
pub features: RevoltFeatures,
|
||||
/// WebSocket URL
|
||||
pub ws: String,
|
||||
/// URL pointing to the client serving this node
|
||||
pub app: String,
|
||||
/// Web Push VAPID public key
|
||||
pub vapid: String,
|
||||
}
|
||||
|
||||
/// # Query Node
|
||||
///
|
||||
/// Fetch the server configuration for this Revolt instance.
|
||||
#[openapi(tag = "Core")]
|
||||
#[get("/")]
|
||||
pub 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(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
161
crates/delta/src/routes/servers/member_edit.rs
Normal file
161
crates/delta/src/routes/servers/member_edit.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
73
crates/delta/src/routes/servers/member_fetch_all.rs
Normal file
73
crates/delta/src/routes/servers/member_fetch_all.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use revolt_quark::{
|
||||
models::{Member, User},
|
||||
perms,
|
||||
presence::presence_filter_online,
|
||||
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 online_ids = presence_filter_online(&user_ids).await;
|
||||
let mut users = db
|
||||
.fetch_users(&user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|mut user| {
|
||||
user.online = Some(online_ids.contains(&user.id));
|
||||
user.foreign()
|
||||
})
|
||||
.collect::<Vec<User>>();
|
||||
|
||||
// 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)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
@@ -1,32 +1,29 @@
|
||||
use rocket::Route;
|
||||
|
||||
mod server_create;
|
||||
mod server_delete;
|
||||
mod server_edit;
|
||||
mod server_fetch;
|
||||
mod server_ack;
|
||||
|
||||
mod channel_create;
|
||||
|
||||
mod member_edit;
|
||||
mod member_fetch;
|
||||
mod member_fetch_all;
|
||||
mod member_remove;
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
|
||||
mod ban_create;
|
||||
mod ban_list;
|
||||
mod ban_remove;
|
||||
|
||||
mod channel_create;
|
||||
mod emoji_list;
|
||||
mod invites_fetch;
|
||||
|
||||
mod roles_create;
|
||||
mod roles_edit;
|
||||
mod roles_delete;
|
||||
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;
|
||||
|
||||
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,
|
||||
@@ -45,6 +42,7 @@ pub fn routes() -> Vec<Route> {
|
||||
roles_edit::req,
|
||||
roles_delete::req,
|
||||
permissions_set::req,
|
||||
permissions_set_default::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)
|
||||
}
|
||||
}
|
||||
52
crates/delta/src/routes/servers/permissions_set_default.rs
Normal file
52
crates/delta/src/routes/servers/permissions_set_default.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use rocket::serde::json::Json;
|
||||
use serde::Deserialize;
|
||||
|
||||
use revolt_quark::{
|
||||
models::{server::PartialServer, Server, User},
|
||||
perms, Db, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
/// # Permission Value
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct DataSetServerDefaultPermission {
|
||||
/// Default member permission value
|
||||
permissions: u64,
|
||||
}
|
||||
|
||||
/// # Set Default Permission
|
||||
///
|
||||
/// Sets permissions for the default role in this server.
|
||||
#[openapi(tag = "Server Permissions")]
|
||||
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataSetServerDefaultPermission>,
|
||||
) -> Result<Json<Server>> {
|
||||
let data = data.into_inner();
|
||||
|
||||
let mut server = target.as_server(db).await?;
|
||||
let mut permissions = perms(&user).server(&server);
|
||||
|
||||
permissions
|
||||
.throw_permission(db, Permission::ManagePermissions)
|
||||
.await?;
|
||||
|
||||
permissions
|
||||
.throw_permission_value(db, data.permissions)
|
||||
.await?;
|
||||
|
||||
server
|
||||
.update(
|
||||
db,
|
||||
PartialServer {
|
||||
default_permissions: Some(data.permissions as i64),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Json(server))
|
||||
}
|
||||
74
crates/delta/src/routes/servers/roles_create.rs
Normal file
74
crates/delta/src/routes/servers/roles_create.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use revolt_quark::{
|
||||
models::{server::Role, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Role Data
|
||||
#[derive(Validate, Deserialize, JsonSchema)]
|
||||
pub struct DataCreateRole {
|
||||
/// Role name
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
name: String,
|
||||
/// Ranking position
|
||||
///
|
||||
/// Smaller values take priority.
|
||||
rank: Option<i64>,
|
||||
}
|
||||
|
||||
/// # New Role Response
|
||||
#[derive(Serialize, JsonSchema)]
|
||||
pub struct NewRoleResponse {
|
||||
/// Id of the role
|
||||
id: String,
|
||||
/// New role
|
||||
role: Role,
|
||||
}
|
||||
|
||||
/// # Create Role
|
||||
///
|
||||
/// Creates a new server role.
|
||||
#[openapi(tag = "Server Permissions")]
|
||||
#[post("/<target>/roles", data = "<data>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataCreateRole>,
|
||||
) -> Result<Json<NewRoleResponse>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
let server = target.as_server(db).await?;
|
||||
let mut permissions = perms(&user).server(&server);
|
||||
|
||||
permissions
|
||||
.throw_permission(db, Permission::ManageRole)
|
||||
.await?;
|
||||
|
||||
let member_rank = permissions.get_member_rank();
|
||||
let rank = if let Some(given_rank) = data.rank {
|
||||
if given_rank <= member_rank.unwrap_or(i64::MIN) {
|
||||
return Err(Error::NotElevated);
|
||||
}
|
||||
|
||||
given_rank
|
||||
} else {
|
||||
member_rank.unwrap_or(0).saturating_add(1)
|
||||
};
|
||||
|
||||
let role = Role {
|
||||
name: data.name,
|
||||
rank,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(Json(NewRoleResponse {
|
||||
id: role.create(db, &server.id).await?,
|
||||
role,
|
||||
}))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user