mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-06 03:06:04 +00:00
Compare commits
379 Commits
20230223-1
...
20240710-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78b9fa168b | ||
|
|
e290d168ac | ||
|
|
78cd89ec32 | ||
|
|
f9d9059e73 | ||
|
|
0954d71f51 | ||
|
|
9ea2bd9f2f | ||
|
|
d6bcb844db | ||
|
|
ce20e689cc | ||
|
|
d9cfc9d0e1 | ||
|
|
64a07d09f4 | ||
|
|
00e881799f | ||
|
|
fa598dd6f8 | ||
|
|
ac4e2cb10b | ||
|
|
ec578ab0ef | ||
|
|
ea6ba59841 | ||
|
|
f903f716e3 | ||
|
|
c5f4b94aa5 | ||
|
|
1d5dae4751 | ||
|
|
ac20b6bc99 | ||
|
|
6ec8007e4e | ||
|
|
e5eea267cf | ||
|
|
de5add09d0 | ||
|
|
1ec8f46c1d | ||
|
|
80666848cc | ||
|
|
e8e9613169 | ||
|
|
eda36436a8 | ||
|
|
93e05e9f18 | ||
|
|
090d8cba71 | ||
|
|
f16e72e329 | ||
|
|
8248a4a5b8 | ||
|
|
cf0b4274c8 | ||
|
|
36ecb48c7b | ||
|
|
ba411fef43 | ||
|
|
4055363cff | ||
|
|
d179d1e695 | ||
|
|
fc9ceb7540 | ||
|
|
a31a3e2e65 | ||
|
|
b12e728514 | ||
|
|
962c7d62c7 | ||
|
|
d677716f93 | ||
|
|
8099310f89 | ||
|
|
5c40f66010 | ||
|
|
8e7dd21bce | ||
|
|
4868205df4 | ||
|
|
96fb0eecca | ||
|
|
2cb20618da | ||
|
|
92e948aabc | ||
|
|
cb4435d700 | ||
|
|
4c6e78e1a5 | ||
|
|
da9a91e05f | ||
|
|
506968634e | ||
|
|
a5d0cdf0dd | ||
|
|
c59aad43c8 | ||
|
|
19e72babc9 | ||
|
|
bfcdb13d6d | ||
|
|
878c1a83ad | ||
|
|
471e0f55e4 | ||
|
|
08127218ce | ||
|
|
b69202c222 | ||
|
|
b3494b12fc | ||
|
|
7703475868 | ||
|
|
4be3bdc4c3 | ||
|
|
3e26e7e89d | ||
|
|
13b95d383e | ||
|
|
3c76005c33 | ||
|
|
3065e153d5 | ||
|
|
a1eeedabb9 | ||
|
|
0fc59b8e2a | ||
|
|
265d3ba8ef | ||
|
|
20d398d02c | ||
|
|
6bc74749d2 | ||
|
|
301676fb54 | ||
|
|
6b488f347e | ||
|
|
1a8e43d280 | ||
|
|
acb4190f70 | ||
|
|
9e42567050 | ||
|
|
78fb6e1982 | ||
|
|
9de963fb56 | ||
|
|
03340a2b5a | ||
|
|
b2d3344ddd | ||
|
|
49bb235938 | ||
|
|
4e7049d51e | ||
|
|
2de7598f8d | ||
|
|
f6a565385e | ||
|
|
569bd1d5e1 | ||
|
|
aca1fe6dff | ||
|
|
aded2e3239 | ||
|
|
54a4eff623 | ||
|
|
50c36dcefd | ||
|
|
f16c275bcb | ||
|
|
74ff072199 | ||
|
|
52431bd1dd | ||
|
|
fa67e87467 | ||
|
|
8f9b18444a | ||
|
|
7b18e983c9 | ||
|
|
c0e64c9052 | ||
|
|
0d54ccd342 | ||
|
|
d4d5b23c7d | ||
|
|
a641034895 | ||
|
|
9dd24526ef | ||
|
|
6d4f772d78 | ||
|
|
4c4dada3da | ||
|
|
cf82972a80 | ||
|
|
ea00f0fec1 | ||
|
|
226dbca6e0 | ||
|
|
e4b5a23f71 | ||
|
|
9663caa1ff | ||
|
|
f9f0f409a8 | ||
|
|
8f4d6c864b | ||
|
|
513461c99a | ||
|
|
41ef76ff3e | ||
|
|
c451e431cd | ||
|
|
09a848f5a6 | ||
|
|
308193377d | ||
|
|
3d3326e68a | ||
|
|
88c2232a6c | ||
|
|
25016efbc2 | ||
|
|
db63ac4064 | ||
|
|
1f593a1708 | ||
|
|
219c16a69c | ||
|
|
aed8f69f34 | ||
|
|
196a856c68 | ||
|
|
9bf60e87e9 | ||
|
|
b90e7a4412 | ||
|
|
cb813e91f0 | ||
|
|
a6bc9fcbb3 | ||
|
|
cd0737911b | ||
|
|
7705354d4d | ||
|
|
39ac8b2278 | ||
|
|
c5494aa1ca | ||
|
|
1a96bf5a4b | ||
|
|
37a1244751 | ||
|
|
bf39f18f8d | ||
|
|
95be7dc87a | ||
|
|
9be171c7b6 | ||
|
|
786fbb8ae4 | ||
|
|
6c0210e8a1 | ||
|
|
456bf7b42a | ||
|
|
21ffea0f9f | ||
|
|
96d90215d2 | ||
|
|
29dcf714f2 | ||
|
|
cb97004d3f | ||
|
|
0b6e5dd9db | ||
|
|
7132877201 | ||
|
|
4d42fb74e7 | ||
|
|
866ace3381 | ||
|
|
0798e81862 | ||
|
|
2fa5ac41ac | ||
|
|
25ae1555a2 | ||
|
|
881888bd1c | ||
|
|
41f20c2239 | ||
|
|
38f1381fea | ||
|
|
6838b0b1f0 | ||
|
|
055deab105 | ||
|
|
6d2073120d | ||
|
|
9789909061 | ||
|
|
895de86f1e | ||
|
|
98b8c5403b | ||
|
|
aa7c75c60e | ||
|
|
4270f0c5d4 | ||
|
|
eb1f45d208 | ||
|
|
9929f7499a | ||
|
|
279d9ef1b5 | ||
|
|
5a9bb9e68d | ||
|
|
a8fc9e928a | ||
|
|
da231ae390 | ||
|
|
7a31da4189 | ||
|
|
7d0b5392b5 | ||
|
|
1e1d44c0e4 | ||
|
|
1c4588967f | ||
|
|
853366a297 | ||
|
|
7568f49755 | ||
|
|
0542788567 | ||
|
|
73f7b8f007 | ||
|
|
c2412190a8 | ||
|
|
5372296dc0 | ||
|
|
41a47bdf8f | ||
|
|
51c26e324d | ||
|
|
3a55d00c6a | ||
|
|
004e84ece9 | ||
|
|
3d0914ce52 | ||
|
|
bde432cb75 | ||
|
|
28d225952a | ||
|
|
2fb9e7a802 | ||
|
|
431046cb4c | ||
|
|
92032cd9bc | ||
|
|
bfdd713d38 | ||
|
|
430df03b20 | ||
|
|
f5f70287e7 | ||
|
|
d5ba4ebf0c | ||
|
|
3bfead4ff1 | ||
|
|
d04621b1d6 | ||
|
|
a681df04bd | ||
|
|
9f3c1036d0 | ||
|
|
c9011ac692 | ||
|
|
42f977f536 | ||
|
|
a0580c5f5a | ||
|
|
121a9cd87c | ||
|
|
d87d608d9e | ||
|
|
bd9ecc1519 | ||
|
|
f5a113da47 | ||
|
|
11fdb0c1dc | ||
|
|
e0033ceb12 | ||
|
|
7318ec6ef6 | ||
|
|
a516c7adcf | ||
|
|
c48109ca66 | ||
|
|
f0ef781cd7 | ||
|
|
d9dd040db4 | ||
|
|
fec947b4c8 | ||
|
|
ec6df36c25 | ||
|
|
76150db293 | ||
|
|
815c3fe99e | ||
|
|
54878e8e8d | ||
|
|
6e4798f1d4 | ||
|
|
55bd6fb087 | ||
|
|
49035f4817 | ||
|
|
c0ebaa0bd3 | ||
|
|
b98b244fc3 | ||
|
|
c8d5128b0c | ||
|
|
0578a05a05 | ||
|
|
9a412b3e08 | ||
|
|
8deec1f80a | ||
|
|
26afbeed84 | ||
|
|
9975de01bc | ||
|
|
ac525466b8 | ||
|
|
c7a04e4559 | ||
|
|
5bbe30edbc | ||
|
|
31c7dc0577 | ||
|
|
aba5c7d8af | ||
|
|
2f4ea4cabb | ||
|
|
7e801a31bd | ||
|
|
eb4870c9d5 | ||
|
|
b1fdd7fc74 | ||
|
|
bf3e027067 | ||
|
|
a7a7e49e18 | ||
|
|
3c834d06d4 | ||
|
|
f5e6086d2a | ||
|
|
3365a8e482 | ||
|
|
268c2d1355 | ||
|
|
e9d6b963df | ||
|
|
f9d9447203 | ||
|
|
a0002d0b43 | ||
|
|
23188032ca | ||
|
|
f9f5a30e2c | ||
|
|
e393e17b59 | ||
|
|
a29d8f6aab | ||
|
|
1dbbc3ed8d | ||
|
|
410acb5b5c | ||
|
|
53c9160cd6 | ||
|
|
28b1170633 | ||
|
|
4abd4070f7 | ||
|
|
bca17b11a7 | ||
|
|
2a6d532852 | ||
|
|
a9a5af8cc8 | ||
|
|
edfa8e5256 | ||
|
|
2ebcfdc770 | ||
|
|
fc47df786b | ||
|
|
f6aa405607 | ||
|
|
101e8feaa2 | ||
|
|
df8f588e15 | ||
|
|
3ef549537e | ||
|
|
d81d08f1ce | ||
|
|
a77b7717b8 | ||
|
|
d5f903781d | ||
|
|
8b12546b56 | ||
|
|
5bb91a7113 | ||
|
|
a04c2512b4 | ||
|
|
c3f0df6567 | ||
|
|
8b8aec4dc1 | ||
|
|
1d228ff76e | ||
|
|
b5863c0104 | ||
|
|
08ccc43f8d | ||
|
|
e33ae17061 | ||
|
|
60ebdb64d7 | ||
|
|
c27d0ad5bb | ||
|
|
c1b9d941f3 | ||
|
|
175866ce00 | ||
|
|
8c5de18991 | ||
|
|
1afb27a508 | ||
|
|
6a8481f7df | ||
|
|
7f201565c0 | ||
|
|
ae9474b95d | ||
|
|
050f3abc89 | ||
|
|
888c22cb54 | ||
|
|
f633fccbca | ||
|
|
39fec310f9 | ||
|
|
d5d922d830 | ||
|
|
f8c8407af3 | ||
|
|
e6d0d44c5a | ||
|
|
1933c9ea3d | ||
|
|
8a695b4bb5 | ||
|
|
bf71b45fbb | ||
|
|
9124c9f1e3 | ||
|
|
bcc2d05625 | ||
|
|
22bfd720b5 | ||
|
|
63f56aec0c | ||
|
|
dd3d7e9c49 | ||
|
|
dbb66edd9f | ||
|
|
8bfb48dff3 | ||
|
|
12d963d2bd | ||
|
|
c817c2dd40 | ||
|
|
8a230ba989 | ||
|
|
2a9cc3190c | ||
|
|
e84d55a697 | ||
|
|
403a94f70c | ||
|
|
633eb78630 | ||
|
|
bbe1f4936c | ||
|
|
11a87263be | ||
|
|
8b30dddc06 | ||
|
|
b93dd90caf | ||
|
|
0054019f82 | ||
|
|
736220a94e | ||
|
|
a9c82791b3 | ||
|
|
eacf4decab | ||
|
|
b8cda2ec74 | ||
|
|
69ab7e031b | ||
|
|
ace6431cb8 | ||
|
|
40790de909 | ||
|
|
750037b5d2 | ||
|
|
6b10385c0d | ||
|
|
56ead0f894 | ||
|
|
d633cba630 | ||
|
|
e43833c0ea | ||
|
|
7f86337cb2 | ||
|
|
f2bb388b3b | ||
|
|
43e45aef3f | ||
|
|
c5880596a4 | ||
|
|
59a644891d | ||
|
|
a326cdc736 | ||
|
|
03a28dbb3e | ||
|
|
32542a822e | ||
|
|
1df90ff53b | ||
|
|
57f00a283b | ||
|
|
312e1dd41f | ||
|
|
487b979f0d | ||
|
|
b9d813d8f0 | ||
|
|
36dd128459 | ||
|
|
426d836a09 | ||
|
|
2fad66d24a | ||
|
|
144f0d39c6 | ||
|
|
83429f9f65 | ||
|
|
a60ae78176 | ||
|
|
7b39f4e9d9 | ||
|
|
32a294a64a | ||
|
|
b7b70346b4 | ||
|
|
59832e6ba2 | ||
|
|
f309218573 | ||
|
|
0321eff62b | ||
|
|
01577fd798 | ||
|
|
4c8ea31d98 | ||
|
|
92ac86a6bd | ||
|
|
13ed69c82a | ||
|
|
b83f6da648 | ||
|
|
89f1167239 | ||
|
|
82d868751f | ||
|
|
31b9f18921 | ||
|
|
fd80823910 | ||
|
|
6bd8221eda | ||
|
|
ce77e926a5 | ||
|
|
2710edb76b | ||
|
|
8f1ff9e774 | ||
|
|
c09244039e | ||
|
|
bf9108408e | ||
|
|
fca91e1560 | ||
|
|
304336d905 | ||
|
|
42b4906594 | ||
|
|
3c881a40a6 | ||
|
|
c7849a82bf | ||
|
|
781d51df6f | ||
|
|
758b1e43e7 | ||
|
|
5cc8231c36 | ||
|
|
7b447faefa | ||
|
|
a017256e95 | ||
|
|
1050891d87 | ||
|
|
a246cff7f0 | ||
|
|
c218db5047 | ||
|
|
c7f2f3614f | ||
|
|
c826d8cf07 | ||
|
|
5cb2320760 |
2
.config/nextest.toml
Normal file
2
.config/nextest.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "3s", terminate-after = 2 }
|
||||
28
.env.example
28
.env.example
@@ -1,5 +1,7 @@
|
||||
# MongoDB URI
|
||||
MONGODB=mongodb://localhost
|
||||
AUTUMN_MONGO_URI=mongodb://database
|
||||
REDIS_URI=redis://localhost/
|
||||
|
||||
# URL to where the Revolt app is publicly accessible
|
||||
REVOLT_APP_URL=http://local.revolt.chat:5000
|
||||
@@ -59,6 +61,9 @@ REVOLT_UNSAFE_NO_EMAIL=1
|
||||
## Application Settings
|
||||
##
|
||||
|
||||
# Whether to enable staging only features
|
||||
REVOLT_IS_STAGING=1
|
||||
|
||||
# Whether to only allow users to sign up if they have an invite code
|
||||
REVOLT_INVITE_ONLY=0
|
||||
|
||||
@@ -77,3 +82,26 @@ REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nf
|
||||
##
|
||||
|
||||
# VOSO_MANAGE_TOKEN=CHANGEME
|
||||
|
||||
|
||||
##
|
||||
## Autumn configuration
|
||||
##
|
||||
|
||||
# S3 Region
|
||||
AUTUMN_S3_REGION=minio
|
||||
|
||||
# S3 Endpoint
|
||||
AUTUMN_S3_ENDPOINT=http://minio:9000
|
||||
|
||||
# MinIO Root User
|
||||
MINIO_ROOT_USER=minioautumn
|
||||
|
||||
# MinIO Root Password
|
||||
MINIO_ROOT_PASSWORD=minioautumn
|
||||
|
||||
# AWS Access Key ID
|
||||
AWS_ACCESS_KEY_ID=minioautumn
|
||||
|
||||
# AWS Secret Key
|
||||
AWS_SECRET_ACCESS_KEY=minioautumn
|
||||
|
||||
37
.github/workflows/book.yml
vendored
Normal file
37
.github/workflows/book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Build documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pages: write # To push to a GitHub Pages site
|
||||
id-token: write # To update the deployment status
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install latest mdbook
|
||||
run: |
|
||||
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
|
||||
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
mkdir mdbook
|
||||
curl -sSL $url | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Build Book
|
||||
run: |
|
||||
cd doc
|
||||
mdbook build
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "doc/book"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
36
.github/workflows/cla.yml
vendored
36
.github/workflows/cla.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened,closed,synchronize]
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.1.3-beta
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PAT }}
|
||||
with:
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-document: 'https://github.com/revoltchat/cla/blob/master/CLA.md' # e.g. a CLA or a DCO document
|
||||
# branch should not be protected
|
||||
branch: 'master'
|
||||
allowlist: insertish,bot*
|
||||
|
||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||
remote-organization-name: revoltchat
|
||||
remote-repository-name: cla
|
||||
create-file-commit-message: 'cla(create): creating file for storing CLA Signatures'
|
||||
signed-commit-message: 'cla(sign): $contributorName has signed the CLA in #$pullRequestNo'
|
||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
59
.github/workflows/docker.yaml
vendored
59
.github/workflows/docker.yaml
vendored
@@ -2,65 +2,71 @@ name: Docker Test & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
# branches:
|
||||
# - "main"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "main"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
base:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build base image (amd64)
|
||||
name: Build base image
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- 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
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: docker/login-action@v2
|
||||
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
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/revoltchat/base:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
cache-from: type=gha,scope=buildx-base-multi-arch
|
||||
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
|
||||
|
||||
publish_amd64:
|
||||
publish:
|
||||
needs: [base]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
strategy:
|
||||
matrix:
|
||||
project: [delta, bonfire]
|
||||
name: Build ${{ matrix.project }} image (amd64)
|
||||
name: Build ${{ matrix.project }} image
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
@@ -68,10 +74,11 @@ jobs:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -86,11 +93,11 @@ jobs:
|
||||
{
|
||||
"delta": {
|
||||
"path": "crates/delta",
|
||||
"tag": "revoltchat/server"
|
||||
"tag": "${{ github.repository_owner }}/server"
|
||||
},
|
||||
"bonfire": {
|
||||
"path": "crates/bonfire",
|
||||
"tag": "revoltchat/bonfire"
|
||||
"tag": "${{ github.repository_owner }}/bonfire"
|
||||
}
|
||||
}
|
||||
export_to: output
|
||||
@@ -98,19 +105,21 @@ jobs:
|
||||
# Configure metadata
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ steps.export.outputs.tag }}, ghcr.io/${{ steps.export.outputs.tag }}
|
||||
images: |
|
||||
docker.io/${{ steps.export.outputs.tag }}
|
||||
ghcr.io/${{ steps.export.outputs.tag }}
|
||||
|
||||
# Build crate image
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ${{ steps.export.outputs.path }}/Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
60
.github/workflows/rust.yaml
vendored
60
.github/workflows/rust.yaml
vendored
@@ -2,9 +2,16 @@ name: Rust build, test, and generate specification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/rust.yaml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -21,40 +28,53 @@ jobs:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Install cargo-nextest
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
with:
|
||||
crate: cargo-nextest
|
||||
locked: true
|
||||
|
||||
- name: Run cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
- name: Copy .env.example
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Run services in background
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
docker-compose -f docker-compose.db.yml up -d
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request'
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
cargo run --bin revolt-delta &
|
||||
cp .env.example .env
|
||||
|
||||
- name: Run cargo test
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo nextest run
|
||||
|
||||
- name: Run cargo test (with MongoDB)
|
||||
env:
|
||||
TEST_DB: MONGODB
|
||||
MONGODB: mongodb://localhost
|
||||
run: |
|
||||
cargo nextest run
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
|
||||
|
||||
- name: Wait for API to go up
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: nev7n/wait_for_response@v1
|
||||
with:
|
||||
url: "http://localhost:8000/"
|
||||
|
||||
- name: Checkout API repository
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: revoltchat/api
|
||||
@@ -62,11 +82,11 @@ jobs:
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
with:
|
||||
cwd: "api"
|
||||
|
||||
4
.github/workflows/triage_pr.yml
vendored
4
.github/workflows/triage_pr.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
projectV2(number: 5) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
Rocket.toml
|
||||
Revolt.toml
|
||||
|
||||
target
|
||||
.data
|
||||
.env
|
||||
|
||||
.vercel
|
||||
.DS_Store
|
||||
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy"
|
||||
}
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"nixEnvSelector.suggestion": false,
|
||||
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
|
||||
}
|
||||
|
||||
2561
Cargo.lock
generated
2561
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
@@ -1,2 +1,10 @@
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
resolver = "2"
|
||||
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
|
||||
|
||||
[patch.crates-io]
|
||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||
redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
|
||||
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
|
||||
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
|
||||
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -1,12 +1,32 @@
|
||||
# Build Stage
|
||||
FROM rustlang/rust:nightly-slim AS builder
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
# Install build requirements
|
||||
RUN apt-get update && apt-get install -y libssl-dev pkg-config
|
||||
ARG TARGETARCH
|
||||
|
||||
# Build all crates
|
||||
# Install build requirements
|
||||
RUN dpkg --add-architecture "${TARGETARCH}"
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
make \
|
||||
pkg-config \
|
||||
libssl-dev:"${TARGETARCH}"
|
||||
COPY scripts/build-image-layer.sh /tmp/
|
||||
RUN sh /tmp/build-image-layer.sh tools
|
||||
|
||||
# Build all dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
COPY crates ./crates
|
||||
RUN cargo build --locked --release
|
||||
RUN sh /tmp/build-image-layer.sh apps
|
||||
|
||||
28
Dockerfile.useCurrentArch
Normal file
28
Dockerfile.useCurrentArch
Normal file
@@ -0,0 +1,28 @@
|
||||
# Build Stage
|
||||
FROM rust:1.70.0-slim
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
# Install build requirements
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
make \
|
||||
pkg-config \
|
||||
libssl-dev
|
||||
COPY scripts/build-image-layer.sh /tmp/
|
||||
|
||||
# Build all dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
COPY crates ./crates
|
||||
RUN sh /tmp/build-image-layer.sh apps
|
||||
5
LICENSE
5
LICENSE
@@ -1,4 +1,7 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
With the exception of crates that specify their own LICENSE file,
|
||||
the following license applies to the source code of this project.
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
|
||||
156
README.md
156
README.md
@@ -2,30 +2,152 @@
|
||||
|
||||
This is a monorepo for the Revolt backend.
|
||||
|
||||
| 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 |-->
|
||||
| Crate | Path | Description |
|
||||
| ------------------ | -------------------------------------------------- | ----------------------------- |
|
||||
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |
|
||||
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |
|
||||
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |
|
||||
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |
|
||||
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |
|
||||
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
|
||||
|
||||
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
||||
|
||||
## Resources
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
### Revolt
|
||||
Rust 1.70 or higher.
|
||||
|
||||
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
|
||||
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
|
||||
- [Contribution Guide](https://developers.revolt.chat/contributing)
|
||||
## Development Guide
|
||||
|
||||
## Contributing
|
||||
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
|
||||
|
||||
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
|
||||
Please note that a pull request should only take care of one issue so that we can review it quickly.
|
||||
Before getting started, you'll want to install:
|
||||
|
||||
- Rust toolchain (rustup recommended)
|
||||
- Docker
|
||||
- Git
|
||||
- mold (optional, faster compilation)
|
||||
|
||||
> A **default.nix** is available for Nix users!
|
||||
> Just run `nix-shell` and continue.
|
||||
|
||||
Now you can clone and build the project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/revoltchat/backend revolt-backend
|
||||
cd revolt-backend
|
||||
cargo build
|
||||
```
|
||||
|
||||
If you want to run the API and event servers:
|
||||
|
||||
```bash
|
||||
# create environment file (will be deprecated in future)
|
||||
cp .env.example .env
|
||||
|
||||
# (optionally) copy the default configuration file
|
||||
cp crates/core/config/Revolt.toml Revolt.toml
|
||||
# configure as necessary...
|
||||
```
|
||||
|
||||
You may want to copy the following configuration:
|
||||
|
||||
```toml
|
||||
# Revolt.toml
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost"
|
||||
|
||||
[hosts]
|
||||
app = "http://local.revolt.chat"
|
||||
api = "http://local.revolt.chat:8000"
|
||||
events = "ws://local.revolt.chat:9000"
|
||||
autumn = "http://local.revolt.chat:3000"
|
||||
january = "http://local.revolt.chat:7000"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
```
|
||||
|
||||
Then continue:
|
||||
|
||||
```bash
|
||||
# start other necessary services
|
||||
docker compose up -d
|
||||
|
||||
# run the API server
|
||||
cargo run --bin revolt-delta
|
||||
# run the events server
|
||||
cargo run --bin revolt-bonfire
|
||||
|
||||
# hint:
|
||||
# mold -run <cargo build, cargo run, etc...>
|
||||
```
|
||||
|
||||
You can start a web client by doing the following:
|
||||
|
||||
```bash
|
||||
# if you do not have yarn yet and have a modern Node.js:
|
||||
corepack enable
|
||||
|
||||
# clone the web client and run it:
|
||||
git clone --recursive https://github.com/revoltchat/revite
|
||||
cd revite
|
||||
yarn
|
||||
yarn build:deps
|
||||
yarn dev --port 3001
|
||||
```
|
||||
|
||||
Then go to https://local.revolt.chat:3001
|
||||
|
||||
## Deployment Guide
|
||||
|
||||
### Cutting new crate releases
|
||||
|
||||
Begin by bumping crate versions:
|
||||
|
||||
```bash
|
||||
just patch # 0.0.X
|
||||
just minor # 0.X.0
|
||||
just major # X.0.0
|
||||
```
|
||||
|
||||
Then commit the changes to package files.
|
||||
|
||||
Proceed to publish all the new crates:
|
||||
|
||||
```bash
|
||||
just publish
|
||||
```
|
||||
|
||||
### Cutting new binary releases
|
||||
|
||||
Tag and push a new release by running:
|
||||
|
||||
```bash
|
||||
just release
|
||||
```
|
||||
|
||||
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/revoltchat/backend/releases/new) to create a changelog.
|
||||
|
||||
## Testing
|
||||
|
||||
First, start the required services:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.db.yml up -d
|
||||
```
|
||||
|
||||
Now run tests for whichever database:
|
||||
|
||||
```sh
|
||||
TEST_DB=REFERENCE cargo nextest run
|
||||
TEST_DB=MONGODB cargo nextest run
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). Please check individual crates for further license information.
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
|
||||
|
||||
**Individual crates may supply their own licenses!**
|
||||
|
||||
73
STYLE_GUIDE.md
Normal file
73
STYLE_GUIDE.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Code Style Guide
|
||||
|
||||
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
|
||||
|
||||
## Writing Style
|
||||
|
||||
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
|
||||
|
||||
## `core/database` crate
|
||||
|
||||
w.r.t. `model.rs` files
|
||||
|
||||
- All struct definitions must be commented.
|
||||
```rust
|
||||
/// Server
|
||||
pub struct Server {
|
||||
/// Name of the server
|
||||
pub name: String,
|
||||
```
|
||||
- Struct definitions should not include derives unless necessary (if additional traits such as Hash are required) and instead use `auto_derived!` and `auto_derived_partial!`.
|
||||
```rust
|
||||
auto_derived_partial!(
|
||||
/// Server
|
||||
pub struct Server { .. },
|
||||
"PartialServer"
|
||||
);
|
||||
```
|
||||
- `auto_derived!` macro accepts multiple entries and should be used as such:
|
||||
|
||||
```rust
|
||||
auto_derived!(
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsServer { .. }
|
||||
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsRole { .. }
|
||||
);
|
||||
```
|
||||
|
||||
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
|
||||
```rust
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
```
|
||||
- `impl` blocks may be defined below the struct definitions and should be ordered in the same order of definition. Methods in the block must follow the same guidelines as traits where-in: methods are ordered in terms of CRUD, there are empty line breaks, and methods are commented.
|
||||
|
||||
w.r.t. `ops` module for models
|
||||
|
||||
- All traits must use a the name format `AbstractPlural` where Plural is the plural form of the collection. e.g. Servers
|
||||
- Traits defined must follow these guidelines:
|
||||
|
||||
- Methods are ordered in terms of CRUD, create-read-update-delete ordering.
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait AbstractServerMembers: Sync + Send {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()>;
|
||||
|
||||
/// Fetch a server member by their id
|
||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
||||
|
||||
/// Update information for a server member
|
||||
async fn update_member(&self, .. ) -> Result<()>;
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
- There should be an empty line break between each method declaration.
|
||||
- All methods must have an appropriate comment.
|
||||
|
||||
- When implementing the trait defined in `ops.rs` with each driver, the method declaration style should be the same for ease of searching: same ordering, same comments, same line breaks.
|
||||
28
clippy.toml
Normal file
28
clippy.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
disallowed-methods = [
|
||||
# Shouldn't need to access these directly
|
||||
"revolt_database::models::bots::model::Bot::remove_field",
|
||||
"revolt_database::models::messages::model::Message::attach_sendable_embed",
|
||||
"revolt_database::models::users::model::User::set_relationship",
|
||||
"revolt_database::models::users::model::User::apply_relationship",
|
||||
|
||||
# Prefer to use Object::create()
|
||||
"revolt_database::models::bots::ops::AbstractBots::insert_bot",
|
||||
"revolt_database::models::channel_invites::ops::AbstractChannelInvites::insert_invite",
|
||||
"revolt_database::models::channel_unreads::ops::AbstractChannelUnreads::acknowledge_message",
|
||||
"revolt_database::models::channel_webhooks::ops::AbstractWebhooks::insert_webhook",
|
||||
"revolt_database::models::channels::ops::AbstractChannels::insert_channel",
|
||||
"revolt_database::models::emojis::ops::AbstractEmojis::insert_emoji",
|
||||
"revolt_database::models::files::ops::AbstractAttachments::insert_attachment",
|
||||
"revolt_database::models::messages::ops::AbstractMessages::insert_message",
|
||||
"revolt_database::models::ratelimit_events::ops::AbstractRatelimitEvents::insert_ratelimit_event",
|
||||
"revolt_database::models::server_bans::ops::AbstractServerBans::insert_ban",
|
||||
"revolt_database::models::server_members::ops::AbstractServerMembers::insert_member",
|
||||
"revolt_database::models::servers::ops::AbstractServers::insert_server",
|
||||
"revolt_database::models::users::ops::AbstractUsers::insert_user",
|
||||
|
||||
# Prefer to use Object::update(&self)
|
||||
"revolt_database::models::bots::ops::AbstractBots::update_bot",
|
||||
|
||||
# Prefer to use Object::delete(&self)
|
||||
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.5.9"
|
||||
version = "0.7.14"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -9,15 +9,19 @@ edition = "2021"
|
||||
[dependencies]
|
||||
# util
|
||||
log = "*"
|
||||
sentry = "0.31.5"
|
||||
lru = "0.7.6"
|
||||
ulid = "0.5.0"
|
||||
once_cell = "1.9.0"
|
||||
redis-kiss = "0.1.4"
|
||||
lru_time_cache = "0.11.11"
|
||||
async-channel = "2.3.1"
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# serde
|
||||
bincode = "1.3.3"
|
||||
serde_json = "1.0.79"
|
||||
rmp-serde = "1.0.0"
|
||||
serde = "1.0.136"
|
||||
@@ -25,4 +29,20 @@ serde = "1.0.136"
|
||||
# async
|
||||
futures = "0.3.21"
|
||||
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
|
||||
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
|
||||
async-std = { version = "1.8.0", features = [
|
||||
"tokio1",
|
||||
"tokio02",
|
||||
"attributes",
|
||||
] }
|
||||
|
||||
# core
|
||||
authifier = { version = "1.0.8" }
|
||||
revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database" }
|
||||
revolt-permissions = { version = "0.7.14", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# 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 ./
|
||||
FROM debian:bullseye-slim
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ca-certificates && \
|
||||
apt-get clean
|
||||
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
||||
EXPOSE 9000
|
||||
CMD ["./revolt-bonfire"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_quark::{Error, Result};
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Enumeration of supported protocol formats
|
||||
@@ -37,16 +37,16 @@ impl ProtocolConfiguration {
|
||||
match self.format {
|
||||
ProtocolFormat::Json => {
|
||||
if let Message::Text(text) = msg {
|
||||
serde_json::from_str(text).map_err(|_| Error::InternalError)
|
||||
serde_json::from_str(text).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
ProtocolFormat::Msgpack => {
|
||||
if let Message::Binary(buf) = msg {
|
||||
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError)
|
||||
rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use revolt_quark::{Database, DatabaseInfo};
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
static DBCONN: OnceCell<Database> = OnceCell::new();
|
||||
|
||||
@@ -10,7 +10,9 @@ pub async fn connect() {
|
||||
.await
|
||||
.expect("Failed to connect to the database.");
|
||||
|
||||
DBCONN.set(database).expect("Setting `Database`");
|
||||
if DBCONN.set(database).is_err() {
|
||||
panic!("couldn't set database")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the current database.
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
get_relationship,
|
||||
models::{
|
||||
server_member::FieldsMember,
|
||||
user::{PartialUser, Presence, RelationshipStatus},
|
||||
Channel, Member, User,
|
||||
},
|
||||
perms,
|
||||
presence::presence_filter_online,
|
||||
Database, Permission, Result,
|
||||
use revolt_database::{
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
|
||||
MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::{
|
||||
client::EventV1,
|
||||
state::{Cache, State},
|
||||
};
|
||||
use super::state::{Cache, State};
|
||||
|
||||
/// Cache Manager
|
||||
impl Cache {
|
||||
@@ -23,23 +17,24 @@ impl Cache {
|
||||
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
|
||||
match &channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
let member = self.members.values().find(|x| &x.id.server == server);
|
||||
|
||||
let member = self.members.get(server);
|
||||
let server = self.servers.get(server);
|
||||
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
||||
let mut query =
|
||||
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
|
||||
.channel(channel);
|
||||
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
||||
|
||||
if let Some(member) = member {
|
||||
perms.member.set_ref(member);
|
||||
query = query.member(member);
|
||||
}
|
||||
|
||||
if let Some(server) = server {
|
||||
perms.server.set_ref(server);
|
||||
query = query.server(server);
|
||||
}
|
||||
|
||||
perms
|
||||
.has_permission(db, Permission::ViewChannel)
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.has_channel_permission(ChannelPermission::ViewChannel)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
@@ -64,7 +59,7 @@ impl Cache {
|
||||
/// Check whether we can subscribe to another user
|
||||
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
|
||||
if let Some(user) = self.users.get(&self.user_id) {
|
||||
match get_relationship(user, user_id) {
|
||||
match user.relationship_with(user_id) {
|
||||
RelationshipStatus::Friend
|
||||
| RelationshipStatus::Incoming
|
||||
| RelationshipStatus::Outgoing
|
||||
@@ -96,10 +91,11 @@ impl Cache {
|
||||
impl State {
|
||||
/// Generate a Ready packet for the current user
|
||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
||||
let mut user = self.clone_user();
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: Vec<String> = user
|
||||
let mut user_ids: HashSet<String> = user
|
||||
.relations
|
||||
.as_ref()
|
||||
.map(|arr| arr.iter().map(|x| x.id.to_string()).collect())
|
||||
@@ -107,9 +103,15 @@ impl State {
|
||||
|
||||
// Fetch all memberships with their corresponding servers.
|
||||
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
|
||||
self.cache.members = members
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id.server.clone(), x))
|
||||
.collect();
|
||||
|
||||
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
|
||||
let servers = db.fetch_servers(&server_ids).await?;
|
||||
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
|
||||
// Collect channel ids from servers.
|
||||
let mut channel_ids = vec![];
|
||||
@@ -128,15 +130,14 @@ impl State {
|
||||
for channel in &channels {
|
||||
match channel {
|
||||
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
|
||||
user_ids.append(&mut recipients.clone());
|
||||
user_ids.extend(&mut recipients.clone().into_iter());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch presence data for known users.
|
||||
let online_ids = presence_filter_online(&user_ids).await;
|
||||
user.online = Some(true);
|
||||
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
|
||||
|
||||
// Fetch user data.
|
||||
let users = db
|
||||
@@ -149,68 +150,64 @@ impl State {
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
let emojis = db
|
||||
.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
.await?;
|
||||
|
||||
// Copy data into local state cache.
|
||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
self.cache
|
||||
.users
|
||||
.insert(self.cache.user_id.clone(), user.clone());
|
||||
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
self.cache.channels = channels
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id().to_string(), x))
|
||||
.collect();
|
||||
self.cache.members = members
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id.server.clone(), x))
|
||||
.collect();
|
||||
|
||||
// Make all users appear from our perspective.
|
||||
let mut users: Vec<User> = users
|
||||
let mut users: Vec<v0::User> = users
|
||||
.into_iter()
|
||||
.map(|mut x| {
|
||||
x.online = Some(online_ids.contains(&x.id));
|
||||
x.with_relationship(&user)
|
||||
.map(|other_user| {
|
||||
let is_online = online_ids.contains(&other_user.id);
|
||||
other_user.into_known(&user, is_online)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Make sure we see our own user correctly.
|
||||
user.relationship = Some(RelationshipStatus::User);
|
||||
users.push(user.foreign());
|
||||
users.push(user.into_self(true).await);
|
||||
|
||||
// Set subscription state internally.
|
||||
self.reset_state();
|
||||
self.insert_subscription(self.private_topic.clone());
|
||||
self.reset_state().await;
|
||||
self.insert_subscription(self.private_topic.clone()).await;
|
||||
|
||||
for user in &users {
|
||||
self.insert_subscription(user.id.clone());
|
||||
self.insert_subscription(user.id.clone()).await;
|
||||
}
|
||||
|
||||
for server in &servers {
|
||||
self.insert_subscription(server.id.clone());
|
||||
self.insert_subscription(server.id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", server.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
for channel in &channels {
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
}
|
||||
|
||||
Ok(EventV1::Ready {
|
||||
users,
|
||||
servers,
|
||||
channels,
|
||||
members,
|
||||
emojis,
|
||||
servers: servers.into_iter().map(Into::into).collect(),
|
||||
channels: channels.into_iter().map(Into::into).collect(),
|
||||
members: members.into_iter().map(Into::into).collect(),
|
||||
emojis: emojis.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -244,11 +241,11 @@ impl State {
|
||||
let mut bulk_events = vec![];
|
||||
|
||||
for id in added_channels {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
for id in removed_channels {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
self.cache.channels.remove(&id);
|
||||
|
||||
bulk_events.push(EventV1::ChannelDelete { id });
|
||||
@@ -271,8 +268,8 @@ impl State {
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
bulk_events.push(EventV1::ChannelCreate(channel));
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
bulk_events.push(EventV1::ChannelCreate(channel.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,11 +294,12 @@ impl State {
|
||||
} {
|
||||
let event = EventV1::UserUpdate {
|
||||
id: self.cache.user_id.clone(),
|
||||
data: PartialUser {
|
||||
data: v0::PartialUser {
|
||||
online: Some(target),
|
||||
..Default::default()
|
||||
},
|
||||
clear: vec![],
|
||||
event_id: Some(ulid::Ulid::new().to_string()),
|
||||
};
|
||||
|
||||
for server in self.cache.servers.keys() {
|
||||
@@ -343,8 +341,8 @@ impl State {
|
||||
match event {
|
||||
EventV1::ChannelCreate(channel) => {
|
||||
let id = channel.id().to_string();
|
||||
self.insert_subscription(id.clone());
|
||||
self.cache.channels.insert(id, channel.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
self.cache.channels.insert(id, channel.clone().into());
|
||||
}
|
||||
EventV1::ChannelUpdate {
|
||||
id, data, clear, ..
|
||||
@@ -352,15 +350,21 @@ impl State {
|
||||
let could_view: bool = if let Some(channel) = self.cache.channels.get(id) {
|
||||
self.cache.can_view_channel(db, channel).await
|
||||
} else {
|
||||
true
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(channel) = self.cache.channels.get_mut(id) {
|
||||
for field in clear {
|
||||
channel.remove(field);
|
||||
channel.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
channel.apply_options(data.clone());
|
||||
channel.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if !self.cache.channels.contains_key(id) {
|
||||
if let Ok(channel) = db.fetch_channel(id).await {
|
||||
self.cache.channels.insert(id.clone(), channel);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(channel) = self.cache.channels.get(id) {
|
||||
@@ -368,7 +372,7 @@ impl State {
|
||||
if could_view != can_view {
|
||||
if can_view {
|
||||
queue_add = Some(id.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone().into());
|
||||
} else {
|
||||
queue_remove = Some(id.clone());
|
||||
*event = EventV1::ChannelDelete { id: id.clone() };
|
||||
@@ -377,17 +381,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
EventV1::ChannelDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
self.cache.channels.remove(id);
|
||||
}
|
||||
EventV1::ChannelGroupJoin { user, .. } => {
|
||||
self.insert_subscription(user.clone());
|
||||
self.insert_subscription(user.clone()).await;
|
||||
}
|
||||
EventV1::ChannelGroupLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
} else if !self.cache.can_subscribe_to_user(user) {
|
||||
self.remove_subscription(user);
|
||||
self.remove_subscription(user).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,14 +399,28 @@ impl State {
|
||||
id,
|
||||
server,
|
||||
channels,
|
||||
emojis: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone());
|
||||
self.cache.servers.insert(id.to_string(), server.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", id)).await;
|
||||
}
|
||||
|
||||
self.cache.servers.insert(id.clone(), server.clone().into());
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.clone(),
|
||||
user: self.cache.user_id.clone(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
self.cache.members.insert(id.clone(), member);
|
||||
|
||||
for channel in channels {
|
||||
self.cache
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
.insert(channel.id().to_string(), channel.clone().into());
|
||||
}
|
||||
|
||||
queue_server = Some(id.clone());
|
||||
@@ -412,10 +430,10 @@ impl State {
|
||||
} => {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
for field in clear {
|
||||
server.remove(field);
|
||||
server.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
server.apply_options(data.clone());
|
||||
server.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.default_permissions.is_some() {
|
||||
@@ -425,39 +443,41 @@ impl State {
|
||||
EventV1::ServerMemberJoin { .. } => {
|
||||
// We will always receive ServerCreate when joining a new server.
|
||||
}
|
||||
EventV1::ServerMemberLeave { id, user } => {
|
||||
EventV1::ServerMemberLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel);
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
self.cache.members.remove(id);
|
||||
}
|
||||
}
|
||||
EventV1::ServerDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel);
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
self.cache.members.remove(id);
|
||||
}
|
||||
EventV1::ServerMemberUpdate { id, data, clear } => {
|
||||
if id.user == self.cache.user_id {
|
||||
if let Some(member) = self.cache.members.get_mut(&id.server) {
|
||||
for field in &clear.clone() {
|
||||
member.remove(field);
|
||||
member.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
member.apply_options(data.clone());
|
||||
member.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.roles.is_some() || clear.contains(&FieldsMember::Roles) {
|
||||
if data.roles.is_some() || clear.contains(&v0::FieldsMember::Roles) {
|
||||
queue_server = Some(id.server.clone());
|
||||
}
|
||||
}
|
||||
@@ -472,10 +492,10 @@ impl State {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
if let Some(role) = server.roles.get_mut(role_id) {
|
||||
for field in &clear.clone() {
|
||||
role.remove(field);
|
||||
role.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
role.apply_options(data.clone());
|
||||
role.apply_options(data.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,15 +519,41 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::UserUpdate { event_id, .. } => {
|
||||
if let Some(id) = event_id {
|
||||
if self.cache.seen_events.contains(id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.cache.seen_events.put(id.to_string(), ());
|
||||
}
|
||||
|
||||
*event_id = None;
|
||||
}
|
||||
EventV1::UserRelationship { id, user, .. } => {
|
||||
self.cache.users.insert(id.clone(), user.clone());
|
||||
self.cache.users.insert(id.clone(), user.clone().into());
|
||||
|
||||
if self.cache.can_subscribe_to_user(id) {
|
||||
self.insert_subscription(id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
} else {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::Message(message) => {
|
||||
// Since Message events are fanned out to many clients,
|
||||
// we must reconstruct the relationship value at this end.
|
||||
if let Some(user) = &mut message.user {
|
||||
user.relationship = self
|
||||
.cache
|
||||
.users
|
||||
.get(&self.cache.user_id)
|
||||
.expect("missing self?")
|
||||
.relationship_with(&message.author)
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -518,50 +564,13 @@ impl State {
|
||||
|
||||
// Sub / unsub accordingly.
|
||||
if let Some(id) = queue_add {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
if let Some(id) = queue_remove {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
/// Publish helper wrapper
|
||||
pub async fn p(self, channel: String) {
|
||||
#[cfg(not(debug_assertions))]
|
||||
redis_kiss::p(channel, self).await;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("Publishing event to {channel}: {self:?}");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
redis_kiss::publish(channel, self).await.unwrap();
|
||||
}
|
||||
|
||||
/// Publish user event
|
||||
pub async fn p_user(self, id: String, db: &Database) {
|
||||
self.clone().p(id.clone()).await;
|
||||
|
||||
// ! FIXME: this should be captured by member list in the future
|
||||
// ! and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().p(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish private event
|
||||
pub async fn private(self, id: String) {
|
||||
self.p(format!("{}!", id)).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
}
|
||||
}
|
||||
2
crates/bonfire/src/events/mod.rs
Normal file
2
crates/bonfire/src/events/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod r#impl;
|
||||
pub mod state;
|
||||
211
crates/bonfire/src/events/state.rs
Normal file
211
crates/bonfire/src/events/state.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use lru::LruCache;
|
||||
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
pub enum SubscriptionStateChange {
|
||||
/// No change
|
||||
None,
|
||||
/// Clear all subscriptions
|
||||
Reset,
|
||||
/// Append or remove subscriptions
|
||||
Change {
|
||||
add: Vec<String>,
|
||||
remove: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Dumb per-state cache implementation
|
||||
///
|
||||
/// Ideally this would use a global cache that
|
||||
/// allows for mutations and could use Rc<> to
|
||||
/// track usage. If Rc<> == 1, then it only
|
||||
/// remains in global cache, hence should be
|
||||
/// dropped.
|
||||
///
|
||||
/// ------------------------------------------------
|
||||
/// We can strip these objects to core information!!
|
||||
/// ------------------------------------------------
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub user_id: String,
|
||||
pub is_bot: bool,
|
||||
|
||||
pub users: HashMap<String, User>,
|
||||
pub channels: HashMap<String, Channel>,
|
||||
pub members: HashMap<String, Member>,
|
||||
pub servers: HashMap<String, Server>,
|
||||
|
||||
pub seen_events: LruCache<String, ()>,
|
||||
}
|
||||
|
||||
impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Cache {
|
||||
user_id: Default::default(),
|
||||
is_bot: false,
|
||||
|
||||
users: Default::default(),
|
||||
channels: Default::default(),
|
||||
members: Default::default(),
|
||||
servers: Default::default(),
|
||||
|
||||
seen_events: LruCache::new(20),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Client state
|
||||
pub struct State {
|
||||
pub cache: Cache,
|
||||
|
||||
pub session_id: String,
|
||||
pub private_topic: String,
|
||||
pub state: SubscriptionStateChange,
|
||||
|
||||
pub subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
pub active_servers: Arc<Mutex<LruTimeCache<String, ()>>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Create state from User
|
||||
pub fn from(user: User, session_id: String) -> State {
|
||||
let mut subscribed = HashSet::new();
|
||||
let private_topic = format!("{}!", user.id);
|
||||
subscribed.insert(private_topic.clone());
|
||||
subscribed.insert(user.id.clone());
|
||||
|
||||
let mut cache: Cache = Cache {
|
||||
user_id: user.id.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
cache.users.insert(user.id.clone(), user);
|
||||
|
||||
State {
|
||||
cache,
|
||||
subscribed: Arc::new(RwLock::new(subscribed)),
|
||||
active_servers: Arc::new(Mutex::new(LruTimeCache::with_expiry_duration_and_capacity(
|
||||
Duration::from_secs(900),
|
||||
5,
|
||||
))),
|
||||
session_id,
|
||||
private_topic,
|
||||
state: SubscriptionStateChange::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply currently queued state
|
||||
pub async fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
// Check if we need to change subscriptions to member event topics
|
||||
if !self.cache.is_bot {
|
||||
enum Server {
|
||||
Subscribe(String),
|
||||
Unsubscribe(String),
|
||||
}
|
||||
|
||||
let active_server_changes: Vec<Server> = {
|
||||
let mut active_servers = self.active_servers.lock().await;
|
||||
active_servers
|
||||
.notify_iter()
|
||||
.map(|e| match e {
|
||||
TimedEntry::Valid(k, _) => Server::Subscribe(format!("{}u", k)),
|
||||
TimedEntry::Expired(k, _) => Server::Unsubscribe(format!("{}u", k)),
|
||||
})
|
||||
.collect()
|
||||
// It is bad practice to open more than one Mutex at once and could
|
||||
// lead to a deadlock, so instead we choose to collect the changes.
|
||||
};
|
||||
|
||||
for entry in active_server_changes {
|
||||
match entry {
|
||||
Server::Subscribe(k) => {
|
||||
self.insert_subscription(k).await;
|
||||
}
|
||||
Server::Unsubscribe(k) => {
|
||||
self.remove_subscription(&k).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush changes to subscriptions
|
||||
let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None);
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if let SubscriptionStateChange::Change { add, remove } = &state {
|
||||
for id in add {
|
||||
subscribed.insert(id.clone());
|
||||
}
|
||||
|
||||
for id in remove {
|
||||
subscribed.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// Clone the active user
|
||||
pub fn clone_user(&self) -> User {
|
||||
self.cache.users.get(&self.cache.user_id).unwrap().clone()
|
||||
}
|
||||
|
||||
/// Reset the current state
|
||||
pub async fn reset_state(&mut self) {
|
||||
self.state = SubscriptionStateChange::Reset;
|
||||
self.subscribed.write().await.clear();
|
||||
}
|
||||
|
||||
/// Add a new subscription
|
||||
pub async fn insert_subscription(&mut self, subscription: String) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if subscribed.contains(&subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
match &mut self.state {
|
||||
SubscriptionStateChange::None => {
|
||||
self.state = SubscriptionStateChange::Change {
|
||||
add: vec![subscription.clone()],
|
||||
remove: vec![],
|
||||
};
|
||||
}
|
||||
SubscriptionStateChange::Change { add, .. } => {
|
||||
add.push(subscription.clone());
|
||||
}
|
||||
SubscriptionStateChange::Reset => {}
|
||||
}
|
||||
|
||||
subscribed.insert(subscription);
|
||||
}
|
||||
|
||||
/// Remove existing subscription
|
||||
pub async fn remove_subscription(&mut self, subscription: &str) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if !subscribed.contains(&subscription.to_string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
match &mut self.state {
|
||||
SubscriptionStateChange::None => {
|
||||
self.state = SubscriptionStateChange::Change {
|
||||
add: vec![],
|
||||
remove: vec![subscription.to_string()],
|
||||
};
|
||||
}
|
||||
SubscriptionStateChange::Change { remove, .. } => {
|
||||
remove.push(subscription.to_string());
|
||||
}
|
||||
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
|
||||
}
|
||||
|
||||
subscribed.remove(subscription);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
use std::env;
|
||||
|
||||
use async_std::net::TcpListener;
|
||||
use revolt_quark::presence::presence_clear_region;
|
||||
use revolt_presence::clear_region;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
|
||||
mod database;
|
||||
mod websocket;
|
||||
@@ -14,11 +15,11 @@ mod websocket;
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
// Configure requirements for Bonfire.
|
||||
revolt_quark::configure!();
|
||||
revolt_config::configure!(events);
|
||||
database::connect().await;
|
||||
|
||||
// Clean up the current region information.
|
||||
presence_clear_region(None).await;
|
||||
clear_region(None).await;
|
||||
|
||||
// Setup a TCP listener to accept WebSocket connections on.
|
||||
// By default, we bind to port 9000 on all interfaces.
|
||||
@@ -29,6 +30,10 @@ async fn main() {
|
||||
|
||||
// Start accepting new connections and spawn a client for each connection.
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
websocket::spawn_client(database::get_db(), stream, addr);
|
||||
async_std::task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
websocket::client(database::get_db(), stream, addr).await;
|
||||
info!("User disconnected from {addr:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,253 +1,523 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use revolt_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_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::{RedisError, RedisErrorKind},
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
join, pin_mut, select,
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
||||
use revolt_database::{
|
||||
events::{client::EventV1, server::ClientMessage},
|
||||
Database, User, UserHint,
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
|
||||
use async_std::{net::TcpStream, sync::Mutex, task};
|
||||
use async_std::{
|
||||
net::TcpStream,
|
||||
sync::{Mutex, RwLock},
|
||||
task::spawn,
|
||||
};
|
||||
use revolt_result::create_error;
|
||||
use sentry::Level;
|
||||
|
||||
use crate::config::WebsocketHandshakeCallback;
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
use crate::events::state::{State, SubscriptionStateChange};
|
||||
|
||||
/// Spawn a new WebSocket client worker given access to the database,
|
||||
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
|
||||
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
|
||||
|
||||
/// Start a new WebSocket client worker given access to the database,
|
||||
/// the relevant TCP stream and the remote address of the client.
|
||||
pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
|
||||
// Spawn a new Async task to work on.
|
||||
task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
|
||||
// Upgrade the TCP connection to a WebSocket connection.
|
||||
// In this process, we also parse any additional parameters given.
|
||||
// e.g. wss://example.com?format=json&version=1
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
|
||||
stream,
|
||||
WebsocketHandshakeCallback::from(sender),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Upgrade the TCP connection to a WebSocket connection.
|
||||
// In this process, we also parse any additional parameters given.
|
||||
// e.g. wss://example.com?format=json&version=1
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
if let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
|
||||
stream,
|
||||
WebsocketHandshakeCallback::from(sender),
|
||||
None,
|
||||
)
|
||||
.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()
|
||||
);
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (write, mut read) = ws.split();
|
||||
let write = Mutex::new(write);
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (mut write, mut read) = ws.split();
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
while let Ok(Some(message)) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
write
|
||||
.send(config.encode(&create_error!(InvalidSession)))
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
|
||||
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
write.send(config.encode(&err)).await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user, session_id);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
if let Err(err) = write.send(config.encode(&EventV1::Authenticated)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
let ready_payload = match state.generate_ready_payload(db).await {
|
||||
Ok(ready_payload) => ready_payload,
|
||||
Err(err) => {
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = write.send(config.encode(&ready_payload)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
{
|
||||
// Setup channels and mutexes
|
||||
let write = Mutex::new(write);
|
||||
let subscribed = state.subscribed.clone();
|
||||
let active_servers = state.active_servers.clone();
|
||||
let (topic_signal_s, topic_signal_r) = async_channel::unbounded();
|
||||
|
||||
// TODO: this needs to be rewritten
|
||||
// Create channels through which the tasks can signal to each other they need to clean up
|
||||
let (kill_signal_1_s, kill_signal_1_r) = async_channel::bounded(1);
|
||||
let (kill_signal_2_s, kill_signal_2_r) = async_channel::bounded(1);
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener_with_kill_signal(
|
||||
db,
|
||||
&mut state,
|
||||
addr,
|
||||
&config,
|
||||
topic_signal_r,
|
||||
kill_signal_1_r,
|
||||
&write,
|
||||
kill_signal_2_s,
|
||||
);
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker_with_kill_signal(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id.clone(),
|
||||
&config,
|
||||
topic_signal_s,
|
||||
kill_signal_2_r,
|
||||
read,
|
||||
&write,
|
||||
kill_signal_1_s,
|
||||
);
|
||||
|
||||
join!(listener, worker);
|
||||
}
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
|
||||
// If this was the last session, notify other users that we just went offline.
|
||||
if last_session {
|
||||
state.broadcast_presence_change(false).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn listener_with_kill_signal(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
listener(
|
||||
db,
|
||||
state,
|
||||
addr,
|
||||
config,
|
||||
topic_signal_r,
|
||||
kill_signal_r,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
async fn listener(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
||||
let subscriber = match fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
{
|
||||
Ok(subscriber) => subscriber,
|
||||
Err(err) => {
|
||||
error!("Failed to build a subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = subscriber.init().await {
|
||||
error!("Failed to init subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
};
|
||||
|
||||
// Handle Redis connection dropping
|
||||
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
||||
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
|
||||
subscriber.on_error(move |err| {
|
||||
if let RedisErrorKind::Canceled = err.kind() {
|
||||
let clean_up_s = clean_up_s.clone();
|
||||
spawn(async move {
|
||||
clean_up_s.lock().await.send(()).await.ok();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
'out: loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state().await {
|
||||
SubscriptionStateChange::Reset => {
|
||||
if let Err(err) = subscriber.unsubscribe_all().await {
|
||||
error!("Unsubscribe all failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
|
||||
let subscribed = state.subscribed.read().await;
|
||||
for id in subscribed.iter() {
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
if let Some(token) = config.get_session_token().as_ref() {
|
||||
match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => {
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} has reset their subscriptions");
|
||||
}
|
||||
SubscriptionStateChange::Change { add, remove } => {
|
||||
for id in remove {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
if let Err(err) = subscriber.unsubscribe(id).await {
|
||||
error!("Unsubscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) =
|
||||
presence_create_session(&user_id, 0).await;
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
if let Ok(ready_payload) = state.generate_ready_payload(db).await {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&ready_payload))
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = async {
|
||||
if let Ok(mut conn) = redis_kiss::open_pubsub_connection().await
|
||||
{
|
||||
loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state() {
|
||||
SubscriptionStateChange::Reset => {
|
||||
for id in state.iter_subscriptions() {
|
||||
conn.subscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} has reset their subscriptions");
|
||||
}
|
||||
SubscriptionStateChange::Change { add, remove } => {
|
||||
for id in remove {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
conn.unsubscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
conn.subscribe(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
// * Debug logging of current subscriptions.
|
||||
/*#[cfg(debug_assertions)]
|
||||
info!(
|
||||
"User {addr:?} is subscribed to {:?}",
|
||||
state
|
||||
.iter_subscriptions()
|
||||
.collect::<Vec<&String>>()
|
||||
);*/
|
||||
|
||||
// Handle incoming events.
|
||||
match conn.on_message().next().await.map(|item| {
|
||||
(
|
||||
item.get_channel_name().to_string(),
|
||||
redis_kiss::decode_payload::<EventV1>(&item),
|
||||
)
|
||||
}) {
|
||||
Some((channel, item)) => {
|
||||
if let Ok(mut event) = item {
|
||||
if state
|
||||
.handle_incoming_event_v1(
|
||||
db, &mut event,
|
||||
)
|
||||
.await
|
||||
&& write.lock().await
|
||||
.send(config.encode(&event))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
warn!("Failed to deserialise an event for {channel}!");
|
||||
}
|
||||
}
|
||||
// No more data, assume we disconnected or otherwise
|
||||
// something bad occurred, so disconnect user.
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker =
|
||||
async {
|
||||
while let Ok(Some(msg)) = read.try_next().await {
|
||||
if let Ok(payload) = config.decode(&msg) {
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(
|
||||
&EventV1::Pong { data },
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
|
||||
// Wait for either disconnect or for listener to die.
|
||||
select!(
|
||||
() = listener => {},
|
||||
() = worker => {}
|
||||
);
|
||||
|
||||
// * Combine the streams back once we are ready to disconnect.
|
||||
/* ws = read.reunite(write).unwrap(); */
|
||||
}
|
||||
|
||||
// Clean up presence session.
|
||||
let last_session = 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();
|
||||
}
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Disconnect the WebSocket if it isn't already.
|
||||
/*ws.close(Some(CloseFrame {
|
||||
code: CloseCode::Normal,
|
||||
reason: std::borrow::Cow::from(""),
|
||||
}))
|
||||
.await
|
||||
.unwrap();*/
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
info!("User disconnected from {addr:?}");
|
||||
});
|
||||
let t1 = message_rx.recv().fuse();
|
||||
let t2 = topic_signal_r.recv().fuse();
|
||||
let t3 = kill_signal_r.recv().fuse();
|
||||
let t4 = clean_up_r.recv().fuse();
|
||||
|
||||
pin_mut!(t1, t2, t3, t4);
|
||||
|
||||
select! {
|
||||
_ = t4 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t3 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t2 => {},
|
||||
message = t1 => {
|
||||
// Handle incoming events.
|
||||
let message = match message {
|
||||
Ok(message) => message,
|
||||
Err(e) => {
|
||||
error!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
break 'out;
|
||||
}
|
||||
};
|
||||
|
||||
let event = match *REDIS_PAYLOAD_TYPE {
|
||||
PayloadType::Json => message
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
|
||||
PayloadType::Msgpack => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
|
||||
PayloadType::Bincode => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
|
||||
};
|
||||
|
||||
let Some(mut event) = event else {
|
||||
let err = format!(
|
||||
"Failed to deserialise an event for {}! Introspection: `{:?}`",
|
||||
message.channel,
|
||||
message
|
||||
.value
|
||||
.as_string()
|
||||
.map(|x| x.chars().take(32).collect::<String>())
|
||||
);
|
||||
|
||||
error!("{}", err);
|
||||
sentry::capture_message(&err, Level::Error);
|
||||
break 'out;
|
||||
};
|
||||
|
||||
if let EventV1::Auth(auth) = &event {
|
||||
if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
|
||||
if &state.session_id == session_id {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else if let AuthifierEvent::DeleteAllSessions {
|
||||
exclude_session_id, ..
|
||||
} = auth
|
||||
{
|
||||
if let Some(excluded) = exclude_session_id {
|
||||
if &state.session_id != excluded {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let result = write.lock().await.send(config.encode(&event)).await;
|
||||
if let Err(e) = result {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while sending an event to {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
break 'out;
|
||||
}
|
||||
|
||||
if let EventV1::Logout = event {
|
||||
info!("User {addr:?} received log out event!");
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = subscriber.quit().await {
|
||||
error!("{}", err);
|
||||
sentry::capture_error(&err);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker_with_kill_signal(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
worker(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id,
|
||||
config,
|
||||
topic_signal_s,
|
||||
kill_signal_r,
|
||||
read,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
loop {
|
||||
let t1 = read.try_next().fuse();
|
||||
let t2 = kill_signal_r.recv().fuse();
|
||||
|
||||
pin_mut!(t1, t2);
|
||||
|
||||
select! {
|
||||
_ = t2 => {
|
||||
return;
|
||||
},
|
||||
result = t1 => {
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => {
|
||||
warn!("Received a None message!");
|
||||
sentry::capture_message("Received a None message!", Level::Warning);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while reading an event from {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Subscribe { server_id } => {
|
||||
let mut servers = active_servers.lock().await;
|
||||
let has_item = servers.contains_key(&server_id);
|
||||
servers.insert(server_id, ());
|
||||
|
||||
if !has_item {
|
||||
// Poke the listener to adjust subscriptions
|
||||
topic_signal_s.send(()).await.ok();
|
||||
}
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Pong { data }))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
crates/core/config/Cargo.toml
Normal file
34
crates/core/config/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.7.14"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Configuration"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
test = ["async-std"]
|
||||
default = ["test"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
dotenv = "0.15.0"
|
||||
config = "0.13.3"
|
||||
cached = "0.44.0"
|
||||
once_cell = "1.18.0"
|
||||
|
||||
# Serde
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
# Async
|
||||
futures-locks = "0.7.1"
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
|
||||
# Logging
|
||||
log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
# Sentry
|
||||
sentry = "0.31.5"
|
||||
9
crates/core/config/LICENSE
Normal file
9
crates/core/config/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
3
crates/core/config/Revolt.test.toml
Normal file
3
crates/core/config/Revolt.test.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost/"
|
||||
97
crates/core/config/Revolt.toml
Normal file
97
crates/core/config/Revolt.toml
Normal file
@@ -0,0 +1,97 @@
|
||||
[database]
|
||||
mongodb = "mongodb://database"
|
||||
redis = "redis://redis/"
|
||||
|
||||
[hosts]
|
||||
app = "http://local.revolt.chat"
|
||||
api = "http://local.revolt.chat/api"
|
||||
events = "ws://local.revolt.chat/ws"
|
||||
autumn = "http://local.revolt.chat/autumn"
|
||||
january = "http://local.revolt.chat/january"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
[api]
|
||||
|
||||
[api.registration]
|
||||
invite_only = false
|
||||
|
||||
[api.smtp]
|
||||
host = ""
|
||||
username = ""
|
||||
password = ""
|
||||
from_address = ""
|
||||
|
||||
[api.vapid]
|
||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
||||
|
||||
[api.fcm]
|
||||
api_key = ""
|
||||
|
||||
[api.apn]
|
||||
pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
[api.security]
|
||||
authifier_shield_key = ""
|
||||
voso_legacy_token = ""
|
||||
trust_cloudflare = false
|
||||
|
||||
[api.security.captcha]
|
||||
hcaptcha_key = ""
|
||||
hcaptcha_sitekey = ""
|
||||
|
||||
[api.workers]
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[features]
|
||||
webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.global]
|
||||
group_size = 100
|
||||
message_embeds = 5
|
||||
message_replies = 5
|
||||
message_reactions = 20
|
||||
server_emoji = 100
|
||||
server_roles = 200
|
||||
server_channels = 200
|
||||
|
||||
new_user_days = 3
|
||||
|
||||
[features.limits.new_user]
|
||||
outgoing_friend_requests = 5
|
||||
|
||||
bots = 2
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20000000
|
||||
avatar_size = 4000000
|
||||
background_size = 6000000
|
||||
icon_size = 2500000
|
||||
banner_size = 6000000
|
||||
emoji_size = 500000
|
||||
|
||||
[features.limits.default]
|
||||
outgoing_friend_requests = 10
|
||||
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20000000
|
||||
avatar_size = 4000000
|
||||
background_size = 6000000
|
||||
icon_size = 2500000
|
||||
banner_size = 6000000
|
||||
emoji_size = 500000
|
||||
|
||||
[sentry]
|
||||
api = ""
|
||||
events = ""
|
||||
268
crates/core/config/src/lib.rs
Normal file
268
crates/core/config/src/lib.rs
Normal file
@@ -0,0 +1,268 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
use config::{Config, File, FileFormat};
|
||||
use futures_locks::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub use sentry::capture_error;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::env;
|
||||
|
||||
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
RwLock::new({
|
||||
let mut builder = Config::builder().add_source(File::from_str(
|
||||
include_str!("../Revolt.toml"),
|
||||
FileFormat::Toml,
|
||||
));
|
||||
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
builder = builder.add_source(File::from_str(
|
||||
include_str!("../Revolt.test.toml"),
|
||||
FileFormat::Toml,
|
||||
));
|
||||
} else if std::path::Path::new("Revolt.toml").exists() {
|
||||
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
|
||||
}
|
||||
|
||||
builder.build().unwrap()
|
||||
})
|
||||
});
|
||||
|
||||
// https://gifbox.me/view/gT5mqxYKCZv-twilight-meow
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Database {
|
||||
pub mongodb: String,
|
||||
pub redis: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Hosts {
|
||||
pub app: String,
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
pub autumn: String,
|
||||
pub january: String,
|
||||
pub voso_legacy: String,
|
||||
pub voso_legacy_ws: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiRegistration {
|
||||
pub invite_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSmtp {
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub from_address: String,
|
||||
pub reply_to: Option<String>,
|
||||
pub port: Option<i32>,
|
||||
pub use_tls: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiVapid {
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiFcm {
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiApn {
|
||||
pub pkcs8: String,
|
||||
pub key_id: String,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurityCaptcha {
|
||||
pub hcaptcha_key: String,
|
||||
pub hcaptcha_sitekey: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurity {
|
||||
pub authifier_shield_key: String,
|
||||
pub voso_legacy_token: String,
|
||||
pub captcha: ApiSecurityCaptcha,
|
||||
pub trust_cloudflare: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiWorkers {
|
||||
pub max_concurrent_connections: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Api {
|
||||
pub registration: ApiRegistration,
|
||||
pub smtp: ApiSmtp,
|
||||
pub vapid: ApiVapid,
|
||||
pub fcm: ApiFcm,
|
||||
pub apn: ApiApn,
|
||||
pub security: ApiSecurity,
|
||||
pub workers: ApiWorkers,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct GlobalLimits {
|
||||
pub group_size: usize,
|
||||
pub message_embeds: usize,
|
||||
pub message_replies: usize,
|
||||
pub message_reactions: usize,
|
||||
pub server_emoji: usize,
|
||||
pub server_roles: usize,
|
||||
pub server_channels: usize,
|
||||
|
||||
pub new_user_days: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimits {
|
||||
pub outgoing_friend_requests: usize,
|
||||
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
|
||||
pub attachment_size: usize,
|
||||
pub avatar_size: usize,
|
||||
pub background_size: usize,
|
||||
pub icon_size: usize,
|
||||
pub banner_size: usize,
|
||||
pub emoji_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimitsCollection {
|
||||
pub global: GlobalLimits,
|
||||
|
||||
pub new_user: FeaturesLimits,
|
||||
pub default: FeaturesLimits,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub roles: HashMap<String, FeaturesLimits>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Features {
|
||||
pub limits: FeaturesLimitsCollection,
|
||||
pub webhooks_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Sentry {
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Settings {
|
||||
pub database: Database,
|
||||
pub hosts: Hosts,
|
||||
pub api: Api,
|
||||
pub features: Features,
|
||||
pub sentry: Sentry,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn preflight_checks(&self) {
|
||||
if self.api.smtp.host.is_empty() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
|
||||
panic!("Running in production without email is not recommended, set REVOLT_UNSAFE_NO_EMAIL=1 to override.");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
log::warn!("No SMTP settings specified! Remember to configure email.");
|
||||
}
|
||||
|
||||
if self.api.security.captcha.hcaptcha_key.is_empty() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
|
||||
panic!("Running in production without CAPTCHA is not recommended, set REVOLT_UNSAFE_NO_CAPTCHA=1 to override.");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init() {
|
||||
println!(
|
||||
":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m",
|
||||
config().await
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn read() -> Config {
|
||||
CONFIG_BUILDER.read().await.clone()
|
||||
}
|
||||
|
||||
#[cached(time = 30)]
|
||||
pub async fn config() -> Settings {
|
||||
read().await.try_deserialize::<Settings>().unwrap()
|
||||
}
|
||||
|
||||
/// Configure logging and common Rust variables
|
||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
if std::env::var("ROCKET_ADDRESS").is_err() {
|
||||
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
|
||||
}
|
||||
|
||||
pretty_env_logger::init();
|
||||
log::info!("Starting {release}");
|
||||
|
||||
if dsn.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(sentry::init((
|
||||
dsn,
|
||||
sentry::ClientOptions {
|
||||
release: Some(release.into()),
|
||||
..Default::default()
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! configure {
|
||||
($application: ident) => {
|
||||
let config = $crate::config().await;
|
||||
let _sentry = $crate::setup_logging(
|
||||
concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")),
|
||||
config.sentry.$application,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::init;
|
||||
|
||||
#[async_std::test]
|
||||
async fn it_works() {
|
||||
init().await;
|
||||
}
|
||||
}
|
||||
95
crates/core/database/Cargo.toml
Normal file
95
crates/core/database/Cargo.toml
Normal file
@@ -0,0 +1,95 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.7.14"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Database Implementation"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
# Databases
|
||||
mongodb = ["dep:mongodb", "bson"]
|
||||
|
||||
# ... Other
|
||||
tasks = ["isahc", "linkify", "url-escape"]
|
||||
async-std-runtime = ["async-std"]
|
||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||
|
||||
# Default Features
|
||||
default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.7.14", path = "../config" }
|
||||
revolt-result = { version = "0.7.14", path = "../result" }
|
||||
revolt-models = { version = "0.7.14", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.14", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.14", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
|
||||
# Utility
|
||||
log = "0.4"
|
||||
lru = "0.11.0"
|
||||
rand = "0.8.5"
|
||||
ulid = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
base64 = "0.21.3"
|
||||
once_cell = "1.17"
|
||||
indexmap = "1.9.1"
|
||||
decancer = "1.6.2"
|
||||
deadqueue = "0.2.4"
|
||||
linkify = { optional = true, version = "0.8.1" }
|
||||
url-escape = { optional = true, version = "0.1.1" }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
isahc = { optional = true, version = "1.7", features = ["json"] }
|
||||
|
||||
# Serialisation
|
||||
serde_json = "1"
|
||||
revolt_optional_struct = "0.2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
|
||||
|
||||
# Events
|
||||
redis-kiss = { version = "0.1.4" }
|
||||
|
||||
# Database
|
||||
bson = { optional = true, version = "2.1.0" }
|
||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
||||
|
||||
# Database Migration
|
||||
unicode-segmentation = "1.10.1"
|
||||
regex = "1"
|
||||
|
||||
# Async Language Features
|
||||
futures = "0.3.19"
|
||||
async-lock = "2.8.0"
|
||||
async-trait = "0.1.51"
|
||||
async-recursion = "1.0.4"
|
||||
|
||||
# Async
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
|
||||
# Rocket Impl
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
|
||||
"json",
|
||||
], optional = true }
|
||||
revolt_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
||||
|
||||
# Notifications
|
||||
fcm = "0.9.2"
|
||||
web-push = "0.10.0"
|
||||
revolt_a2 = { version = "0.10.0", default-features = false, features = [
|
||||
"ring",
|
||||
] }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0.8" }
|
||||
28
crates/core/database/fixtures/group_with_members.json
Normal file
28
crates/core/database/fixtures/group_with_members.json
Normal file
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Member",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "Member",
|
||||
"discriminator": "0002"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "Group",
|
||||
"name": "My Group",
|
||||
"owner": "__ID:0__",
|
||||
"recipients": ["__ID:0__", "__ID:1__"]
|
||||
}
|
||||
]
|
||||
89
crates/core/database/fixtures/server_with_roles.json
Normal file
89
crates/core/database/fixtures/server_with_roles.json
Normal file
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Moderator",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "User",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "TextChannel",
|
||||
"name": "General",
|
||||
"server": "__ID:4__",
|
||||
"default_permissions": {
|
||||
"a": 0,
|
||||
"d": 1048576
|
||||
},
|
||||
"role_permissions": {
|
||||
"__ID:5__": {
|
||||
"a": 1048576,
|
||||
"d": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_object_type": "Server",
|
||||
"_id": "__ID:4__",
|
||||
"owner": "__ID:0__",
|
||||
"name": "Server",
|
||||
"channels": ["__ID:3__"],
|
||||
"roles": {
|
||||
"__ID:5__": {
|
||||
"name": "Moderator",
|
||||
"permissions": {
|
||||
"a": 545270208,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 3
|
||||
},
|
||||
"__ID:6__": {
|
||||
"name": "Owner",
|
||||
"permissions": {
|
||||
"a": 0,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 0
|
||||
}
|
||||
},
|
||||
"default_permissions": 4000322560
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:0__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": ["__ID:6__"],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:1__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": ["__ID:5__"],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:2__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"joined_at": 1698318340195
|
||||
}
|
||||
]
|
||||
89
crates/core/database/src/drivers/mod.rs
Normal file
89
crates/core/database/src/drivers/mod.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
|
||||
pub use self::mongodb::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
/// Database information to use to create a client
|
||||
pub enum DatabaseInfo {
|
||||
/// Auto-detect the database in use
|
||||
Auto,
|
||||
/// Auto-detect the database in use and create an empty testing database
|
||||
Test(String),
|
||||
/// Use the mock database
|
||||
Reference,
|
||||
/// Connect to MongoDB
|
||||
MongoDb { uri: String, database_name: String },
|
||||
/// Use existing MongoDB connection
|
||||
MongoDbFromClient(::mongodb::Client, String),
|
||||
}
|
||||
|
||||
/// Database
|
||||
#[derive(Clone)]
|
||||
pub enum Database {
|
||||
/// Mock database
|
||||
Reference(ReferenceDb),
|
||||
/// MongoDB database
|
||||
MongoDb(MongoDb),
|
||||
}
|
||||
|
||||
impl DatabaseInfo {
|
||||
/// Create a database client from the given database information
|
||||
#[async_recursion]
|
||||
pub async fn connect(self) -> Result<Database, String> {
|
||||
let config = config().await;
|
||||
|
||||
Ok(match self {
|
||||
DatabaseInfo::Auto => {
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
DatabaseInfo::Test(format!(
|
||||
"revolt_test_{}",
|
||||
rand::thread_rng().gen_range(1_000_000..10_000_000)
|
||||
))
|
||||
.connect()
|
||||
.await?
|
||||
} else if !config.database.mongodb.is_empty() {
|
||||
DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name: "revolt".to_string(),
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
} else {
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Test(database_name) => {
|
||||
match std::env::var("TEST_DB")
|
||||
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
|
||||
.as_str()
|
||||
{
|
||||
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
|
||||
"MONGODB" => {
|
||||
DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name,
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
}
|
||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Reference => Database::Reference(Default::default()),
|
||||
DatabaseInfo::MongoDb { uri, database_name } => {
|
||||
let client = ::mongodb::Client::with_uri_str(uri)
|
||||
.await
|
||||
.map_err(|_| "Failed to init db connection.".to_string())?;
|
||||
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
}
|
||||
DatabaseInfo::MongoDbFromClient(client, database_name) => {
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,19 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
|
||||
use bson::{to_document, Document};
|
||||
use futures::StreamExt;
|
||||
use mongodb::{
|
||||
options::{FindOneOptions, FindOptions},
|
||||
results::{DeleteResult, InsertOneResult, UpdateResult},
|
||||
};
|
||||
use rocket::serde::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use mongodb::bson::{doc, to_document, Document};
|
||||
use mongodb::error::Result;
|
||||
use mongodb::options::{FindOneOptions, FindOptions};
|
||||
use mongodb::results::{DeleteResult, InsertOneResult, UpdateResult};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result};
|
||||
|
||||
pub mod admin {
|
||||
pub mod migrations;
|
||||
}
|
||||
|
||||
pub mod media {
|
||||
pub mod attachment;
|
||||
pub mod emoji;
|
||||
}
|
||||
|
||||
pub mod channels {
|
||||
pub mod channel;
|
||||
pub mod channel_invite;
|
||||
pub mod channel_unread;
|
||||
pub mod message;
|
||||
}
|
||||
|
||||
pub mod servers {
|
||||
pub mod server;
|
||||
pub mod server_ban;
|
||||
pub mod server_member;
|
||||
}
|
||||
|
||||
pub mod users {
|
||||
pub mod bot;
|
||||
pub mod user;
|
||||
pub mod user_settings;
|
||||
}
|
||||
|
||||
pub mod safety {
|
||||
pub mod report;
|
||||
pub mod snapshot;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MongoDb(pub mongodb::Client);
|
||||
|
||||
impl AbstractDatabase for MongoDb {}
|
||||
database_derived!(
|
||||
#[cfg(feature = "mongodb")]
|
||||
/// MongoDB implementation
|
||||
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
||||
);
|
||||
|
||||
impl Deref for MongoDb {
|
||||
type Target = mongodb::Client;
|
||||
@@ -57,30 +23,40 @@ impl Deref for MongoDb {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl MongoDb {
|
||||
/// Get the Revolt database
|
||||
pub fn db(&self) -> mongodb::Database {
|
||||
self.database("revolt")
|
||||
self.database(&self.1)
|
||||
}
|
||||
|
||||
/// Get a collection by its name
|
||||
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
|
||||
self.db().collection(collection)
|
||||
}
|
||||
|
||||
async fn insert_one<T: Serialize>(
|
||||
/// Insert one document into a collection
|
||||
pub async fn insert_one<T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
document: T,
|
||||
) -> Result<InsertOneResult> {
|
||||
self.col::<T>(collection)
|
||||
.insert_one(document, None)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "insert_one",
|
||||
with: collection,
|
||||
})
|
||||
self.col::<T>(collection).insert_one(document, None).await
|
||||
}
|
||||
|
||||
async fn find_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
/// Count documents by projection
|
||||
pub async fn count_documents(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<u64> {
|
||||
self.col::<Document>(collection)
|
||||
.count_documents(projection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find multiple documents in a collection with options
|
||||
pub async fn find_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
@@ -92,17 +68,21 @@ impl MongoDb {
|
||||
Ok(self
|
||||
.col::<T>(collection)
|
||||
.find(projection, options)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "find",
|
||||
with: collection,
|
||||
})?
|
||||
.filter_map(|s| async { s.ok() })
|
||||
.await?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
// Hard fail on invalid documents
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<T>>()
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
/// Find multiple documents in a collection
|
||||
pub async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
@@ -110,39 +90,37 @@ impl MongoDb {
|
||||
self.find_with_options(collection, projection, None).await
|
||||
}
|
||||
|
||||
async fn find_one_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
/// Find one document with options
|
||||
pub async fn find_one_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
options: O,
|
||||
) -> Result<T>
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
O: Into<Option<FindOneOptions>>,
|
||||
{
|
||||
self.col::<T>(collection)
|
||||
.find_one(projection, options)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "find_one",
|
||||
with: collection,
|
||||
})?
|
||||
.ok_or(Error::NotFound)
|
||||
}
|
||||
|
||||
async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
/// Find one document
|
||||
pub async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<T> {
|
||||
) -> Result<Option<T>> {
|
||||
self.find_one_with_options(collection, projection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn find_one_by_id<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
/// Find one document by its ID
|
||||
pub async fn find_one_by_id<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
) -> Result<T> {
|
||||
) -> Result<Option<T>> {
|
||||
self.find_one(
|
||||
collection,
|
||||
doc! {
|
||||
@@ -152,7 +130,8 @@ impl MongoDb {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn update_one<P, T: Serialize>(
|
||||
/// Update one document given a projection, partial document, and list of paths to unset
|
||||
pub async fn update_one<P, T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
@@ -182,22 +161,16 @@ impl MongoDb {
|
||||
to_document(&prefix_keys(&partial, prefix))
|
||||
} else {
|
||||
to_document(&partial)
|
||||
}.map_err(|_| Error::DatabaseError {
|
||||
operation: "to_document",
|
||||
with: collection
|
||||
})?
|
||||
}?
|
||||
};
|
||||
|
||||
self.col::<Document>(collection)
|
||||
.update_one(projection, query, None)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: collection,
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_one_by_id<P, T: Serialize>(
|
||||
/// Update one document given an ID, partial document, and list of paths to unset
|
||||
pub async fn update_one_by_id<P, T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
@@ -220,7 +193,8 @@ impl MongoDb {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_one(
|
||||
/// Delete one document by the given projection
|
||||
pub async fn delete_one(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
@@ -228,13 +202,14 @@ impl MongoDb {
|
||||
self.col::<Document>(collection)
|
||||
.delete_one(projection, None)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "delete_one",
|
||||
with: collection,
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_one_by_id(&self, collection: &'static str, id: &str) -> Result<DeleteResult> {
|
||||
/// Delete one document by the given ID
|
||||
pub async fn delete_one_by_id(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
) -> Result<DeleteResult> {
|
||||
self.delete_one(
|
||||
collection,
|
||||
doc! {
|
||||
@@ -245,6 +220,7 @@ impl MongoDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Just a string ID struct
|
||||
#[derive(Deserialize)]
|
||||
pub struct DocumentId {
|
||||
#[serde(rename = "_id")]
|
||||
@@ -252,5 +228,16 @@ pub struct DocumentId {
|
||||
}
|
||||
|
||||
pub trait IntoDocumentPath: Send + Sync {
|
||||
/// Create JSON key path
|
||||
fn as_path(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
/// Prefix keys on an arbitrary object
|
||||
pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_json::Value> {
|
||||
let v: String = serde_json::to_string(t).unwrap();
|
||||
let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap();
|
||||
v.into_iter()
|
||||
.filter(|(_k, v)| !v.is_null())
|
||||
.map(|(k, v)| (format!("{}{}", prefix.to_owned(), k), v))
|
||||
.collect()
|
||||
}
|
||||
32
crates/core/database/src/drivers/reference.rs
Normal file
32
crates/core/database/src/drivers/reference.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
|
||||
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default)]
|
||||
pub struct ReferenceDb {
|
||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
|
||||
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
|
||||
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
|
||||
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
|
||||
pub files: Arc<Mutex<HashMap<String, File>>>,
|
||||
pub messages: Arc<Mutex<HashMap<String, Message>>>,
|
||||
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
|
||||
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
||||
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
|
||||
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
|
||||
pub servers: Arc<Mutex<HashMap<String, Server>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
|
||||
}
|
||||
);
|
||||
@@ -1,13 +1,15 @@
|
||||
use authifier::AuthifierEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::channel::{FieldsChannel, PartialChannel};
|
||||
use crate::models::message::{AppendMessage, PartialMessage};
|
||||
use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer};
|
||||
use crate::models::server_member::{FieldsMember, MemberCompositeKey, PartialMember};
|
||||
use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus};
|
||||
use crate::models::{Channel, Emoji, Member, Message, Report, Server, User, UserSettings};
|
||||
use crate::Error;
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
|
||||
FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, RemovalIntention,
|
||||
Report, Server, User, UserSettings, Webhook,
|
||||
};
|
||||
use revolt_result::Error;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
/// WebSocket Client Errors
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
@@ -46,19 +48,19 @@ pub enum EventV1 {
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
|
||||
/// Logged out
|
||||
Logout,
|
||||
/// Basic data to cache
|
||||
Ready {
|
||||
users: Vec<User>,
|
||||
servers: Vec<Server>,
|
||||
channels: Vec<Channel>,
|
||||
members: Vec<Member>,
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
emojis: Vec<Emoji>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping },
|
||||
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
@@ -105,6 +107,83 @@ pub enum EventV1 {
|
||||
/// Bulk delete messages
|
||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
||||
|
||||
/// New server
|
||||
ServerCreate {
|
||||
id: String,
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
emojis: Vec<Emoji>,
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
ServerUpdate {
|
||||
id: String,
|
||||
data: PartialServer,
|
||||
clear: Vec<FieldsServer>,
|
||||
},
|
||||
|
||||
/// Delete server
|
||||
ServerDelete { id: String },
|
||||
|
||||
/// Update existing server member
|
||||
ServerMemberUpdate {
|
||||
id: MemberCompositeKey,
|
||||
data: PartialMember,
|
||||
clear: Vec<FieldsMember>,
|
||||
},
|
||||
|
||||
/// User joins server
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
reason: RemovalIntention,
|
||||
},
|
||||
|
||||
/// Server role created or updated
|
||||
ServerRoleUpdate {
|
||||
id: String,
|
||||
role_id: String,
|
||||
data: PartialRole,
|
||||
clear: Vec<FieldsRole>,
|
||||
},
|
||||
|
||||
/// Server role deleted
|
||||
ServerRoleDelete { id: String, role_id: String },
|
||||
|
||||
/// Update existing user
|
||||
UserUpdate {
|
||||
id: String,
|
||||
data: PartialUser,
|
||||
clear: Vec<FieldsUser>,
|
||||
event_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Relationship with another user changed
|
||||
UserRelationship { id: String, user: User },
|
||||
/// Settings updated remotely
|
||||
UserSettingsUpdate { id: String, update: UserSettings },
|
||||
|
||||
/// User has been platform banned or deleted their account
|
||||
///
|
||||
/// Clients should remove the following associated data:
|
||||
/// - Messages
|
||||
/// - DM Channels
|
||||
/// - Relationships
|
||||
/// - Server Memberships
|
||||
///
|
||||
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
||||
UserPlatformWipe { user_id: String, flags: i32 },
|
||||
/// New emoji
|
||||
EmojiCreate(Emoji),
|
||||
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
|
||||
/// New report
|
||||
ReportCreate(Report),
|
||||
/// New channel
|
||||
ChannelCreate(Channel),
|
||||
|
||||
@@ -137,85 +216,60 @@ pub enum EventV1 {
|
||||
message_id: String,
|
||||
},
|
||||
|
||||
/// New server
|
||||
ServerCreate {
|
||||
/// New webhook
|
||||
WebhookCreate(Webhook),
|
||||
|
||||
/// Update existing webhook
|
||||
WebhookUpdate {
|
||||
id: String,
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
data: PartialWebhook,
|
||||
remove: Vec<FieldsWebhook>,
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
ServerUpdate {
|
||||
id: String,
|
||||
data: PartialServer,
|
||||
clear: Vec<FieldsServer>,
|
||||
},
|
||||
|
||||
/// Delete server
|
||||
ServerDelete { id: String },
|
||||
|
||||
/// Update existing server member
|
||||
ServerMemberUpdate {
|
||||
id: MemberCompositeKey,
|
||||
data: PartialMember,
|
||||
clear: Vec<FieldsMember>,
|
||||
},
|
||||
|
||||
/// User joins server
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave { id: String, user: String },
|
||||
|
||||
/// Server role created or updated
|
||||
ServerRoleUpdate {
|
||||
id: String,
|
||||
role_id: String,
|
||||
data: PartialRole,
|
||||
clear: Vec<FieldsRole>,
|
||||
},
|
||||
|
||||
/// Server role deleted
|
||||
ServerRoleDelete { id: String, role_id: String },
|
||||
|
||||
/// Update existing user
|
||||
UserUpdate {
|
||||
id: String,
|
||||
data: PartialUser,
|
||||
clear: Vec<FieldsUser>,
|
||||
},
|
||||
|
||||
/// Relationship with another user changed
|
||||
UserRelationship {
|
||||
id: String,
|
||||
user: User,
|
||||
// ! this field can be deprecated
|
||||
status: RelationshipStatus,
|
||||
},
|
||||
|
||||
/// Settings updated remotely
|
||||
UserSettingsUpdate { id: String, update: UserSettings },
|
||||
|
||||
/// User has been platform banned or deleted their account
|
||||
///
|
||||
/// Clients should remove the following associated data:
|
||||
/// - Messages
|
||||
/// - DM Channels
|
||||
/// - Relationships
|
||||
/// - Server Memberships
|
||||
///
|
||||
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
||||
UserPlatformWipe { user_id: String, flags: i32 },
|
||||
|
||||
/// New emoji
|
||||
EmojiCreate(Emoji),
|
||||
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
|
||||
/// New report
|
||||
ReportCreate(Report),
|
||||
/// Delete webhook
|
||||
WebhookDelete { id: String },
|
||||
|
||||
/// Auth events
|
||||
Auth(AuthifierEvent),
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
/// Publish helper wrapper
|
||||
pub async fn p(self, channel: String) {
|
||||
#[cfg(not(debug_assertions))]
|
||||
redis_kiss::p(channel, self).await;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("Publishing event to {channel}: {self:?}");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
redis_kiss::publish(channel, self).await.unwrap();
|
||||
}
|
||||
|
||||
/// Publish user event
|
||||
pub async fn p_user(self, id: String, db: &Database) {
|
||||
self.clone().p(id.clone()).await;
|
||||
|
||||
// TODO: this should be captured by member list in the future and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().server(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish private event
|
||||
pub async fn private(self, id: String) {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish server member event
|
||||
pub async fn server(self, id: String) {
|
||||
self.p(format!("{id}u")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
pub mod client;
|
||||
pub mod r#impl;
|
||||
pub mod server;
|
||||
pub mod state;
|
||||
@@ -8,5 +8,6 @@ pub enum ClientMessage {
|
||||
Authenticate { token: String },
|
||||
BeginTyping { channel: String },
|
||||
EndTyping { channel: String },
|
||||
Subscribe { server_id: String },
|
||||
Ping { data: Ping, responded: Option<()> },
|
||||
}
|
||||
89
crates/core/database/src/lib.rs
Normal file
89
crates/core/database/src/lib.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_recursion;
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[macro_use]
|
||||
extern crate revolt_optional_struct;
|
||||
|
||||
#[macro_use]
|
||||
extern crate revolt_result;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use mongodb;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
#[macro_use]
|
||||
extern crate bson;
|
||||
|
||||
macro_rules! database_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
#[derive(Clone)]
|
||||
$item
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! auto_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
$item
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! auto_derived_partial {
|
||||
( $item:item, $name:expr ) => {
|
||||
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[optional_name = $name]
|
||||
#[opt_skip_serializing_none]
|
||||
#[opt_some_priority]
|
||||
$item
|
||||
};
|
||||
}
|
||||
|
||||
mod drivers;
|
||||
pub use drivers::*;
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! database_test {
|
||||
( | $db: ident | $test:expr ) => {
|
||||
let db = $crate::DatabaseInfo::Test(format!(
|
||||
"{}:{}",
|
||||
file!().replace('/', "_").replace(".rs", ""),
|
||||
line!()
|
||||
))
|
||||
.connect()
|
||||
.await
|
||||
.expect("Database connection failed.");
|
||||
|
||||
db.drop_database().await;
|
||||
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
(|$db: $crate::Database| $test)(db.clone()).await;
|
||||
|
||||
db.drop_database().await
|
||||
};
|
||||
}
|
||||
|
||||
mod models;
|
||||
pub mod util;
|
||||
pub use models::*;
|
||||
|
||||
pub mod events;
|
||||
pub mod tasks;
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
}
|
||||
5
crates/core/database/src/models/admin_migrations/mod.rs
Normal file
5
crates/core/database/src/models/admin_migrations/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
24
crates/core/database/src/models/admin_migrations/model.rs
Normal file
24
crates/core/database/src/models/admin_migrations/model.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
auto_derived!(
|
||||
/// Document representing migration information
|
||||
pub struct MigrationInfo {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: i32,
|
||||
/// Current database revision
|
||||
pub revision: i32,
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[async_std::test]
|
||||
async fn migrate() {
|
||||
database_test!(|db| async move {
|
||||
// Initialise the database
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Migrate the existing database
|
||||
db.migrate_database().await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
12
crates/core/database/src/models/admin_migrations/ops.rs
Normal file
12
crates/core/database/src/models/admin_migrations/ops.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractMigrations: Sync + Send {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self);
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()>;
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
use crate::{AbstractMigrations, Result};
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::super::MongoDb;
|
||||
use super::AbstractMigrations;
|
||||
|
||||
mod init;
|
||||
mod scripts;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractMigrations for MongoDb {
|
||||
async fn migrate_database(&self) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self) {
|
||||
self.db().drop(None).await.ok();
|
||||
}
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()> {
|
||||
info!("Migrating the database.");
|
||||
|
||||
let list = self
|
||||
@@ -15,7 +22,7 @@ impl AbstractMigrations for MongoDb {
|
||||
.await
|
||||
.expect("Failed to fetch database names.");
|
||||
|
||||
if list.iter().any(|x| x == "revolt") {
|
||||
if list.iter().any(|x| x == &self.1) {
|
||||
scripts::migrate_database(self).await;
|
||||
} else {
|
||||
init::create_database(self).await;
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::r#impl::mongo::MongoDb;
|
||||
|
||||
use super::scripts::LATEST_REVISION;
|
||||
|
||||
use mongodb::bson::doc;
|
||||
use mongodb::options::CreateCollectionOptions;
|
||||
use crate::mongodb::bson::doc;
|
||||
use crate::mongodb::options::CreateCollectionOptions;
|
||||
use crate::MongoDb;
|
||||
|
||||
pub async fn create_database(db: &MongoDb) {
|
||||
info!("Creating database.");
|
||||
@@ -45,6 +44,10 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create channel_unreads collection.");
|
||||
|
||||
db.create_collection("channel_webhooks", None)
|
||||
.await
|
||||
.expect("Failed to create channel_webhooks collection.");
|
||||
|
||||
db.create_collection("migrations", None)
|
||||
.await
|
||||
.expect("Failed to create migrations collection.");
|
||||
@@ -57,10 +60,26 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create user_settings collection.");
|
||||
|
||||
db.create_collection("safety_reports", None)
|
||||
.await
|
||||
.expect("Failed to create safety_reports collection.");
|
||||
|
||||
db.create_collection("safety_snapshots", None)
|
||||
.await
|
||||
.expect("Failed to create safety_snapshots collection.");
|
||||
|
||||
db.create_collection("safety_strikes", None)
|
||||
.await
|
||||
.expect("Failed to create safety_strikes collection.");
|
||||
|
||||
db.create_collection("bots", None)
|
||||
.await
|
||||
.expect("Failed to create bots collection.");
|
||||
|
||||
db.create_collection("ratelimit_events", None)
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events collection.");
|
||||
|
||||
db.create_collection(
|
||||
"pubsub",
|
||||
CreateCollectionOptions::builder()
|
||||
@@ -80,6 +99,18 @@ pub async fn create_database(db: &MongoDb) {
|
||||
"username": 1_i32
|
||||
},
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
@@ -103,18 +134,18 @@ pub async fn create_database(db: &MongoDb) {
|
||||
},
|
||||
"name": "content"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32
|
||||
},
|
||||
"name": "channel"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"_id": 1_i32
|
||||
},
|
||||
"name": "channel_id_compound"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -182,5 +213,24 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to save migration info.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "ratelimit_events",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id": 1_i32,
|
||||
"target_id": 1_i32,
|
||||
"event_type": 1_i32,
|
||||
},
|
||||
"name": "compound_key"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
|
||||
info!("Created database.");
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
use std::time::Duration;
|
||||
use std::{collections::HashSet, ops::BitXor, time::Duration};
|
||||
|
||||
use bson::{Bson, DateTime};
|
||||
use futures::StreamExt;
|
||||
use mongodb::{
|
||||
bson::{doc, from_bson, from_document, to_document, Document},
|
||||
options::FindOptions,
|
||||
use crate::{
|
||||
mongodb::{
|
||||
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
||||
options::FindOptions,
|
||||
},
|
||||
Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
||||
};
|
||||
use bson::oid::ObjectId;
|
||||
use futures::StreamExt;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{r#impl::mongo::MongoDb, Permission, DEFAULT_PERMISSION_SERVER};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MigrationInfo {
|
||||
@@ -16,7 +20,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 18;
|
||||
pub const LATEST_REVISION: i32 = 27;
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -503,13 +507,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
|
||||
update.insert(
|
||||
"default_permissions",
|
||||
(*DEFAULT_PERMISSION_SERVER
|
||||
// Remove Send Message permission if it wasn't originally granted
|
||||
^ (if has_send {
|
||||
0
|
||||
} else {
|
||||
Permission::SendMessage as u64
|
||||
})) as i64,
|
||||
// Remove Send Message permission if it wasn't originally granted
|
||||
(4000323584).bitxor(if has_send { 0 } else { (1 << 22) as u64 }) as i64,
|
||||
);
|
||||
|
||||
if let Some(Bson::Document(mut roles)) = document.remove("roles") {
|
||||
@@ -568,7 +567,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
doc! {
|
||||
"default_permissions": {
|
||||
"a": 0_i64,
|
||||
"d": Permission::SendMessage as i64
|
||||
"d": (1 << 22) as i64
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -661,8 +660,419 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.expect("Failed to update server members.");
|
||||
}
|
||||
|
||||
if revision <= 18 {
|
||||
info!("Running migration [revision 18 / 27-02-2022]: Create author index on messages. Drop plain channel index if exists.");
|
||||
|
||||
if db
|
||||
.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"dropIndexes": "messages",
|
||||
"index": ["channel"]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
info!("Failed to drop `messages.channel` index but this is ok since that means it's probably gone.");
|
||||
}
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "messages",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32,
|
||||
},
|
||||
"name": "author"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create messages author index.");
|
||||
}
|
||||
|
||||
if revision <= 19 {
|
||||
info!(
|
||||
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
|
||||
);
|
||||
|
||||
db.db()
|
||||
.create_collection("safety_reports", None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.db()
|
||||
.create_collection("safety_snapshots", None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if revision <= 20 {
|
||||
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "safety_snapshots",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"report_id": 1_i32
|
||||
},
|
||||
"name": "report_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create safety snapshot index.");
|
||||
}
|
||||
|
||||
if revision <= 21 {
|
||||
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
|
||||
|
||||
db.db()
|
||||
.create_collection("safety_strikes", None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if revision <= 22 {
|
||||
info!("Running migration [revision 22 / 31-05-2023]: Add moderator_id to account strikes.");
|
||||
|
||||
db.col::<Document>("safety_strikes")
|
||||
.update_many(
|
||||
doc! {},
|
||||
doc! {
|
||||
"$set": {
|
||||
"moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4"
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update server members.");
|
||||
}
|
||||
|
||||
if revision <= 23 {
|
||||
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"dropIndexes": "users",
|
||||
"index": "username"
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to drop existing username index.");
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct UserInformation {
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
username: String,
|
||||
}
|
||||
|
||||
let re_username = regex::Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap();
|
||||
|
||||
let users: Vec<UserInformation> = db
|
||||
.col::<UserInformation>("users")
|
||||
.find(doc! {}, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.map(|doc| doc.expect("id and username"))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
let search_space: Vec<String> = DISCRIMINATOR_SEARCH_SPACE.iter().cloned().collect();
|
||||
let mut claimed: HashSet<String> = HashSet::new();
|
||||
|
||||
for i in 0..users.len() {
|
||||
let info = &users[i];
|
||||
let mut discriminator = {
|
||||
let mut rng = rand::thread_rng();
|
||||
search_space.choose(&mut rng).unwrap()
|
||||
};
|
||||
|
||||
if re_username.is_match(&info.username) {
|
||||
while claimed.contains(&format!("{}#{}", info.username, discriminator)) {
|
||||
let new_discriminator = {
|
||||
let mut rng = rand::thread_rng();
|
||||
search_space.choose(&mut rng).unwrap()
|
||||
};
|
||||
|
||||
info!(
|
||||
"Re-rolled {} to {new_discriminator} from {discriminator}",
|
||||
info.username
|
||||
);
|
||||
|
||||
discriminator = new_discriminator;
|
||||
}
|
||||
|
||||
claimed.insert(format!("{}#{}", info.username, discriminator));
|
||||
|
||||
info!(
|
||||
"({}/{}) Migrating user \"{}\" to #{} - compliant",
|
||||
i + 1,
|
||||
users.len(),
|
||||
info.username,
|
||||
discriminator
|
||||
);
|
||||
|
||||
db.col::<UserInformation>("users")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &info.id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"discriminator": discriminator
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
let mut sanitised = info
|
||||
.username
|
||||
.graphemes(true)
|
||||
.filter(|s| re_username.is_match(s))
|
||||
.collect::<String>();
|
||||
|
||||
while sanitised.len() < 2 {
|
||||
sanitised += "_";
|
||||
}
|
||||
|
||||
while claimed.contains(&format!("{}#{}", sanitised, discriminator)) {
|
||||
let new_discriminator = {
|
||||
let mut rng = rand::thread_rng();
|
||||
search_space.choose(&mut rng).unwrap()
|
||||
};
|
||||
|
||||
info!("Re-rolled {sanitised} to {new_discriminator} from {discriminator}");
|
||||
discriminator = new_discriminator;
|
||||
}
|
||||
|
||||
claimed.insert(format!("{}#{}", sanitised, discriminator));
|
||||
|
||||
info!(
|
||||
"({}/{}) Migrating user \"{}\" to #{} - sanitised: \"{}\"",
|
||||
i + 1,
|
||||
users.len(),
|
||||
info.username,
|
||||
discriminator,
|
||||
sanitised
|
||||
);
|
||||
|
||||
db.col::<UserInformation>("users")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &info.id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"username": sanitised,
|
||||
"discriminator": discriminator,
|
||||
"display_name": &info.username
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if revision <= 24 {
|
||||
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
|
||||
|
||||
db.db()
|
||||
.create_collection("channel_webhooks", None)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "users",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32
|
||||
},
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create username index.");
|
||||
};
|
||||
|
||||
if revision <= 25 {
|
||||
info!("Running migration [revision 25 / 11-06-2023]: Add permissions to webhooks.");
|
||||
|
||||
db.col::<Document>("webhooks")
|
||||
.update_many(
|
||||
doc! {},
|
||||
doc! {
|
||||
"$set": {
|
||||
"permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update webhooks.");
|
||||
}
|
||||
|
||||
if revision <= 25 {
|
||||
info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index.");
|
||||
|
||||
db.db()
|
||||
.create_collection("ratelimit_events", None)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "ratelimit_events",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id": 1_i32,
|
||||
"target_id": 1_i32,
|
||||
"event_type": 1_i32,
|
||||
},
|
||||
"name": "compound_key"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
}
|
||||
|
||||
if revision <= 26 {
|
||||
info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging.");
|
||||
|
||||
auto_derived!(
|
||||
pub enum OldInvite {
|
||||
Server {
|
||||
#[serde(rename = "_id")]
|
||||
code: String,
|
||||
server: String,
|
||||
creator: String,
|
||||
channel: String,
|
||||
},
|
||||
Group {
|
||||
#[serde(rename = "_id")]
|
||||
code: String,
|
||||
creator: String,
|
||||
channel: String,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct Outer {
|
||||
_id: ObjectId,
|
||||
#[serde(flatten)]
|
||||
invite: OldInvite,
|
||||
}
|
||||
|
||||
let invites = db
|
||||
.db()
|
||||
.collection::<Outer>("channel_invites")
|
||||
.find(
|
||||
doc! {
|
||||
"type": { "$exists": false }
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("failed to find invites")
|
||||
.filter_map(|s| async { s.ok() })
|
||||
.collect::<Vec<Outer>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|invite| match invite.invite {
|
||||
OldInvite::Server {
|
||||
code,
|
||||
server,
|
||||
creator,
|
||||
channel,
|
||||
} => Invite::Server {
|
||||
code,
|
||||
server,
|
||||
creator,
|
||||
channel,
|
||||
},
|
||||
OldInvite::Group {
|
||||
code,
|
||||
creator,
|
||||
channel,
|
||||
} => Invite::Group {
|
||||
code,
|
||||
creator,
|
||||
channel,
|
||||
},
|
||||
})
|
||||
.collect::<Vec<Invite>>();
|
||||
|
||||
if !invites.is_empty() {
|
||||
db.db()
|
||||
.collection("channel_invites")
|
||||
.insert_many(invites, None)
|
||||
.await
|
||||
.expect("failed to insert corrected invite");
|
||||
|
||||
db.db()
|
||||
.collection::<Outer>("channel_invites")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"type": { "$exists": false }
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("failed to find invites");
|
||||
}
|
||||
}
|
||||
|
||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
LATEST_REVISION
|
||||
LATEST_REVISION.max(revision)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractMigrations;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractMigrations for ReferenceDb {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self) {}
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()> {
|
||||
// Here you would do your typical migrations if this was a real database.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/bots/mod.rs
Normal file
5
crates/core/database/src/models/bots/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
223
crates/core/database/src/models/bots/model.rs
Normal file
223
crates/core/database/src/models/bots/model.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Bot
|
||||
pub struct Bot {
|
||||
/// Bot Id
|
||||
///
|
||||
/// This equals the associated bot user's id.
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// User Id of the bot owner
|
||||
pub owner: String,
|
||||
/// Token used to authenticate requests for this bot
|
||||
pub token: String,
|
||||
/// Whether the bot is public
|
||||
/// (may be invited by anyone)
|
||||
pub public: bool,
|
||||
|
||||
/// Whether to enable analytics
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub analytics: bool,
|
||||
/// Whether this bot should be publicly discoverable
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub discoverable: bool,
|
||||
/// Reserved; URL for handling interactions
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub interactions_url: String,
|
||||
/// URL for terms of service
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub terms_of_service_url: String,
|
||||
/// URL for privacy policy
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub privacy_policy_url: String,
|
||||
|
||||
/// Enum of bot flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
},
|
||||
"PartialBot"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Optional fields on bot object
|
||||
pub enum FieldsBot {
|
||||
Token,
|
||||
InteractionsURL,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Bot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Default::default(),
|
||||
owner: Default::default(),
|
||||
token: Default::default(),
|
||||
public: Default::default(),
|
||||
analytics: Default::default(),
|
||||
discoverable: Default::default(),
|
||||
interactions_url: Default::default(),
|
||||
terms_of_service_url: Default::default(),
|
||||
privacy_policy_url: Default::default(),
|
||||
flags: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Bot {
|
||||
/// Create a new bot
|
||||
pub async fn create<D>(
|
||||
db: &Database,
|
||||
username: String,
|
||||
owner: &User,
|
||||
data: D,
|
||||
) -> Result<(Bot, User)>
|
||||
where
|
||||
D: Into<Option<PartialBot>>,
|
||||
{
|
||||
if owner.bot.is_some() {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
|
||||
return Err(create_error!(ReachedMaximumBots));
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
|
||||
let user = User::create(
|
||||
db,
|
||||
username,
|
||||
Some(id.to_string()),
|
||||
Some(PartialUser {
|
||||
bot: Some(BotInformation {
|
||||
owner: owner.id.to_string(),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut bot = Bot {
|
||||
id,
|
||||
owner: owner.id.to_string(),
|
||||
token: nanoid::nanoid!(64),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(data) = data.into() {
|
||||
bot.apply_options(data);
|
||||
}
|
||||
|
||||
db.insert_bot(&bot).await?;
|
||||
Ok((bot, user))
|
||||
}
|
||||
|
||||
/// Remove a field from this object
|
||||
pub fn remove_field(&mut self, field: &FieldsBot) {
|
||||
match field {
|
||||
FieldsBot::Token => self.token = nanoid::nanoid!(64),
|
||||
FieldsBot::InteractionsURL => {
|
||||
self.interactions_url = String::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update this bot
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
mut partial: PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
if remove.contains(&FieldsBot::Token) {
|
||||
partial.token = Some(nanoid::nanoid!(64));
|
||||
}
|
||||
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
db.update_bot(&self.id, &partial, remove).await?;
|
||||
|
||||
if partial.token.is_some() {
|
||||
EventV1::Logout.private(self.id.clone()).await;
|
||||
}
|
||||
|
||||
self.apply_options(partial);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete this bot
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
db.fetch_user(&self.id).await?.mark_deleted(db).await?;
|
||||
db.delete_bot(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Bot, FieldsBot, PartialBot, User};
|
||||
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let owner = User::create(&db, "Owner".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (bot, _) = Bot::create(
|
||||
&db,
|
||||
"Bot Name".to_string(),
|
||||
&owner,
|
||||
PartialBot {
|
||||
token: Some("my token".to_string()),
|
||||
interactions_url: Some("some url".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!bot.interactions_url.is_empty());
|
||||
|
||||
let mut updated_bot = bot.clone();
|
||||
updated_bot
|
||||
.update(
|
||||
&db,
|
||||
PartialBot {
|
||||
public: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
vec![FieldsBot::Token, FieldsBot::InteractionsURL],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched_bot1 = db.fetch_bot(&bot.id).await.unwrap();
|
||||
let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap();
|
||||
let fetched_bots = db.fetch_bots_by_user(&owner.id).await.unwrap();
|
||||
|
||||
assert!(!bot.public);
|
||||
assert!(fetched_bot1.public);
|
||||
assert!(!bot.interactions_url.is_empty());
|
||||
assert!(fetched_bot1.interactions_url.is_empty());
|
||||
assert_ne!(bot.token, fetched_bot1.token);
|
||||
assert_eq!(updated_bot, fetched_bot1);
|
||||
assert_eq!(fetched_bot1, fetched_bot2);
|
||||
assert_eq!(fetched_bot1, fetched_bots[0]);
|
||||
assert_eq!(1, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
|
||||
|
||||
bot.delete(&db).await.unwrap();
|
||||
assert!(db.fetch_bot(&bot.id).await.is_err());
|
||||
assert_eq!(0, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
|
||||
assert_eq!(db.fetch_user(&bot.id).await.unwrap().flags, Some(2))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,35 @@
|
||||
use crate::models::bot::{Bot, FieldsBot, PartialBot};
|
||||
use crate::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractBot: Sync + Send {
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot>;
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot>;
|
||||
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()>;
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(&self, id: &str, bot: &PartialBot, remove: Vec<FieldsBot>) -> Result<()>;
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>>;
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize>;
|
||||
}
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractBots: Sync + Send {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()>;
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot>;
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot>;
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>>;
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize>;
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
92
crates/core/database/src/models/bots/ops/mongodb.rs
Normal file
92
crates/core/database/src/models/bots/ops/mongodb.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractBots;
|
||||
|
||||
static COL: &str = "bots";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractBots for MongoDb {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
|
||||
query!(self, insert_one, COL, &bot).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"token": token
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"owner": user_id
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
|
||||
query!(
|
||||
self,
|
||||
count_documents,
|
||||
COL,
|
||||
doc! {
|
||||
"owner": user_id
|
||||
}
|
||||
)
|
||||
.map(|v| v as usize)
|
||||
}
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsBot {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
FieldsBot::InteractionsURL => Some("interactions_url"),
|
||||
FieldsBot::Token => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
82
crates/core/database/src/models/bots/ops/reference.rs
Normal file
82
crates/core/database/src/models/bots/ops/reference.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
use super::AbstractBots;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractBots for ReferenceDb {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if bots.contains_key(&bot.id) {
|
||||
Err(create_database_error!("insert", "bot"))
|
||||
} else {
|
||||
bots.insert(bot.id.to_string(), bot.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
|
||||
let bots = self.bots.lock().await;
|
||||
bots.get(id).cloned().ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
|
||||
let bots = self.bots.lock().await;
|
||||
bots.values()
|
||||
.find(|bot| bot.token == token)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
|
||||
let bots = self.bots.lock().await;
|
||||
Ok(bots
|
||||
.values()
|
||||
.filter(|bot| bot.owner == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
|
||||
let bots = self.bots.lock().await;
|
||||
Ok(bots.values().filter(|bot| bot.owner == user_id).count())
|
||||
}
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if let Some(bot) = bots.get_mut(id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
bot.remove_field(&field);
|
||||
}
|
||||
|
||||
bot.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if bots.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/channel_invites/mod.rs
Normal file
5
crates/core/database/src/models/channel_invites/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -1,18 +1,46 @@
|
||||
use nanoid::nanoid;
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use crate::{
|
||||
models::{Channel, Invite, User},
|
||||
Database, Error, Result,
|
||||
};
|
||||
use crate::{Channel, Database, User};
|
||||
|
||||
lazy_static! {
|
||||
static ref ALPHABET: [char; 54] = [
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
|
||||
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
|
||||
'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'
|
||||
];
|
||||
}
|
||||
static ALPHABET: [char; 54] = [
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
|
||||
'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
|
||||
auto_derived!(
|
||||
/// Invite
|
||||
#[serde(tag = "type")]
|
||||
pub enum Invite {
|
||||
/// Invite to a specific server channel
|
||||
Server {
|
||||
/// Invite code
|
||||
#[serde(rename = "_id")]
|
||||
code: String,
|
||||
/// Id of the server this invite points to
|
||||
server: String,
|
||||
/// Id of user who created this invite
|
||||
creator: String,
|
||||
/// Id of the server channel this invite points to
|
||||
channel: String,
|
||||
},
|
||||
/// Invite to a group channel
|
||||
Group {
|
||||
/// Invite code
|
||||
#[serde(rename = "_id")]
|
||||
code: String,
|
||||
/// Id of user who created this invite
|
||||
creator: String,
|
||||
/// Id of the group channel this invite points to
|
||||
channel: String,
|
||||
}, /* User {
|
||||
code: String,
|
||||
user: String
|
||||
} */
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Invite {
|
||||
/// Get the invite code for this invite
|
||||
pub fn code(&'_ self) -> &'_ str {
|
||||
@@ -29,9 +57,13 @@ impl Invite {
|
||||
}
|
||||
|
||||
/// Create a new invite from given information
|
||||
pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result<Invite> {
|
||||
let code = nanoid!(8, &*ALPHABET);
|
||||
let invite = match &target {
|
||||
pub async fn create_channel_invite(
|
||||
db: &Database,
|
||||
creator: &User,
|
||||
channel: &Channel,
|
||||
) -> Result<Invite> {
|
||||
let code = nanoid::nanoid!(8, &ALPHABET);
|
||||
let invite = match &channel {
|
||||
Channel::Group { id, .. } => Ok(Invite::Group {
|
||||
code,
|
||||
creator: creator.id.clone(),
|
||||
@@ -45,7 +77,7 @@ impl Invite {
|
||||
channel: id.clone(),
|
||||
})
|
||||
}
|
||||
_ => Err(Error::InvalidOperation),
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}?;
|
||||
|
||||
db.insert_invite(&invite).await?;
|
||||
@@ -69,6 +101,6 @@ impl Invite {
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::NotFound)
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
use crate::models::Invite;
|
||||
use crate::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelInvite: Sync + Send {
|
||||
/// Fetch an invite by its id
|
||||
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
|
||||
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
|
||||
|
||||
/// Delete an invite by its id
|
||||
async fn delete_invite(&self, code: &str) -> Result<()>;
|
||||
|
||||
/// Fetch all invites for a server
|
||||
async fn fetch_invites_for_server(&self, server: &str) -> Result<Vec<Invite>>;
|
||||
}
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelInvites: Sync + Send {
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
|
||||
|
||||
/// Fetch an invite by its id
|
||||
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
|
||||
|
||||
/// Fetch all invites for a server
|
||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>>;
|
||||
|
||||
/// Delete an invite by its id
|
||||
async fn delete_invite(&self, code: &str) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractChannelInvites;
|
||||
|
||||
static COL: &str = "channel_invites";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelInvites for MongoDb {
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
||||
query!(self, insert_one, COL, &invite).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch an invite by the code
|
||||
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
|
||||
query!(self, find_one_by_id, COL, code)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all invites for a server
|
||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
|
||||
Ok(self
|
||||
.col::<Invite>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"server": server_id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Delete an invite by its code
|
||||
async fn delete_invite(&self, code: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, code).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractChannelInvites;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelInvites for ReferenceDb {
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
||||
let mut invites = self.channel_invites.lock().await;
|
||||
if invites.contains_key(invite.code()) {
|
||||
Err(create_database_error!("insert", "invite"))
|
||||
} else {
|
||||
invites.insert(invite.code().to_string(), invite.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch an invite by the code
|
||||
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
|
||||
let invites = self.channel_invites.lock().await;
|
||||
invites
|
||||
.get(code)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all invites for a server
|
||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
|
||||
let invites = self.channel_invites.lock().await;
|
||||
Ok(invites
|
||||
.values()
|
||||
.filter(|invite| match invite {
|
||||
Invite::Server { server, .. } => server == server_id,
|
||||
_ => false,
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Delete an invite by its code
|
||||
async fn delete_invite(&self, code: &str) -> Result<()> {
|
||||
let mut invites = self.channel_invites.lock().await;
|
||||
if invites.remove(code).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/channel_unreads/mod.rs
Normal file
5
crates/core/database/src/models/channel_unreads/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
24
crates/core/database/src/models/channel_unreads/model.rs
Normal file
24
crates/core/database/src/models/channel_unreads/model.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
auto_derived!(
|
||||
/// Channel Unread
|
||||
pub struct ChannelUnread {
|
||||
/// Composite key pointing to a user's view of a channel
|
||||
#[serde(rename = "_id")]
|
||||
pub id: ChannelCompositeKey,
|
||||
|
||||
/// Id of the last message read in this channel by a user
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_id: Option<String>,
|
||||
/// Array of message ids that mention the user
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentions: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Composite primary key consisting of channel and user id
|
||||
#[derive(Hash)]
|
||||
pub struct ChannelCompositeKey {
|
||||
/// Channel Id
|
||||
pub channel: String,
|
||||
/// User Id
|
||||
pub user: String,
|
||||
}
|
||||
);
|
||||
31
crates/core/database/src/models/channel_unreads/ops.rs
Normal file
31
crates/core/database/src/models/channel_unreads/ops.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ChannelUnread;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelUnreads: Sync + Send {
|
||||
/// Acknowledge a message.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
|
||||
|
||||
/// Add a mention.
|
||||
async fn add_mention_to_unread<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_ids: &[String],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
||||
}
|
||||
@@ -1,69 +1,71 @@
|
||||
use bson::Document;
|
||||
use mongodb::options::UpdateOptions;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::models::channel_unread::ChannelUnread;
|
||||
use crate::{AbstractChannelUnread, Error, Result};
|
||||
use crate::ChannelUnread;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::super::MongoDb;
|
||||
use super::AbstractChannelUnreads;
|
||||
|
||||
static COL: &str = "channel_unreads";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelUnread for MongoDb {
|
||||
async fn acknowledge_message(&self, channel: &str, user: &str, message: &str) -> Result<()> {
|
||||
impl AbstractChannelUnreads for MongoDb {
|
||||
/// Acknowledge a message.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id.channel": channel,
|
||||
"_id.user": user,
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": user_id,
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
"mentions": 1_i32
|
||||
},
|
||||
"$set": {
|
||||
"last_id": message
|
||||
"last_id": message_id
|
||||
}
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "channel_unread",
|
||||
})
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> {
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
|
||||
let current_time = Ulid::new().to_string();
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.channel": {
|
||||
"$in": channels
|
||||
"$in": channel_ids
|
||||
},
|
||||
"_id.user": user
|
||||
"_id.user": user_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "delete_many",
|
||||
with: "channel_unreads",
|
||||
})?;
|
||||
.map_err(|_| create_database_error!("delete_many", COL))?;
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.insert_many(
|
||||
channels
|
||||
channel_ids
|
||||
.iter()
|
||||
.map(|channel| {
|
||||
.map(|channel_id| {
|
||||
doc! {
|
||||
"_id": {
|
||||
"channel": channel,
|
||||
"user": user
|
||||
"channel": channel_id,
|
||||
"user": user_id
|
||||
},
|
||||
"last_id": ¤t_time
|
||||
}
|
||||
@@ -72,29 +74,27 @@ impl AbstractChannelUnread for MongoDb {
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_many",
|
||||
with: "channel_unreads",
|
||||
})
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
}
|
||||
|
||||
/// Add a mention.
|
||||
async fn add_mention_to_unread<'a>(
|
||||
&self,
|
||||
channel: &str,
|
||||
user: &str,
|
||||
ids: &[String],
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_ids: &[String],
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id.channel": channel,
|
||||
"_id.user": user,
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": user_id,
|
||||
},
|
||||
doc! {
|
||||
"$push": {
|
||||
"mentions": {
|
||||
"$each": ids
|
||||
"$each": message_ids
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -102,19 +102,18 @@ impl AbstractChannelUnread for MongoDb {
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "channel_unread",
|
||||
})
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
async fn fetch_unreads(&self, user: &str) -> Result<Vec<ChannelUnread>> {
|
||||
self.find(
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.user": user
|
||||
},
|
||||
"_id.user": user_id
|
||||
}
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{ChannelCompositeKey, ChannelUnread, ReferenceDb};
|
||||
|
||||
use super::AbstractChannelUnreads;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelUnreads for ReferenceDb {
|
||||
/// Acknowledge a message.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
};
|
||||
|
||||
if let Some(unread) = unreads.get_mut(&key) {
|
||||
unread.mentions = None;
|
||||
unread.last_id.replace(message_id.to_string());
|
||||
} else {
|
||||
unreads.insert(
|
||||
key.clone(),
|
||||
ChannelUnread {
|
||||
id: key,
|
||||
last_id: Some(message_id.to_string()),
|
||||
mentions: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
|
||||
let current_time = Ulid::new().to_string();
|
||||
for channel_id in channel_ids {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
self.acknowledge_message(channel_id, user_id, ¤t_time)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a mention.
|
||||
async fn add_mention_to_unread<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_ids: &[String],
|
||||
) -> Result<()> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
};
|
||||
|
||||
if let Some(unread) = unreads.get_mut(&key) {
|
||||
unread.mentions.replace(message_ids.to_vec());
|
||||
} else {
|
||||
unreads.insert(
|
||||
key.clone(),
|
||||
ChannelUnread {
|
||||
id: key,
|
||||
last_id: None,
|
||||
mentions: Some(message_ids.to_vec()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
||||
let unreads = self.channel_unreads.lock().await;
|
||||
Ok(unreads
|
||||
.values()
|
||||
.filter(|unread| unread.id.user == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/channel_webhooks/mod.rs
Normal file
5
crates/core/database/src/models/channel_webhooks/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
175
crates/core/database/src/models/channel_webhooks/model.rs
Normal file
175
crates/core/database/src/models/channel_webhooks/model.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::events::client::EventV1;
|
||||
use crate::{Database, File};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Webhook
|
||||
pub struct Webhook {
|
||||
/// Webhook Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// The name of the webhook
|
||||
pub name: String,
|
||||
|
||||
/// The avatar of the webhook
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<File>,
|
||||
|
||||
/// The channel this webhook belongs to
|
||||
pub channel_id: String,
|
||||
|
||||
/// The permissions of the webhook
|
||||
pub permissions: u64,
|
||||
|
||||
/// The private token for the webhook
|
||||
pub token: Option<String>,
|
||||
},
|
||||
"PartialWebhook"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Optional fields on webhook object
|
||||
pub enum FieldsWebhook {
|
||||
Avatar,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Webhook {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Default::default(),
|
||||
name: Default::default(),
|
||||
avatar: None,
|
||||
channel_id: Default::default(),
|
||||
permissions: Default::default(),
|
||||
token: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Webhook {
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_webhook(self).await?;
|
||||
|
||||
// Avoid leaking the token to people who receive the event
|
||||
let mut webhook = self.clone();
|
||||
webhook.token = None;
|
||||
|
||||
EventV1::WebhookCreate(webhook.into())
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn assert_token(&self, token: &str) -> Result<()> {
|
||||
if self.token.as_deref() == Some(token) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(InvalidCredentials))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
mut partial: PartialWebhook,
|
||||
remove: Vec<FieldsWebhook>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field)
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_webhook(&self.id, &partial, &remove).await?;
|
||||
|
||||
partial.token = None; // Avoid leaking the token to people who receive the event
|
||||
|
||||
EventV1::WebhookUpdate {
|
||||
id: self.id.clone(),
|
||||
data: partial.into(),
|
||||
remove: remove.into_iter().map(|v| v.into()).collect(),
|
||||
}
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsWebhook) {
|
||||
match field {
|
||||
FieldsWebhook::Avatar => self.avatar = None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
db.delete_webhook(&self.id).await?;
|
||||
|
||||
EventV1::WebhookDelete {
|
||||
id: self.id.clone(),
|
||||
}
|
||||
.p(self.channel_id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let webhook_id = "webhook";
|
||||
let channel_id = "channel";
|
||||
|
||||
let webhook = Webhook {
|
||||
id: webhook_id.to_string(),
|
||||
name: "Webhook Name".to_string(),
|
||||
channel_id: channel_id.to_string(),
|
||||
avatar: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_webhook(&webhook).await.unwrap();
|
||||
|
||||
let mut updated_webhook = webhook.clone();
|
||||
updated_webhook
|
||||
.update(
|
||||
&db,
|
||||
PartialWebhook {
|
||||
name: Some("New Name".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![FieldsWebhook::Avatar],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched_webhook = db.fetch_webhook(webhook_id).await.unwrap();
|
||||
let fetched_webhooks = db.fetch_webhooks_for_channel(channel_id).await.unwrap();
|
||||
|
||||
assert_eq!(updated_webhook, fetched_webhook);
|
||||
assert_ne!(webhook, fetched_webhook);
|
||||
assert_eq!(1, fetched_webhooks.len());
|
||||
assert_eq!(fetched_webhook, fetched_webhooks[0]);
|
||||
|
||||
webhook.delete(&db).await.unwrap();
|
||||
assert!(db.fetch_webhook(webhook_id).await.is_err());
|
||||
assert_eq!(
|
||||
0,
|
||||
db.fetch_webhooks_for_channel(channel_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
29
crates/core/database/src/models/channel_webhooks/ops.rs
Normal file
29
crates/core/database/src/models/channel_webhooks/ops.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractWebhooks: Sync + Send {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>;
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook>;
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>>;
|
||||
|
||||
/// Update webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractWebhooks;
|
||||
|
||||
static COL: &str = "channel_webhooks";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractWebhooks for MongoDb {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
|
||||
query!(self, insert_one, COL, &webhook).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
|
||||
query!(self, find_one_by_id, COL, webhook_id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
||||
Ok(self
|
||||
.col::<Webhook>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"channel_id": channel_id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Update webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
webhook_id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, webhook_id).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsWebhook {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsWebhook::Avatar => "avatar",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
use super::AbstractWebhooks;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractWebhooks for ReferenceDb {
|
||||
/// Insert new webhook into the database
|
||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if webhooks.contains_key(&webhook.id) {
|
||||
Err(create_database_error!("insert", "webhook"))
|
||||
} else {
|
||||
webhooks.insert(webhook.id.to_string(), webhook.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch webhook by id
|
||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
|
||||
let webhooks = self.channel_webhooks.lock().await;
|
||||
webhooks
|
||||
.get(webhook_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch webhooks for channel
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
||||
let webhooks = self.channel_webhooks.lock().await;
|
||||
Ok(webhooks
|
||||
.values()
|
||||
.filter(|webhook| webhook.channel_id == channel_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Update webhook with new information
|
||||
async fn update_webhook(
|
||||
&self,
|
||||
webhook_id: &str,
|
||||
partial: &PartialWebhook,
|
||||
remove: &[FieldsWebhook],
|
||||
) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if let Some(webhook) = webhooks.get_mut(webhook_id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
webhook.remove_field(field);
|
||||
}
|
||||
|
||||
webhook.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete webhook by id
|
||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
|
||||
let mut webhooks = self.channel_webhooks.lock().await;
|
||||
if webhooks.remove(webhook_id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/channels/mod.rs
Normal file
5
crates/core/database/src/models/channels/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
826
crates/core/database/src/models/channels/model.rs
Normal file
826
crates/core/database/src/models/channels/model.rs
Normal file
@@ -0,0 +1,826 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::{self, MessageAuthor};
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
|
||||
Server, SystemMessage, User,
|
||||
};
|
||||
|
||||
auto_derived!(
|
||||
#[serde(tag = "channel_type")]
|
||||
pub enum Channel {
|
||||
/// Personal "Saved Notes" channel which allows users to save messages
|
||||
SavedMessages {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the user this channel belongs to
|
||||
user: String,
|
||||
},
|
||||
/// Direct message channel between two users
|
||||
DirectMessage {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
|
||||
/// Whether this direct message channel is currently open on both sides
|
||||
active: bool,
|
||||
/// 2-tuple of user ids participating in direct message
|
||||
recipients: Vec<String>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
},
|
||||
/// Group channel between 1 or more participants
|
||||
Group {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
/// User id of the owner of the group
|
||||
owner: String,
|
||||
/// Channel description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
/// Array of user ids participating in channel
|
||||
recipients: Vec<String>,
|
||||
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
|
||||
/// Permissions assigned to members of this group
|
||||
/// (does not apply to the owner of the group)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
permissions: Option<i64>,
|
||||
|
||||
/// Whether this group is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
/// Text channel belonging to a server
|
||||
TextChannel {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
/// Channel description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
description: Option<String>,
|
||||
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
/// Id of the last message sent in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
last_message_id: Option<String>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
/// Voice channel belonging to a server
|
||||
VoiceChannel {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Channel description
|
||||
description: Option<String>,
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
#[derive(Default)]
|
||||
pub struct PartialChannel {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub owner: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<File>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nsfw: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub permissions: Option<i64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role_permissions: Option<HashMap<String, OverrideField>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_permissions: Option<OverrideField>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_message_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Optional fields on channel object
|
||||
pub enum FieldsChannel {
|
||||
Description,
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Channel {
|
||||
/* /// Create a channel
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_channel(self).await?;
|
||||
|
||||
let event = EventV1::ChannelCreate(self.clone().into());
|
||||
match self {
|
||||
Self::SavedMessages { user, .. } => event.private(user.clone()).await,
|
||||
Self::DirectMessage { recipients, .. } | Self::Group { recipients, .. } => {
|
||||
for recipient in recipients {
|
||||
event.clone().private(recipient.clone()).await;
|
||||
}
|
||||
}
|
||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => {
|
||||
event.p(server.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}*/
|
||||
|
||||
/// Create a new server channel
|
||||
pub async fn create_server_channel(
|
||||
db: &Database,
|
||||
server: &mut Server,
|
||||
data: v0::DataCreateServerChannel,
|
||||
update_server: bool,
|
||||
) -> Result<Channel> {
|
||||
let config = config().await;
|
||||
if server.channels.len() > config.features.limits.global.server_channels {
|
||||
return Err(create_error!(TooManyChannels {
|
||||
max: config.features.limits.global.server_channels,
|
||||
}));
|
||||
};
|
||||
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
let channel = match data.channel_type {
|
||||
v0::LegacyServerChannelType::Text => Channel::TextChannel {
|
||||
id: id.clone(),
|
||||
server: server.id.to_owned(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
icon: None,
|
||||
last_message_id: None,
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
},
|
||||
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
|
||||
id: id.clone(),
|
||||
server: server.id.to_owned(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
icon: None,
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
},
|
||||
};
|
||||
|
||||
db.insert_channel(&channel).await?;
|
||||
|
||||
if update_server {
|
||||
server
|
||||
.update(
|
||||
db,
|
||||
PartialServer {
|
||||
channels: Some([server.channels.clone(), [id].into()].concat()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
EventV1::ChannelCreate(channel.clone().into())
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
/// Create a group
|
||||
pub async fn create_group(
|
||||
db: &Database,
|
||||
mut data: v0::DataCreateGroup,
|
||||
owner_id: String,
|
||||
) -> Result<Channel> {
|
||||
data.users.insert(owner_id.to_string());
|
||||
|
||||
let config = config().await;
|
||||
if data.users.len() > config.features.limits.global.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.global.group_size,
|
||||
}));
|
||||
}
|
||||
|
||||
let recipients = data.users.into_iter().collect::<Vec<String>>();
|
||||
let channel = Channel::Group {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
|
||||
name: data.name,
|
||||
owner: owner_id,
|
||||
description: data.description,
|
||||
recipients: recipients.clone(),
|
||||
|
||||
icon: None,
|
||||
last_message_id: None,
|
||||
|
||||
permissions: None,
|
||||
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
};
|
||||
|
||||
db.insert_channel(&channel).await?;
|
||||
|
||||
let event = EventV1::ChannelCreate(channel.clone().into());
|
||||
for recipient in recipients {
|
||||
event.clone().private(recipient).await;
|
||||
}
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
/// Create a DM (or return the existing one / saved messages)
|
||||
pub async fn create_dm(db: &Database, user_a: &User, user_b: &User) -> Result<Channel> {
|
||||
// Try to find existing channel
|
||||
if let Ok(channel) = db.find_direct_message_channel(&user_a.id, &user_b.id).await {
|
||||
Ok(channel)
|
||||
} else {
|
||||
let channel = if user_a.id == user_b.id {
|
||||
// Create a new saved messages channel
|
||||
Channel::SavedMessages {
|
||||
id: Ulid::new().to_string(),
|
||||
user: user_a.id.to_string(),
|
||||
}
|
||||
} else {
|
||||
// Create a new DM channel
|
||||
Channel::DirectMessage {
|
||||
id: Ulid::new().to_string(),
|
||||
active: true, // show by default
|
||||
recipients: vec![user_a.id.clone(), user_b.id.clone()],
|
||||
last_message_id: None,
|
||||
}
|
||||
};
|
||||
|
||||
db.insert_channel(&channel).await?;
|
||||
|
||||
match &channel {
|
||||
Channel::DirectMessage { .. } => {
|
||||
let event = EventV1::ChannelCreate(channel.clone().into());
|
||||
event.clone().private(user_a.id.clone()).await;
|
||||
event.private(user_b.id.clone()).await;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add user to a group
|
||||
pub async fn add_user_to_group(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
user: &User,
|
||||
by_id: &str,
|
||||
) -> Result<()> {
|
||||
if let Channel::Group { recipients, .. } = self {
|
||||
if recipients.contains(&String::from(&user.id)) {
|
||||
return Err(create_error!(AlreadyInGroup));
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
if recipients.len() >= config.features.limits.global.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.global.group_size
|
||||
}));
|
||||
}
|
||||
|
||||
recipients.push(String::from(&user.id));
|
||||
}
|
||||
|
||||
match &self {
|
||||
Channel::Group { id, .. } => {
|
||||
db.add_user_to_group(id, &user.id).await?;
|
||||
|
||||
EventV1::ChannelGroupJoin {
|
||||
id: id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
SystemMessage::UserAdded {
|
||||
id: user.id.to_string(),
|
||||
by: by_id.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user.id.to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Map out whether it is a direct DM
|
||||
pub fn is_direct_dm(&self) -> bool {
|
||||
matches!(self, Channel::DirectMessage { .. })
|
||||
}
|
||||
|
||||
/// Check whether has a user as a recipient
|
||||
pub fn contains_user(&self, user_id: &str) -> bool {
|
||||
match self {
|
||||
Channel::Group { recipients, .. } => recipients.contains(&String::from(user_id)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get list of recipients
|
||||
pub fn users(&self) -> Result<Vec<String>> {
|
||||
match self {
|
||||
Channel::Group { recipients, .. } => Ok(recipients.to_owned()),
|
||||
_ => Err(create_error!(NotFound)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone this channel's id
|
||||
pub fn id(&self) -> String {
|
||||
match self {
|
||||
Channel::DirectMessage { id, .. }
|
||||
| Channel::Group { id, .. }
|
||||
| Channel::SavedMessages { id, .. }
|
||||
| Channel::TextChannel { id, .. }
|
||||
| Channel::VoiceChannel { id, .. } => id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set role permission on a channel
|
||||
pub async fn set_role_permission(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
db.set_channel_role_permission(id, role_id, permissions)
|
||||
.await?;
|
||||
|
||||
role_permissions.insert(role_id.to_string(), permissions);
|
||||
|
||||
EventV1::ChannelUpdate {
|
||||
id: id.clone(),
|
||||
data: PartialChannel {
|
||||
role_permissions: Some(role_permissions.clone()),
|
||||
..Default::default()
|
||||
}
|
||||
.into(),
|
||||
clear: vec![],
|
||||
}
|
||||
.p(server.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update channel data
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
let id = self.id().to_string();
|
||||
db.update_channel(&id, &partial, remove.clone()).await?;
|
||||
|
||||
EventV1::ChannelUpdate {
|
||||
id: id.clone(),
|
||||
data: partial.into(),
|
||||
clear: remove.into_iter().map(|v| v.into()).collect(),
|
||||
}
|
||||
.p(match self {
|
||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
|
||||
_ => id,
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a field from Channel object
|
||||
pub fn remove_field(&mut self, field: &FieldsChannel) {
|
||||
match field {
|
||||
FieldsChannel::Description => match self {
|
||||
Self::Group { description, .. }
|
||||
| Self::TextChannel { description, .. }
|
||||
| Self::VoiceChannel { description, .. } => {
|
||||
description.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Icon => match self {
|
||||
Self::Group { icon, .. }
|
||||
| Self::TextChannel { icon, .. }
|
||||
| Self::VoiceChannel { icon, .. } => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::DefaultPermissions => match self {
|
||||
Self::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
} => {
|
||||
default_permissions.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove multiple fields from Channel object
|
||||
pub fn remove_fields(&mut self, partial: Vec<FieldsChannel>) {
|
||||
for field in partial {
|
||||
self.remove_field(&field)
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply partial channel to channel
|
||||
pub fn apply_options(&mut self, partial: PartialChannel) {
|
||||
match self {
|
||||
Self::SavedMessages { .. } => {}
|
||||
Self::DirectMessage { active, .. } => {
|
||||
if let Some(v) = partial.active {
|
||||
*active = v;
|
||||
}
|
||||
}
|
||||
Self::Group {
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
permissions,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
*name = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.owner {
|
||||
*owner = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.description {
|
||||
description.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.icon {
|
||||
icon.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.nsfw {
|
||||
*nsfw = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.permissions {
|
||||
permissions.replace(v);
|
||||
}
|
||||
}
|
||||
Self::TextChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
*name = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.description {
|
||||
description.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.icon {
|
||||
icon.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.nsfw {
|
||||
*nsfw = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.role_permissions {
|
||||
*role_permissions = v;
|
||||
}
|
||||
|
||||
if let Some(v) = partial.default_permissions {
|
||||
default_permissions.replace(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledge a message
|
||||
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
|
||||
EventV1::ChannelAck {
|
||||
id: self.id().to_string(),
|
||||
user: user.to_string(),
|
||||
message_id: message.to_string(),
|
||||
}
|
||||
.private(user.to_string())
|
||||
.await;
|
||||
|
||||
crate::tasks::ack::queue(
|
||||
self.id().to_string(),
|
||||
user.to_string(),
|
||||
AckEvent::AckMessage {
|
||||
id: message.to_string(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove user from a group
|
||||
pub async fn remove_user_from_group(
|
||||
&self,
|
||||
db: &Database,
|
||||
user: &User,
|
||||
by_id: Option<&str>,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
match &self {
|
||||
Channel::Group {
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
recipients,
|
||||
..
|
||||
} => {
|
||||
if &user.id == owner {
|
||||
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
|
||||
db.update_channel(
|
||||
id,
|
||||
&PartialChannel {
|
||||
owner: Some(new_owner.into()),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
SystemMessage::ChannelOwnershipChanged {
|
||||
from: owner.to_string(),
|
||||
to: new_owner.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: name,
|
||||
avatar: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
return self.delete(db).await;
|
||||
}
|
||||
}
|
||||
|
||||
db.remove_user_from_group(id, &user.id).await?;
|
||||
|
||||
EventV1::ChannelGroupLeave {
|
||||
id: id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
if !silent {
|
||||
if let Some(by) = by_id {
|
||||
SystemMessage::UserRemove {
|
||||
id: user.id.to_string(),
|
||||
by: by.to_string(),
|
||||
}
|
||||
} else {
|
||||
SystemMessage::UserLeft {
|
||||
id: user.id.to_string(),
|
||||
}
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a channel
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
let id = self.id().to_string();
|
||||
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
|
||||
// TODO: missing functionality:
|
||||
// - group invites
|
||||
// - channels list / categories list on server
|
||||
db.delete_channel(self).await
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsChannel {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsChannel::Description => "description",
|
||||
FieldsChannel::Icon => "icon",
|
||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
|
||||
use crate::{fixture, util::permissions::DatabasePermissionQuery};
|
||||
|
||||
#[async_std::test]
|
||||
async fn permissions_group_channel() {
|
||||
database_test!(|db| async move {
|
||||
fixture!(db, "group_with_members",
|
||||
owner user 0
|
||||
member1 user 1
|
||||
member2 user 2
|
||||
channel channel 3);
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
|
||||
assert!(calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &member1).channel(&channel);
|
||||
assert!(calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &member2).channel(&channel);
|
||||
assert!(!calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
});
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn permissions_text_channel() {
|
||||
database_test!(|db| async move {
|
||||
fixture!(db, "server_with_roles",
|
||||
owner user 0
|
||||
moderator user 1
|
||||
user user 2
|
||||
channel channel 3);
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
|
||||
assert!(calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &moderator).channel(&channel);
|
||||
assert!(calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &user).channel(&channel);
|
||||
assert!(!calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::SendMessage));
|
||||
});
|
||||
}
|
||||
}
|
||||
50
crates/core/database/src/models/channels/ops.rs
Normal file
50
crates/core/database/src/models/channels/ops.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannels: Sync + Send {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()>;
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel>;
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>>;
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>>;
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel>;
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel>;
|
||||
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()>;
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel_id: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()>;
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
|
||||
}
|
||||
@@ -1,87 +1,192 @@
|
||||
use super::AbstractChannels;
|
||||
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
||||
use bson::{Bson, Document};
|
||||
|
||||
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
|
||||
use crate::r#impl::mongo::IntoDocumentPath;
|
||||
use crate::{AbstractChannel, AbstractServer, Error, OverrideField, Result};
|
||||
|
||||
use super::super::MongoDb;
|
||||
use futures::StreamExt;
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
static COL: &str = "channels";
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
|
||||
// Delete all invites to these channels.
|
||||
self.col::<Document>("channel_invites")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "delete_many",
|
||||
with: "channel_invites",
|
||||
})?;
|
||||
|
||||
// Delete unread message objects on channels.
|
||||
self.col::<Document>("channel_unreads")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "delete_many",
|
||||
with: "channel_unreads",
|
||||
})
|
||||
.map(|_| ())
|
||||
|
||||
// update many attachments with parent id
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannel for MongoDb {
|
||||
async fn fetch_channel(&self, id: &str) -> Result<Channel> {
|
||||
self.find_one_by_id(COL, id).await
|
||||
impl AbstractChannels for MongoDb {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
query!(self, insert_one, COL, &channel).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
|
||||
query!(self, find_one_by_id, COL, channel_id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
||||
self.find(
|
||||
Ok(self
|
||||
.col::<Channel>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("fetch", "channels"))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
"$or": [
|
||||
{
|
||||
"$or": [
|
||||
{
|
||||
"channel_type": "DirectMessage"
|
||||
},
|
||||
{
|
||||
"channel_type": "Group"
|
||||
}
|
||||
],
|
||||
"recipients": user_id
|
||||
},
|
||||
{
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_id
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
self.insert_one(COL, channel).await.map(|_| ())
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_id
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(InternalError))
|
||||
}
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
||||
let doc = match (user_a, user_b) {
|
||||
self_user if self_user.0 == self_user.1 => {
|
||||
doc! {
|
||||
"channel_type": "SavedMessages",
|
||||
"user": self_user.0
|
||||
}
|
||||
}
|
||||
users => {
|
||||
doc! {
|
||||
"channel_type": "DirectMessage",
|
||||
"recipients": {
|
||||
"$all": [ users.0, users.1 ]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
query!(self, find_one, COL, doc)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": channel
|
||||
},
|
||||
doc! {
|
||||
"$push": {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channel"))
|
||||
}
|
||||
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel: &str,
|
||||
role: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! { "_id": channel },
|
||||
doc! {
|
||||
"$set": {
|
||||
"role_permissions.".to_owned() + role: permissions
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channel"))
|
||||
}
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
self.update_one_by_id(
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
channel,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": channel
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "channels"))
|
||||
}
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let id = channel.id().to_string();
|
||||
let server_id = match channel {
|
||||
@@ -151,10 +256,7 @@ impl AbstractChannel for MongoDb {
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "servers",
|
||||
})?;
|
||||
.map_err(|_| create_database_error!("update_one", "servers"))?;
|
||||
}
|
||||
|
||||
// Delete associated attachments
|
||||
@@ -164,139 +266,47 @@ impl AbstractChannel for MongoDb {
|
||||
.await?;
|
||||
|
||||
// Delete the channel itself
|
||||
self.delete_one_by_id(COL, &id).await.map(|_| ())
|
||||
}
|
||||
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
||||
self.find(
|
||||
COL,
|
||||
doc! {
|
||||
"$or": [
|
||||
{
|
||||
"$or": [
|
||||
{
|
||||
"channel_type": "DirectMessage"
|
||||
},
|
||||
{
|
||||
"channel_type": "Group"
|
||||
}
|
||||
],
|
||||
"recipients": user_id
|
||||
},
|
||||
{
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_id
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
self.find_one(
|
||||
COL,
|
||||
doc! {
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_id
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
||||
self.find_one(
|
||||
COL,
|
||||
if user_a == user_b {
|
||||
doc! {
|
||||
"channel_type": "SavedMessages",
|
||||
"user": user_a
|
||||
}
|
||||
} else {
|
||||
doc! {
|
||||
"channel_type": "DirectMessage",
|
||||
"recipients": {
|
||||
"$all": [ user_a, user_b ]
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": channel
|
||||
},
|
||||
doc! {
|
||||
"$push": {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "channel",
|
||||
})
|
||||
}
|
||||
|
||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": channel
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "channel",
|
||||
})
|
||||
}
|
||||
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel: &str,
|
||||
role: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! { "_id": channel },
|
||||
doc! {
|
||||
"$set": {
|
||||
"role_permissions.".to_owned() + role: permissions
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "channel",
|
||||
})
|
||||
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsChannel {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
||||
FieldsChannel::Description => "description",
|
||||
FieldsChannel::Icon => "icon",
|
||||
})
|
||||
impl MongoDb {
|
||||
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
|
||||
// Delete all invites to these channels.
|
||||
self.col::<Document>("channel_invites")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
|
||||
|
||||
// Delete unread message objects on channels.
|
||||
self.col::<Document>("channel_unreads")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
|
||||
.map(|_| ())?;
|
||||
|
||||
// update many attachments with parent id
|
||||
|
||||
// Delete all webhooks on this channel.
|
||||
self.col::<Document>("webhooks")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "webhooks"))
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
157
crates/core/database/src/models/channels/ops/reference.rs
Normal file
157
crates/core/database/src/models/channels/ops/reference.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use super::AbstractChannels;
|
||||
use crate::ReferenceDb;
|
||||
use crate::{Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannels for ReferenceDb {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
|
||||
entry.insert(channel.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_database_error!("insert", "channel"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a channel from the database
|
||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
channels
|
||||
.get(channel_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all channels from the database
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
||||
let channels = self.channels.lock().await;
|
||||
ids.iter()
|
||||
.map(|id| {
|
||||
channels
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
||||
let channels = self.channels.lock().await;
|
||||
Ok(channels
|
||||
.values()
|
||||
.filter(|channel| channel.contains_user(user_id))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
channels
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_database_error!("fetch", "channel"))
|
||||
}
|
||||
|
||||
// Fetch direct message channel (DM or Saved Messages)
|
||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
for (_, data) in channels.iter() {
|
||||
if data.contains_user(user_a) && data.contains_user(user_b) {
|
||||
return Ok(data.to_owned());
|
||||
}
|
||||
}
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
/// Insert a user to a group
|
||||
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
|
||||
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(channel_id) {
|
||||
recipients.push(String::from(user_id));
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(InvalidOperation))
|
||||
}
|
||||
}
|
||||
/// Insert channel role permissions
|
||||
async fn set_channel_role_permission(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
|
||||
if let Some(mut channel) = channels.get_mut(channel_id) {
|
||||
match &mut channel {
|
||||
Channel::TextChannel {
|
||||
role_permissions, ..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
role_permissions, ..
|
||||
} => {
|
||||
if role_permissions.get(role_id).is_some() {
|
||||
role_permissions.remove(role_id);
|
||||
role_permissions.insert(String::from(role_id), permissions);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
_ => Err(create_error!(NotFound)),
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
// Update channel
|
||||
async fn update_channel(
|
||||
&self,
|
||||
id: &str,
|
||||
channel: &PartialChannel,
|
||||
remove: Vec<FieldsChannel>,
|
||||
) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Some(channel_data) = channels.get_mut(id) {
|
||||
channel_data.apply_options(channel.to_owned());
|
||||
channel_data.remove_fields(remove);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Some(channel_data) = channels.get_mut(channel) {
|
||||
if channel_data.users()?.contains(&String::from(user)) {
|
||||
channel_data.users()?.retain(|x| x != user);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
}
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if channels.remove(&channel.id()).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/emojis/mod.rs
Normal file
5
crates/core/database/src/models/emojis/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
87
crates/core/database/src/models/emojis/model.rs
Normal file
87
crates/core/database/src/models/emojis/model.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::events::client::EventV1;
|
||||
use crate::Database;
|
||||
|
||||
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
include_str!("unicode_emoji.txt")
|
||||
.split('\n')
|
||||
.map(|x| x.into())
|
||||
.collect()
|
||||
});
|
||||
|
||||
auto_derived!(
|
||||
/// Emoji
|
||||
pub struct Emoji {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// What owns this emoji
|
||||
pub parent: EmojiParent,
|
||||
/// Uploader user id
|
||||
pub creator_id: String,
|
||||
/// Emoji name
|
||||
pub name: String,
|
||||
/// Whether the emoji is animated
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub animated: bool,
|
||||
/// Whether the emoji is marked as nsfw
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub nsfw: bool,
|
||||
}
|
||||
|
||||
/// Parent Id of the emoji
|
||||
#[serde(tag = "type")]
|
||||
pub enum EmojiParent {
|
||||
Server { id: String },
|
||||
Detached,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Emoji {
|
||||
/// Get parent id
|
||||
fn parent(&self) -> &str {
|
||||
match &self.parent {
|
||||
EmojiParent::Server { id } => id,
|
||||
EmojiParent::Detached => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an emoji
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_emoji(self).await?;
|
||||
|
||||
EventV1::EmojiCreate(self.clone().into())
|
||||
.p(self.parent().to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete an emoji
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
EventV1::EmojiDelete {
|
||||
id: self.id.to_string(),
|
||||
}
|
||||
.p(self.parent().to_string())
|
||||
.await;
|
||||
|
||||
db.detach_emoji(&self).await
|
||||
}
|
||||
|
||||
/// Check whether we can use a given emoji
|
||||
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
||||
if Ulid::from_str(emoji).is_ok() {
|
||||
db.fetch_emoji(emoji).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
use crate::models::Emoji;
|
||||
use crate::Result;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractEmoji: Sync + Send {
|
||||
pub trait AbstractEmojis: Sync + Send {
|
||||
/// Insert emoji into database.
|
||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
|
||||
|
||||
/// Fetch an emoji by its id
|
||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji>;
|
||||
|
||||
@@ -12,9 +19,6 @@ pub trait AbstractEmoji: Sync + Send {
|
||||
/// Fetch emoji by their parent ids
|
||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
|
||||
|
||||
/// Insert emoji into database.
|
||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
|
||||
}
|
||||
@@ -1,49 +1,52 @@
|
||||
use bson::Document;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::Emoji;
|
||||
use crate::{AbstractEmoji, Error, Result};
|
||||
use crate::Emoji;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::super::MongoDb;
|
||||
use super::AbstractEmojis;
|
||||
|
||||
static COL: &str = "emojis";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractEmoji for MongoDb {
|
||||
/// Fetch an emoji by its id
|
||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
||||
self.find_one_by_id(COL, id).await
|
||||
impl AbstractEmojis for MongoDb {
|
||||
/// Insert emoji into database.
|
||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
query!(self, insert_one, COL, &emoji).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch emoji by their ids
|
||||
/// Fetch an emoji by its id
|
||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch emoji by their parent id
|
||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
|
||||
self.find(
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"parent.id": parent_id
|
||||
},
|
||||
}
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Fetch emoji by their parent ids
|
||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
|
||||
self.find(
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"parent.id": {
|
||||
"$in": parent_ids
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Insert emoji into database.
|
||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
self.insert_one(COL, emoji).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete an emoji by its id
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
@@ -61,9 +64,6 @@ impl AbstractEmoji for MongoDb {
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "emojis",
|
||||
})
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
}
|
||||
67
crates/core/database/src/models/emojis/ops/reference.rs
Normal file
67
crates/core/database/src/models/emojis/ops/reference.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::EmojiParent;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractEmojis;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractEmojis for ReferenceDb {
|
||||
/// Insert emoji into database.
|
||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
let mut emojis = self.emojis.lock().await;
|
||||
if emojis.contains_key(&emoji.id) {
|
||||
Err(create_database_error!("insert", "emoji"))
|
||||
} else {
|
||||
emojis.insert(emoji.id.to_string(), emoji.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch an emoji by its id
|
||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
||||
let emojis = self.emojis.lock().await;
|
||||
emojis
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch emoji by their parent id
|
||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
|
||||
let emojis = self.emojis.lock().await;
|
||||
Ok(emojis
|
||||
.values()
|
||||
.filter(|emoji| match &emoji.parent {
|
||||
EmojiParent::Server { id } => id == parent_id,
|
||||
_ => false,
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch emoji by their parent ids
|
||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
|
||||
let emojis = self.emojis.lock().await;
|
||||
Ok(emojis
|
||||
.values()
|
||||
.filter(|emoji| match &emoji.parent {
|
||||
EmojiParent::Server { id } => parent_ids.contains(id),
|
||||
_ => false,
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
let mut emojis = self.emojis.lock().await;
|
||||
if let Some(bot) = emojis.get_mut(&emoji.id) {
|
||||
bot.parent = EmojiParent::Detached;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/files/mod.rs
Normal file
5
crates/core/database/src/models/files/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
107
crates/core/database/src/models/files/model.rs
Normal file
107
crates/core/database/src/models/files/model.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::Database;
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// File
|
||||
pub struct File {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Tag / bucket this file was uploaded to
|
||||
pub tag: String,
|
||||
/// Original filename
|
||||
pub filename: String,
|
||||
/// Parsed metadata of this file
|
||||
pub metadata: Metadata,
|
||||
/// Raw content type of this file
|
||||
pub content_type: String,
|
||||
/// Size of this file (in bytes)
|
||||
pub size: isize,
|
||||
|
||||
/// Whether this file was deleted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deleted: Option<bool>,
|
||||
/// Whether this file was reported
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reported: Option<bool>,
|
||||
|
||||
// TODO: migrate this mess to having:
|
||||
// - author_id
|
||||
// - parent: Parent { Message(id), User(id), etc }
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub server_id: Option<String>,
|
||||
|
||||
/// Id of the object this file is associated with
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_id: Option<String>,
|
||||
},
|
||||
"PartialFile"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Metadata associated with a file
|
||||
#[serde(tag = "type")]
|
||||
#[derive(Default)]
|
||||
pub enum Metadata {
|
||||
/// File is just a generic uncategorised file
|
||||
#[default]
|
||||
File,
|
||||
/// File contains textual data and should be displayed as such
|
||||
Text,
|
||||
/// File is an image with specific dimensions
|
||||
Image { width: isize, height: isize },
|
||||
/// File is a video with specific dimensions
|
||||
Video { width: isize, height: isize },
|
||||
/// File is audio
|
||||
Audio,
|
||||
}
|
||||
);
|
||||
|
||||
impl File {
|
||||
/// Use a file for a message attachment
|
||||
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "attachments", "message", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a user profile background
|
||||
pub async fn use_background(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "backgrounds", "user", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a user avatar
|
||||
pub async fn use_avatar(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "avatars", "user", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for an icon
|
||||
pub async fn use_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a server icon
|
||||
pub async fn use_server_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a server banner
|
||||
pub async fn use_banner(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "banners", "server", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for an emoji
|
||||
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "emojis", "object", parent)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,30 @@
|
||||
use crate::models::attachment::File;
|
||||
use crate::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAttachment: Sync + Send {
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
id: &str,
|
||||
tag: &str,
|
||||
parent_type: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<File>;
|
||||
|
||||
/// Insert attachment into database.
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
|
||||
|
||||
/// Mark an attachment as having been reported.
|
||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Mark an attachment as having been deleted.
|
||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Mark multiple attachments as having been deleted.
|
||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>;
|
||||
}
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::File;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAttachments: Sync + Send {
|
||||
/// Insert attachment into database.
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
id: &str,
|
||||
tag: &str,
|
||||
parent_type: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<File>;
|
||||
|
||||
/// Mark an attachment as having been reported.
|
||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Mark an attachment as having been deleted.
|
||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Mark multiple attachments as having been deleted.
|
||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>;
|
||||
}
|
||||
@@ -1,86 +1,62 @@
|
||||
use bson::Document;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::attachment::File;
|
||||
use crate::{AbstractAttachment, Error, Result};
|
||||
use crate::File;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::super::MongoDb;
|
||||
use super::AbstractAttachments;
|
||||
|
||||
static COL: &str = "attachments";
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_many_attachments(&self, projection: Document) -> Result<()> {
|
||||
#[async_trait]
|
||||
impl AbstractAttachments for MongoDb {
|
||||
/// Insert attachment into database.
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
||||
query!(self, insert_one, COL, &attachment).map(|_| ())
|
||||
}
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
id: &str,
|
||||
tag: &str,
|
||||
parent_type: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<File> {
|
||||
let key = format!("{parent_type}_id");
|
||||
let file = query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id": id,
|
||||
"tag": tag,
|
||||
&key: {
|
||||
"$exists": false
|
||||
}
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))?;
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
projection,
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"deleted": true
|
||||
key: parent_id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_many",
|
||||
with: "attachment",
|
||||
})
|
||||
}
|
||||
}
|
||||
.map_err(|_| create_database_error!("update_one", COL))?;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAttachment for MongoDb {
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
attachment_id: &str,
|
||||
tag: &str,
|
||||
parent_type: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<File> {
|
||||
let key = format!("{}_id", parent_type);
|
||||
match self
|
||||
.find_one::<File>(
|
||||
COL,
|
||||
doc! {
|
||||
"_id": attachment_id,
|
||||
"tag": tag,
|
||||
&key: {
|
||||
"$exists": false
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(file) => {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &file.id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
key: parent_id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "attachment",
|
||||
})?;
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
Err(Error::NotFound) => Err(Error::UnknownAttachment),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
||||
self.insert_one(COL, attachment).await.map(|_| ())
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Mark an attachment as having been reported.
|
||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
@@ -96,12 +72,10 @@ impl AbstractAttachment for MongoDb {
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "attachment",
|
||||
})
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Mark an attachment as having been deleted.
|
||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
@@ -117,15 +91,13 @@ impl AbstractAttachment for MongoDb {
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update_one",
|
||||
with: "attachment",
|
||||
})
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Mark multiple attachments as having been deleted.
|
||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
@@ -140,9 +112,24 @@ impl AbstractAttachment for MongoDb {
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| Error::DatabaseError {
|
||||
operation: "update",
|
||||
with: "attachments",
|
||||
})
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_many_attachments(&self, projection: Document) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
projection,
|
||||
doc! {
|
||||
"$set": {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
}
|
||||
}
|
||||
89
crates/core/database/src/models/files/ops/reference.rs
Normal file
89
crates/core/database/src/models/files/ops/reference.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::File;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractAttachments;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAttachments for ReferenceDb {
|
||||
/// Insert attachment into database.
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
||||
let mut attachments = self.files.lock().await;
|
||||
if attachments.contains_key(&attachment.id) {
|
||||
Err(create_database_error!("insert", "attachment"))
|
||||
} else {
|
||||
attachments.insert(attachment.id.to_string(), attachment.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
id: &str,
|
||||
tag: &str,
|
||||
parent_type: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<File> {
|
||||
let mut files = self.files.lock().await;
|
||||
if let Some(file) = files.get_mut(id) {
|
||||
if file.tag == tag {
|
||||
match parent_type {
|
||||
"message" => file.message_id = Some(parent_id.to_owned()),
|
||||
"user" => file.user_id = Some(parent_id.to_owned()),
|
||||
"object" => file.object_id = Some(parent_id.to_owned()),
|
||||
"server" => file.server_id = Some(parent_id.to_owned()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
Ok(file.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark an attachment as having been reported.
|
||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
|
||||
let mut files = self.files.lock().await;
|
||||
if let Some(file) = files.get_mut(id) {
|
||||
file.reported = Some(true);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark an attachment as having been deleted.
|
||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
|
||||
let mut files = self.files.lock().await;
|
||||
if let Some(file) = files.get_mut(id) {
|
||||
file.deleted = Some(true);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark multiple attachments as having been deleted.
|
||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
|
||||
let mut files = self.files.lock().await;
|
||||
|
||||
for id in ids {
|
||||
if !files.contains_key(id) {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
}
|
||||
|
||||
for id in ids {
|
||||
if let Some(file) = files.get_mut(id) {
|
||||
file.reported = Some(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/messages/mod.rs
Normal file
5
crates/core/database/src/models/messages/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
857
crates/core/database/src/models/messages/model.rs
Normal file
857
crates/core/database/src/models/messages/model.rs
Normal file
@@ -0,0 +1,857 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0::{
|
||||
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
|
||||
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
||||
};
|
||||
use revolt_permissions::{ChannelPermission, PermissionValue};
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1,
|
||||
tasks::{self, ack::AckEvent},
|
||||
util::idempotency::IdempotencyKey,
|
||||
Channel, Database, Emoji, File, User,
|
||||
};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Message
|
||||
pub struct Message {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Unique value generated by client sending this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nonce: Option<String>,
|
||||
/// Id of the channel this message was sent in
|
||||
pub channel: String,
|
||||
/// Id of the user or webhook that sent this message
|
||||
pub author: String,
|
||||
/// The webhook that sent this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub webhook: Option<MessageWebhook>,
|
||||
/// Message content
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
/// System message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system: Option<SystemMessage>,
|
||||
/// Array of attachments
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attachments: Option<Vec<File>>,
|
||||
/// Time at which this message was last edited
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub edited: Option<Timestamp>,
|
||||
/// Attached embeds to this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub embeds: Option<Vec<Embed>>,
|
||||
/// Array of user ids mentioned in this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentions: Option<Vec<String>>,
|
||||
/// Array of message ids this message is replying to
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub replies: Option<Vec<String>>,
|
||||
/// Hashmap of emoji IDs to array of user IDs
|
||||
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
|
||||
pub reactions: IndexMap<String, IndexSet<String>>,
|
||||
/// Information about how this message should be interacted with
|
||||
#[serde(skip_serializing_if = "Interactions::is_default", default)]
|
||||
pub interactions: Interactions,
|
||||
/// Name and / or avatar overrides for this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub masquerade: Option<Masquerade>,
|
||||
|
||||
/// Bitfield of message flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
},
|
||||
"PartialMessage"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// System Event
|
||||
#[serde(tag = "type")]
|
||||
pub enum SystemMessage {
|
||||
#[serde(rename = "text")]
|
||||
Text { content: String },
|
||||
#[serde(rename = "user_added")]
|
||||
UserAdded { id: String, by: String },
|
||||
#[serde(rename = "user_remove")]
|
||||
UserRemove { id: String, by: String },
|
||||
#[serde(rename = "user_joined")]
|
||||
UserJoined { id: String },
|
||||
#[serde(rename = "user_left")]
|
||||
UserLeft { id: String },
|
||||
#[serde(rename = "user_kicked")]
|
||||
UserKicked { id: String },
|
||||
#[serde(rename = "user_banned")]
|
||||
UserBanned { id: String },
|
||||
#[serde(rename = "channel_renamed")]
|
||||
ChannelRenamed { name: String, by: String },
|
||||
#[serde(rename = "channel_description_changed")]
|
||||
ChannelDescriptionChanged { by: String },
|
||||
#[serde(rename = "channel_icon_changed")]
|
||||
ChannelIconChanged { by: String },
|
||||
#[serde(rename = "channel_ownership_changed")]
|
||||
ChannelOwnershipChanged { from: String, to: String },
|
||||
}
|
||||
|
||||
/// Name and / or avatar override information
|
||||
pub struct Masquerade {
|
||||
/// Replace the display name shown on this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
/// Replace the avatar shown on this message (URL to image file)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<String>,
|
||||
/// Replace the display role colour shown on this message
|
||||
///
|
||||
/// Must have `ManageRole` permission to use
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub colour: Option<String>,
|
||||
}
|
||||
|
||||
/// Information to guide interactions on this message
|
||||
#[derive(Default)]
|
||||
pub struct Interactions {
|
||||
/// Reactions which should always appear and be distinct
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub reactions: Option<IndexSet<String>>,
|
||||
/// Whether reactions should be restricted to the given list
|
||||
///
|
||||
/// Can only be set to true if reactions list is of at least length 1
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub restrict_reactions: bool,
|
||||
}
|
||||
|
||||
/// Appended Information
|
||||
pub struct AppendMessage {
|
||||
/// Additional embeds to include in this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub embeds: Option<Vec<Embed>>,
|
||||
}
|
||||
|
||||
/// Message Time Period
|
||||
///
|
||||
/// Filter and sort messages by time
|
||||
#[serde(untagged)]
|
||||
pub enum MessageTimePeriod {
|
||||
Relative {
|
||||
/// 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.
|
||||
nearby: String,
|
||||
},
|
||||
Absolute {
|
||||
/// Message id before which messages should be fetched
|
||||
before: Option<String>,
|
||||
/// Message id after which messages should be fetched
|
||||
after: Option<String>,
|
||||
/// Message sort direction
|
||||
sort: Option<MessageSort>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Message Filter
|
||||
#[derive(Default)]
|
||||
pub struct MessageFilter {
|
||||
/// Parent channel ID
|
||||
pub channel: Option<String>,
|
||||
/// Message author ID
|
||||
pub author: Option<String>,
|
||||
/// Search query
|
||||
pub query: Option<String>,
|
||||
}
|
||||
|
||||
/// Message Query
|
||||
pub struct MessageQuery {
|
||||
/// Maximum number of messages to fetch
|
||||
///
|
||||
/// For fetching nearby messages, this is \`(limit + 1)\`.
|
||||
pub limit: Option<i64>,
|
||||
/// Filter to apply
|
||||
#[serde(flatten)]
|
||||
pub filter: MessageFilter,
|
||||
/// Time period to fetch
|
||||
#[serde(flatten)]
|
||||
pub time_period: MessageTimePeriod,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Message {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Default::default(),
|
||||
nonce: None,
|
||||
channel: Default::default(),
|
||||
author: Default::default(),
|
||||
webhook: None,
|
||||
content: None,
|
||||
system: None,
|
||||
attachments: None,
|
||||
edited: None,
|
||||
embeds: None,
|
||||
mentions: None,
|
||||
replies: None,
|
||||
reactions: Default::default(),
|
||||
interactions: Default::default(),
|
||||
masquerade: None,
|
||||
flags: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Message {
|
||||
/// Create message from API data
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_from_api(
|
||||
db: &Database,
|
||||
channel: Channel,
|
||||
data: DataMessageSend,
|
||||
author: MessageAuthor<'_>,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
limits: FeaturesLimits,
|
||||
mut idempotency: IdempotencyKey,
|
||||
generate_embeds: bool,
|
||||
allow_mentions: bool,
|
||||
) -> Result<Message> {
|
||||
let config = config().await;
|
||||
|
||||
Message::validate_sum(
|
||||
&data.content,
|
||||
data.embeds.as_deref().unwrap_or_default(),
|
||||
limits.message_length,
|
||||
)?;
|
||||
|
||||
idempotency
|
||||
.consume_nonce(data.nonce)
|
||||
.await
|
||||
.map_err(|_| create_error!(InvalidOperation))?;
|
||||
|
||||
// Check the message is not empty
|
||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
||||
{
|
||||
return Err(create_error!(EmptyMessage));
|
||||
}
|
||||
|
||||
// Ensure flags are either not set or have permissible values
|
||||
if let Some(flags) = &data.flags {
|
||||
if flags != &0 && flags != &1 {
|
||||
return Err(create_error!(InvalidProperty));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure restrict_reactions is not specified without reactions list
|
||||
if let Some(interactions) = &data.interactions {
|
||||
if interactions.restrict_reactions {
|
||||
let disallowed = if let Some(list) = &interactions.reactions {
|
||||
list.is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if disallowed {
|
||||
return Err(create_error!(InvalidProperty));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (author_id, webhook) = match &author {
|
||||
MessageAuthor::User(user) => (user.id.clone(), None),
|
||||
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
|
||||
MessageAuthor::System { .. } => ("00000000000000000000000000".to_string(), None),
|
||||
};
|
||||
|
||||
// Start constructing the message
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: channel.id(),
|
||||
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
|
||||
interactions: data
|
||||
.interactions
|
||||
.map(|interactions| interactions.into())
|
||||
.unwrap_or_default(),
|
||||
author: author_id,
|
||||
webhook: webhook.map(|w| w.into()),
|
||||
flags: data.flags.map(|v| v as i32),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Parse mentions in message.
|
||||
let mut mentions = HashSet::new();
|
||||
if allow_mentions {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > config.features.limits.global.message_replies {
|
||||
return Err(create_error!(TooManyReplies {
|
||||
max: config.features.limits.global.message_replies,
|
||||
}));
|
||||
}
|
||||
|
||||
for ReplyIntent { id, mention } in entries {
|
||||
let message = db.fetch_message(&id).await?;
|
||||
|
||||
if mention && allow_mentions {
|
||||
mentions.insert(message.author.to_owned());
|
||||
}
|
||||
|
||||
replies.insert(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
if !mentions.is_empty() {
|
||||
message.mentions.replace(mentions.into_iter().collect());
|
||||
}
|
||||
|
||||
if !replies.is_empty() {
|
||||
message
|
||||
.replies
|
||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
||||
}
|
||||
|
||||
// Add attachments to message.
|
||||
let mut attachments = vec![];
|
||||
if data
|
||||
.attachments
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > limits.message_attachments)
|
||||
{
|
||||
return Err(create_error!(TooManyAttachments {
|
||||
max: limits.message_attachments,
|
||||
}));
|
||||
}
|
||||
|
||||
if data
|
||||
.embeds
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > config.features.limits.global.message_embeds)
|
||||
{
|
||||
return Err(create_error!(TooManyEmbeds {
|
||||
max: config.features.limits.global.message_embeds,
|
||||
}));
|
||||
}
|
||||
|
||||
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
|
||||
attachments.push(
|
||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
if !attachments.is_empty() {
|
||||
message.attachments.replace(attachments);
|
||||
}
|
||||
|
||||
// Process included embeds.
|
||||
for sendable_embed in data.embeds.unwrap_or_default() {
|
||||
message.attach_sendable_embed(db, sendable_embed).await?;
|
||||
}
|
||||
|
||||
// Set content
|
||||
message.content = data.content;
|
||||
|
||||
// Pass-through nonce value for clients
|
||||
message.nonce = Some(idempotency.into_key());
|
||||
|
||||
// Send the message
|
||||
message
|
||||
.send(db, author, user, member, &channel, generate_embeds)
|
||||
.await?;
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Send a message without any notifications
|
||||
pub async fn send_without_notifications(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
is_dm: bool,
|
||||
generate_embeds: bool,
|
||||
) -> Result<()> {
|
||||
db.insert_message(self).await?;
|
||||
|
||||
// Fan out events
|
||||
EventV1::Message(self.clone().into_model(user, member))
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Update last_message_id
|
||||
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
|
||||
|
||||
// Add mentions for affected users
|
||||
if let Some(mentions) = &self.mentions {
|
||||
for user in mentions {
|
||||
tasks::ack::queue(
|
||||
self.channel.to_string(),
|
||||
user.to_string(),
|
||||
AckEvent::AddMention {
|
||||
ids: vec![self.id.to_string()],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate embeds
|
||||
if generate_embeds {
|
||||
if let Some(content) = &self.content {
|
||||
tasks::process_embeds::queue(
|
||||
self.channel.to_string(),
|
||||
self.id.to_string(),
|
||||
content.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a message
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
author: MessageAuthor<'_>,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
channel: &Channel,
|
||||
generate_embeds: bool,
|
||||
) -> Result<()> {
|
||||
self.send_without_notifications(
|
||||
db,
|
||||
user,
|
||||
member,
|
||||
matches!(channel, Channel::DirectMessage { .. }),
|
||||
generate_embeds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !self.has_suppressed_notifications() {
|
||||
// Push out Web Push notifications
|
||||
crate::tasks::web_push::queue(
|
||||
{
|
||||
match channel {
|
||||
Channel::DirectMessage { recipients, .. }
|
||||
| Channel::Group { recipients, .. } => recipients.clone(),
|
||||
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
|
||||
_ => vec![],
|
||||
}
|
||||
},
|
||||
PushNotification::from(
|
||||
self.clone().into_model(None, None),
|
||||
Some(author),
|
||||
&channel.id(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create text embed from sendable embed
|
||||
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
|
||||
embed.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
let media = if let Some(id) = embed.media {
|
||||
Some(
|
||||
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Embed::Text(Text {
|
||||
icon_url: embed.icon_url,
|
||||
url: embed.url,
|
||||
title: embed.title,
|
||||
description: embed.description,
|
||||
media: media.map(|m| m.into()),
|
||||
colour: embed.colour,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Whether this message has suppressed notifications
|
||||
pub fn has_suppressed_notifications(&self) -> bool {
|
||||
if let Some(flags) = self.flags {
|
||||
flags & MessageFlags::SuppressNotifications as i32
|
||||
== MessageFlags::SuppressNotifications as i32
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Update message data
|
||||
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
|
||||
self.apply_options(partial.clone());
|
||||
db.update_message(&self.id, &partial).await?;
|
||||
|
||||
EventV1::MessageUpdate {
|
||||
id: self.id.clone(),
|
||||
channel: self.channel.clone(),
|
||||
data: partial.into(),
|
||||
}
|
||||
.p(self.channel.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to fetch many messages with users
|
||||
pub async fn fetch_with_users(
|
||||
db: &Database,
|
||||
query: MessageQuery,
|
||||
perspective: &User,
|
||||
include_users: Option<bool>,
|
||||
server_id: Option<String>,
|
||||
) -> Result<BulkMessageResponse> {
|
||||
let messages: Vec<v0::Message> = db
|
||||
.fetch_messages(query)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|msg| msg.into_model(None, None))
|
||||
.collect();
|
||||
|
||||
if let Some(true) = include_users {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.flat_map(|m| {
|
||||
let mut users = vec![m.author.clone()];
|
||||
if let Some(system) = &m.system {
|
||||
match system {
|
||||
v0::SystemMessage::ChannelDescriptionChanged { by } => {
|
||||
users.push(by.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelIconChanged { by } => users.push(by.clone()),
|
||||
v0::SystemMessage::ChannelOwnershipChanged { from, to, .. } => {
|
||||
users.push(from.clone());
|
||||
users.push(to.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelRenamed { by, .. } => users.push(by.clone()),
|
||||
v0::SystemMessage::UserAdded { by, id, .. }
|
||||
| v0::SystemMessage::UserRemove { by, id, .. } => {
|
||||
users.push(by.clone());
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::UserBanned { id, .. }
|
||||
| v0::SystemMessage::UserKicked { id, .. }
|
||||
| v0::SystemMessage::UserJoined { id, .. }
|
||||
| v0::SystemMessage::UserLeft { id, .. } => {
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::Text { .. } => {}
|
||||
}
|
||||
}
|
||||
users
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
let users = User::fetch_many_ids_as_mutuals(db, perspective, &user_ids).await?;
|
||||
|
||||
Ok(BulkMessageResponse::MessagesAndUsers {
|
||||
messages,
|
||||
users,
|
||||
members: if let Some(server_id) = server_id {
|
||||
Some(
|
||||
db.fetch_members(&server_id, &user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
} else {
|
||||
Ok(BulkMessageResponse::JustMessages(messages))
|
||||
}
|
||||
}
|
||||
|
||||
/// Append content to message
|
||||
pub async fn append(
|
||||
db: &Database,
|
||||
id: String,
|
||||
channel: String,
|
||||
append: AppendMessage,
|
||||
) -> Result<()> {
|
||||
db.append_message(&id, &append).await?;
|
||||
|
||||
EventV1::MessageAppend {
|
||||
id,
|
||||
channel: channel.to_string(),
|
||||
append: append.into(),
|
||||
}
|
||||
.p(channel)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert sendable embed to text embed and attach to message
|
||||
pub async fn attach_sendable_embed(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
embed: v0::SendableEmbed,
|
||||
) -> Result<()> {
|
||||
let media: Option<v0::File> = if let Some(id) = embed.media {
|
||||
Some(
|
||||
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
|
||||
.await?
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let embed = v0::Embed::Text(v0::Text {
|
||||
icon_url: embed.icon_url,
|
||||
url: embed.url,
|
||||
title: embed.title,
|
||||
description: embed.description,
|
||||
media,
|
||||
colour: embed.colour,
|
||||
});
|
||||
|
||||
if let Some(embeds) = &mut self.embeds {
|
||||
embeds.push(embed);
|
||||
} else {
|
||||
self.embeds = Some(vec![embed]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a reaction to a message
|
||||
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
|
||||
// Check how many reactions are already on the message
|
||||
let config = config().await;
|
||||
if self.reactions.len() >= config.features.limits.global.message_reactions
|
||||
&& !self.reactions.contains_key(emoji)
|
||||
{
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Check if the emoji is whitelisted
|
||||
if !self.interactions.can_use(emoji) {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Check if the emoji is usable by us
|
||||
if !Emoji::can_use(db, emoji).await? {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Send reaction event
|
||||
EventV1::MessageReact {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
user_id: user.id.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Add emoji
|
||||
db.add_reaction(&self.id, emoji, &user.id).await
|
||||
}
|
||||
|
||||
/// Validate the sum of content of a message is under threshold
|
||||
pub fn validate_sum(
|
||||
content: &Option<String>,
|
||||
embeds: &[SendableEmbed],
|
||||
max_length: usize,
|
||||
) -> Result<()> {
|
||||
let mut running_total = 0;
|
||||
if let Some(content) = content {
|
||||
running_total += content.len();
|
||||
}
|
||||
|
||||
for embed in embeds {
|
||||
if let Some(desc) = &embed.description {
|
||||
running_total += desc.len();
|
||||
}
|
||||
}
|
||||
|
||||
if running_total <= max_length {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(PayloadTooLarge))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a message
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
let file_ids: Vec<String> = self
|
||||
.attachments
|
||||
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
if !file_ids.is_empty() {
|
||||
db.mark_attachments_as_deleted(&file_ids).await?;
|
||||
}
|
||||
|
||||
db.delete_message(&self.id).await?;
|
||||
|
||||
EventV1::MessageDelete {
|
||||
id: self.id,
|
||||
channel: self.channel.clone(),
|
||||
}
|
||||
.p(self.channel)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bulk delete messages
|
||||
pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec<String>) -> Result<()> {
|
||||
let valid_ids = db
|
||||
.fetch_messages_by_id(&ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|msg| msg.channel == channel)
|
||||
.map(|msg| msg.id)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
db.delete_messages(channel, &valid_ids).await?;
|
||||
EventV1::BulkMessageDelete {
|
||||
channel: channel.to_string(),
|
||||
ids: valid_ids,
|
||||
}
|
||||
.p(channel.to_string())
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
|
||||
// Check if it actually exists
|
||||
let empty = if let Some(users) = self.reactions.get(emoji) {
|
||||
if !users.contains(user) {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
users.len() == 1
|
||||
} else {
|
||||
return Err(create_error!(NotFound));
|
||||
};
|
||||
|
||||
// Send reaction event
|
||||
EventV1::MessageUnreact {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
user_id: user.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
if empty {
|
||||
// If empty, remove the reaction entirely
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
} else {
|
||||
// Otherwise only remove that one reaction
|
||||
db.remove_reaction(&self.id, emoji, user).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
|
||||
// Send reaction event
|
||||
EventV1::MessageRemoveReaction {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Write to database
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemMessage {
|
||||
pub fn into_message(self, channel: String) -> Message {
|
||||
Message {
|
||||
id: Ulid::new().to_string(),
|
||||
channel,
|
||||
author: "00000000000000000000000000".to_string(),
|
||||
system: Some(self),
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactions {
|
||||
/// Validate interactions info is correct
|
||||
pub async fn validate(&self, db: &Database, permissions: &PermissionValue) -> Result<()> {
|
||||
let config = config().await;
|
||||
|
||||
if let Some(reactions) = &self.reactions {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
||||
|
||||
if reactions.len() > config.features.limits.global.message_reactions {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
for reaction in reactions {
|
||||
if !Emoji::can_use(db, reaction).await? {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if we can use a given emoji to react
|
||||
pub fn can_use(&self, emoji: &str) -> bool {
|
||||
if self.restrict_reactions {
|
||||
if let Some(reactions) = &self.reactions {
|
||||
reactions.contains(emoji)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if default initialisation of fields
|
||||
pub fn is_default(&self) -> bool {
|
||||
!self.restrict_reactions && self.reactions.is_none()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
use crate::models::message::{AppendMessage, Message, MessageSort, PartialMessage};
|
||||
use crate::Result;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractMessage: Sync + Send {
|
||||
pub trait AbstractMessages: Sync + Send {
|
||||
/// Insert a new message into the database
|
||||
async fn insert_message(&self, message: &Message) -> Result<()>;
|
||||
|
||||
/// Fetch a message by its id
|
||||
async fn fetch_message(&self, id: &str) -> Result<Message>;
|
||||
|
||||
/// Insert a new message into the database
|
||||
async fn insert_message(&self, message: &Message) -> Result<()>;
|
||||
/// Fetch multiple messages by given query
|
||||
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
|
||||
|
||||
/// Fetch multiple messages by given IDs
|
||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
|
||||
@@ -15,34 +25,6 @@ pub trait AbstractMessage: Sync + Send {
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
|
||||
|
||||
/// Delete a message from the database by its id
|
||||
async fn delete_message(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Delete messages from a channel by their ids and corresponding channel id
|
||||
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()>;
|
||||
|
||||
/// Fetch multiple messages
|
||||
async fn fetch_messages(
|
||||
&self,
|
||||
channel: &str,
|
||||
limit: Option<i64>,
|
||||
before: Option<String>,
|
||||
after: Option<String>,
|
||||
sort: Option<MessageSort>,
|
||||
nearby: Option<String>,
|
||||
) -> Result<Vec<Message>>;
|
||||
|
||||
/// Search for messages
|
||||
async fn search_messages(
|
||||
&self,
|
||||
channel: &str,
|
||||
query: &str,
|
||||
limit: Option<i64>,
|
||||
before: Option<String>,
|
||||
after: Option<String>,
|
||||
sort: MessageSort,
|
||||
) -> Result<Vec<Message>>;
|
||||
|
||||
/// Add a new reaction to a message
|
||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
|
||||
|
||||
@@ -51,4 +33,10 @@ pub trait AbstractMessage: Sync + Send {
|
||||
|
||||
/// Remove reaction from a message
|
||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
|
||||
|
||||
/// Delete a message from the database by its id
|
||||
async fn delete_message(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Delete messages from a channel by their ids and corresponding channel id
|
||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>;
|
||||
}
|
||||
351
crates/core/database/src/models/messages/ops/mongodb.rs
Normal file
351
crates/core/database/src/models/messages/ops/mongodb.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
use bson::{to_bson, Document};
|
||||
use futures::try_join;
|
||||
use mongodb::options::FindOptions;
|
||||
use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{
|
||||
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
|
||||
};
|
||||
|
||||
use super::AbstractMessages;
|
||||
|
||||
static COL: &str = "messages";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractMessages for MongoDb {
|
||||
/// Insert a new message into the database
|
||||
async fn insert_message(&self, message: &Message) -> Result<()> {
|
||||
query!(self, insert_one, COL, &message).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a message by its id
|
||||
async fn fetch_message(&self, id: &str) -> Result<Message> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch multiple messages by given query
|
||||
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
|
||||
let mut filter = doc! {};
|
||||
|
||||
// 1. Apply message filters
|
||||
if let Some(channel) = query.filter.channel {
|
||||
filter.insert("channel", channel);
|
||||
}
|
||||
|
||||
if let Some(author) = query.filter.author {
|
||||
filter.insert("author", author);
|
||||
}
|
||||
|
||||
let is_search_query = if let Some(query) = query.filter.query {
|
||||
filter.insert(
|
||||
"$text",
|
||||
doc! {
|
||||
"$search": query
|
||||
},
|
||||
);
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 2. Find query limit
|
||||
let limit = query.limit.unwrap_or(50);
|
||||
|
||||
// 3. Apply message time period
|
||||
match query.time_period {
|
||||
MessageTimePeriod::Relative { nearby } => {
|
||||
// 3.1. Prepare filters
|
||||
let mut older_message_filter = filter.clone();
|
||||
let mut newer_message_filter = filter;
|
||||
|
||||
older_message_filter.insert(
|
||||
"_id",
|
||||
doc! {
|
||||
"$lt": &nearby
|
||||
},
|
||||
);
|
||||
|
||||
newer_message_filter.insert(
|
||||
"_id",
|
||||
doc! {
|
||||
"$gte": &nearby
|
||||
},
|
||||
);
|
||||
|
||||
// 3.2. Execute in both directions
|
||||
let (a, b) = try_join!(
|
||||
self.find_with_options::<_, Message>(
|
||||
COL,
|
||||
newer_message_filter,
|
||||
FindOptions::builder()
|
||||
.limit(limit / 2 + 1)
|
||||
.sort(doc! {
|
||||
"_id": 1_i32
|
||||
})
|
||||
.build(),
|
||||
),
|
||||
self.find_with_options::<_, Message>(
|
||||
COL,
|
||||
older_message_filter,
|
||||
FindOptions::builder()
|
||||
.limit(limit / 2)
|
||||
.sort(doc! {
|
||||
"_id": -1_i32
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
)
|
||||
.map_err(|_| create_database_error!("find", COL))?;
|
||||
|
||||
Ok([a, b].concat())
|
||||
}
|
||||
MessageTimePeriod::Absolute {
|
||||
before,
|
||||
after,
|
||||
sort,
|
||||
} => {
|
||||
// 3.1. Apply message ID filter
|
||||
if let Some(doc) = match (before, after) {
|
||||
(Some(before), Some(after)) => Some(doc! {
|
||||
"$lt": before,
|
||||
"$gt": after
|
||||
}),
|
||||
(Some(before), _) => Some(doc! {
|
||||
"$lt": before
|
||||
}),
|
||||
(_, Some(after)) => Some(doc! {
|
||||
"$gt": after
|
||||
}),
|
||||
_ => None,
|
||||
} {
|
||||
filter.insert("_id", doc);
|
||||
}
|
||||
|
||||
// 3.2. Execute with given message sort
|
||||
self.find_with_options(
|
||||
COL,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.limit(limit)
|
||||
.sort(match sort.unwrap_or(MessageSort::Latest) {
|
||||
// Sort by relevance, fallback to latest
|
||||
MessageSort::Relevance => {
|
||||
if is_search_query {
|
||||
doc! {
|
||||
"score": {
|
||||
"$meta": "textScore"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
doc! {
|
||||
"_id": -1_i32
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort by latest first
|
||||
MessageSort::Latest => doc! {
|
||||
"_id": -1_i32
|
||||
},
|
||||
// Sort by oldest first
|
||||
MessageSort::Oldest => doc! {
|
||||
"_id": 1_i32
|
||||
},
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch multiple messages by given IDs
|
||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
|
||||
self.find_with_options(
|
||||
COL,
|
||||
doc! {
|
||||
"ids": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))
|
||||
}
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
||||
let mut query = doc! {};
|
||||
|
||||
if let Some(embeds) = &append.embeds {
|
||||
if !embeds.is_empty() {
|
||||
query.insert(
|
||||
"$push",
|
||||
doc! {
|
||||
"embeds": {
|
||||
"$each": to_bson(embeds)
|
||||
.map_err(|_| create_database_error!("to_bson", "embeds"))?
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if query.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
query,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Add a new reaction to a message
|
||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$addToSet": {
|
||||
format!("reactions.{emoji}"): user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
format!("reactions.{emoji}"): user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Remove reaction from a message
|
||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
format!("reactions.{emoji}"): 1
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Delete a message from the database by its id
|
||||
async fn delete_message(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete messages from a channel by their ids and corresponding channel id
|
||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": channel,
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("delete_many", COL))
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
|
||||
let mut for_attachments = projection.clone();
|
||||
for_attachments.insert(
|
||||
"attachments",
|
||||
doc! {
|
||||
"$exists": 1_i32
|
||||
},
|
||||
);
|
||||
|
||||
// Check if there are any attachments we need to delete.
|
||||
let message_ids_with_attachments = self
|
||||
.find_with_options::<_, DocumentId>(
|
||||
COL,
|
||||
for_attachments,
|
||||
FindOptions::builder()
|
||||
.projection(doc! { "_id": 1_i32 })
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find_many", "attachments"))?
|
||||
.into_iter()
|
||||
.map(|x| x.id)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// If we found any, mark them as deleted.
|
||||
if !message_ids_with_attachments.is_empty() {
|
||||
self.col::<Document>("attachments")
|
||||
.update_many(
|
||||
doc! {
|
||||
"message_id": {
|
||||
"$in": message_ids_with_attachments
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_many", "attachments"))?;
|
||||
}
|
||||
|
||||
// And then delete said messages.
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(projection, None)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("delete_many", COL))
|
||||
}
|
||||
}
|
||||
278
crates/core/database/src/models/messages/ops/reference.rs
Normal file
278
crates/core/database/src/models/messages/ops/reference.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
use futures::future::try_join_all;
|
||||
use indexmap::IndexSet;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
||||
|
||||
use super::AbstractMessages;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractMessages for ReferenceDb {
|
||||
/// Insert a new message into the database
|
||||
async fn insert_message(&self, message: &Message) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if messages.contains_key(&message.id) {
|
||||
Err(create_database_error!("insert", "message"))
|
||||
} else {
|
||||
messages.insert(message.id.to_string(), message.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a message by its id
|
||||
async fn fetch_message(&self, id: &str) -> Result<Message> {
|
||||
let messages = self.messages.lock().await;
|
||||
messages
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch multiple messages by given query
|
||||
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
|
||||
let messages = self.messages.lock().await;
|
||||
let matched_messages = messages
|
||||
.values()
|
||||
.filter(|message| {
|
||||
if let Some(channel) = &query.filter.channel {
|
||||
if &message.channel != channel {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(author) = &query.filter.author {
|
||||
if &message.author != author {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(query) = &query.filter.query {
|
||||
if let Some(content) = &message.content {
|
||||
if !content.to_lowercase().contains(query) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// FIXME: sorting, etc (will be required for tests)
|
||||
|
||||
Ok(matched_messages)
|
||||
|
||||
/*
|
||||
// 2. Find query limit
|
||||
let limit = query.limit.unwrap_or(50);
|
||||
|
||||
// 3. Apply message time period
|
||||
match query.time_period {
|
||||
MessageTimePeriod::Relative { nearby } => {
|
||||
// 3.1. Prepare filters
|
||||
let mut older_message_filter = filter.clone();
|
||||
let mut newer_message_filter = filter;
|
||||
|
||||
older_message_filter.insert(
|
||||
"_id",
|
||||
doc! {
|
||||
"$lt": &nearby
|
||||
},
|
||||
);
|
||||
|
||||
newer_message_filter.insert(
|
||||
"_id",
|
||||
doc! {
|
||||
"$gte": &nearby
|
||||
},
|
||||
);
|
||||
|
||||
// 3.2. Execute in both directions
|
||||
let (a, b) = try_join!(
|
||||
self.find_with_options::<_, Message>(
|
||||
COL,
|
||||
newer_message_filter,
|
||||
FindOptions::builder()
|
||||
.limit(limit / 2 + 1)
|
||||
.sort(doc! {
|
||||
"_id": 1_i32
|
||||
})
|
||||
.build(),
|
||||
),
|
||||
self.find_with_options::<_, Message>(
|
||||
COL,
|
||||
older_message_filter,
|
||||
FindOptions::builder()
|
||||
.limit(limit / 2)
|
||||
.sort(doc! {
|
||||
"_id": -1_i32
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
)
|
||||
.map_err(|_| create_database_error!("find", COL))?;
|
||||
|
||||
Ok([a, b].concat())
|
||||
}
|
||||
MessageTimePeriod::Absolute {
|
||||
before,
|
||||
after,
|
||||
sort,
|
||||
} => {
|
||||
// 3.1. Apply message ID filter
|
||||
if let Some(doc) = match (before, after) {
|
||||
(Some(before), Some(after)) => Some(doc! {
|
||||
"$lt": before,
|
||||
"$gt": after
|
||||
}),
|
||||
(Some(before), _) => Some(doc! {
|
||||
"$lt": before
|
||||
}),
|
||||
(_, Some(after)) => Some(doc! {
|
||||
"$gt": after
|
||||
}),
|
||||
_ => None,
|
||||
} {
|
||||
filter.insert("_id", doc);
|
||||
}
|
||||
|
||||
// 3.2. Execute with given message sort
|
||||
self.find_with_options(
|
||||
COL,
|
||||
filter,
|
||||
FindOptions::builder()
|
||||
.limit(limit)
|
||||
.sort(match sort.unwrap_or(MessageSort::Latest) {
|
||||
// Sort by relevance, fallback to latest
|
||||
MessageSort::Relevance => {
|
||||
if is_search_query {
|
||||
doc! {
|
||||
"score": {
|
||||
"$meta": "textScore"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
doc! {
|
||||
"_id": -1_i32
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort by latest first
|
||||
MessageSort::Latest => doc! {
|
||||
"_id": -1_i32
|
||||
},
|
||||
// Sort by oldest first
|
||||
MessageSort::Oldest => doc! {
|
||||
"_id": 1_i32
|
||||
},
|
||||
})
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Fetch multiple messages by given IDs
|
||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
|
||||
try_join_all(ids.iter().map(|id| self.fetch_message(id))).await
|
||||
}
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message_data) = messages.get_mut(id) {
|
||||
message_data.apply_options(message.to_owned());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message_data) = messages.get_mut(id) {
|
||||
if let Some(embeds) = &append.embeds {
|
||||
if !embeds.is_empty() {
|
||||
if let Some(embeds_data) = &mut message_data.embeds {
|
||||
embeds_data.extend(embeds.clone());
|
||||
} else {
|
||||
message_data.embeds = Some(embeds.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new reaction to a message
|
||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message) = messages.get_mut(id) {
|
||||
if let Some(users) = message.reactions.get_mut(emoji) {
|
||||
users.insert(user.to_string());
|
||||
} else {
|
||||
message
|
||||
.reactions
|
||||
.insert(emoji.to_string(), IndexSet::from([user.to_string()]));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message) = messages.get_mut(id) {
|
||||
if let Some(users) = message.reactions.get_mut(emoji) {
|
||||
users.remove(&user.to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove reaction from a message
|
||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message) = messages.get_mut(id) {
|
||||
message.reactions.remove(emoji);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a message from the database by its id
|
||||
async fn delete_message(&self, id: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if messages.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete messages from a channel by their ids and corresponding channel id
|
||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
|
||||
self.messages
|
||||
.lock()
|
||||
.await
|
||||
.retain(|id, message| message.channel != channel && !ids.contains(id));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
74
crates/core/database/src/models/mod.rs
Normal file
74
crates/core/database/src/models/mod.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
mod admin_migrations;
|
||||
mod bots;
|
||||
mod channel_invites;
|
||||
mod channel_unreads;
|
||||
mod channel_webhooks;
|
||||
mod channels;
|
||||
mod emojis;
|
||||
mod files;
|
||||
mod messages;
|
||||
mod ratelimit_events;
|
||||
mod safety_reports;
|
||||
mod safety_snapshots;
|
||||
mod server_bans;
|
||||
mod server_members;
|
||||
mod servers;
|
||||
mod user_settings;
|
||||
mod users;
|
||||
|
||||
pub use admin_migrations::*;
|
||||
pub use bots::*;
|
||||
pub use channel_invites::*;
|
||||
pub use channel_unreads::*;
|
||||
pub use channel_webhooks::*;
|
||||
pub use channels::*;
|
||||
pub use emojis::*;
|
||||
pub use files::*;
|
||||
pub use messages::*;
|
||||
pub use ratelimit_events::*;
|
||||
pub use safety_reports::*;
|
||||
pub use safety_snapshots::*;
|
||||
pub use server_bans::*;
|
||||
pub use server_members::*;
|
||||
pub use servers::*;
|
||||
pub use user_settings::*;
|
||||
pub use users::*;
|
||||
|
||||
use crate::{Database, MongoDb, ReferenceDb};
|
||||
|
||||
pub trait AbstractDatabase:
|
||||
Sync
|
||||
+ Send
|
||||
+ admin_migrations::AbstractMigrations
|
||||
+ bots::AbstractBots
|
||||
+ channels::AbstractChannels
|
||||
+ channel_invites::AbstractChannelInvites
|
||||
+ channel_unreads::AbstractChannelUnreads
|
||||
+ channel_webhooks::AbstractWebhooks
|
||||
+ emojis::AbstractEmojis
|
||||
+ files::AbstractAttachments
|
||||
+ messages::AbstractMessages
|
||||
+ ratelimit_events::AbstractRatelimitEvents
|
||||
+ safety_reports::AbstractReport
|
||||
+ safety_snapshots::AbstractSnapshot
|
||||
+ server_bans::AbstractServerBans
|
||||
+ server_members::AbstractServerMembers
|
||||
+ servers::AbstractServers
|
||||
+ user_settings::AbstractUserSettings
|
||||
+ users::AbstractUsers
|
||||
{
|
||||
}
|
||||
|
||||
impl AbstractDatabase for ReferenceDb {}
|
||||
impl AbstractDatabase for MongoDb {}
|
||||
|
||||
impl std::ops::Deref for Database {
|
||||
type Target = dyn AbstractDatabase;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match &self {
|
||||
Database::Reference(dummy) => dummy,
|
||||
Database::MongoDb(mongo) => mongo,
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/ratelimit_events/mod.rs
Normal file
5
crates/core/database/src/models/ratelimit_events/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
47
crates/core/database/src/models/ratelimit_events/model.rs
Normal file
47
crates/core/database/src/models/ratelimit_events/model.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::fmt;
|
||||
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
auto_derived!(
|
||||
/// Ratelimit Event
|
||||
pub struct RatelimitEvent {
|
||||
/// Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Relevant Object Id
|
||||
pub target_id: String,
|
||||
/// Type of event
|
||||
pub event_type: RatelimitEventType,
|
||||
}
|
||||
|
||||
/// Event type
|
||||
pub enum RatelimitEventType {
|
||||
DiscriminatorChange,
|
||||
}
|
||||
);
|
||||
|
||||
impl fmt::Display for RatelimitEventType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl RatelimitEvent {
|
||||
/// Create ratelimit event
|
||||
pub async fn create(
|
||||
db: &Database,
|
||||
target_id: String,
|
||||
event_type: RatelimitEventType,
|
||||
) -> Result<()> {
|
||||
db.insert_ratelimit_event(&RatelimitEvent {
|
||||
id: Ulid::new().to_string(),
|
||||
target_id,
|
||||
event_type,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
20
crates/core/database/src/models/ratelimit_events/ops.rs
Normal file
20
crates/core/database/src/models/ratelimit_events/ops.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractRatelimitEvents: Sync + Send {
|
||||
/// Insert a new ratelimit event
|
||||
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()>;
|
||||
|
||||
/// Count number of events in given duration and check if we've hit the limit
|
||||
async fn has_ratelimited(
|
||||
&self,
|
||||
target_id: &str,
|
||||
event_type: RatelimitEventType,
|
||||
period: Duration,
|
||||
count: usize,
|
||||
) -> Result<bool>;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use super::AbstractRatelimitEvents;
|
||||
use crate::{MongoDb, RatelimitEvent, RatelimitEventType};
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
static COL: &str = "ratelimit_events";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractRatelimitEvents for MongoDb {
|
||||
/// Insert a new ratelimit event
|
||||
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
|
||||
query!(self, insert_one, COL, &event).map(|_| ())
|
||||
}
|
||||
|
||||
/// Count number of events in given duration and check if we've hit the limit
|
||||
async fn has_ratelimited(
|
||||
&self,
|
||||
target_id: &str,
|
||||
event_type: RatelimitEventType,
|
||||
period: Duration,
|
||||
count: usize,
|
||||
) -> Result<bool> {
|
||||
self.col::<RatelimitEvent>(COL)
|
||||
.count_documents(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
|
||||
},
|
||||
"target_id": target_id,
|
||||
"event_type": event_type.to_string()
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|c| c as usize >= count)
|
||||
.map_err(|_| create_database_error!("count_documents", COL))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::AbstractRatelimitEvents;
|
||||
use crate::RatelimitEvent;
|
||||
use crate::RatelimitEventType;
|
||||
use crate::ReferenceDb;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractRatelimitEvents for ReferenceDb {
|
||||
/// Insert a new ratelimit event
|
||||
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
|
||||
let mut ratelimit_events = self.ratelimit_events.lock().await;
|
||||
if ratelimit_events.contains_key(&event.id) {
|
||||
Err(create_database_error!("insert", "message"))
|
||||
} else {
|
||||
ratelimit_events.insert(event.id.to_string(), event.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Count number of events in given duration and check if we've hit the limit
|
||||
async fn has_ratelimited(
|
||||
&self,
|
||||
target_id: &str,
|
||||
event_type: RatelimitEventType,
|
||||
period: Duration,
|
||||
count: usize,
|
||||
) -> Result<bool> {
|
||||
let ratelimit_events = self.ratelimit_events.lock().await;
|
||||
let gte_cmp_id = Ulid::from_datetime(SystemTime::now() - period).to_string();
|
||||
|
||||
Ok(ratelimit_events
|
||||
.iter()
|
||||
.filter(|(id, event)| {
|
||||
id.cmp(&>e_cmp_id) == Ordering::Greater
|
||||
&& event.target_id == target_id
|
||||
&& event.event_type == event_type
|
||||
})
|
||||
.count()
|
||||
>= count)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="9BKyaxnbqnTkbfLuLd1z" name="Page-1">
|
||||
<mxGraphModel dx="1033" dy="710" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<mxGraphModel dx="2466" dy="650" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
@@ -35,18 +35,12 @@
|
||||
<mxCell id="2" value="Attachment" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="220" y="490" width="80" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="49" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="3" target="5" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="195" y="340"/>
|
||||
<mxPoint x="245" y="340"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="51" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="3" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="290" y="160" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="95" y="340"/>
|
||||
<mxPoint x="195" y="340"/>
|
||||
<mxPoint x="195" y="260"/>
|
||||
<mxPoint x="260" y="260"/>
|
||||
<mxPoint x="260" y="220"/>
|
||||
@@ -54,11 +48,19 @@
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="68" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" edge="1" parent="1" source="3" target="5">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="95" y="340"/>
|
||||
<mxPoint x="245" y="340"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Channel Invite" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="150" y="400" width="90" height="30" as="geometry"/>
|
||||
<mxGeometry x="50" y="400" width="90" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="Channel Unread" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="400" width="100" height="30" as="geometry"/>
|
||||
<mxGeometry x="-70" y="400" width="100" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="43" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;dashed=1;" parent="1" source="5" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
@@ -66,6 +68,14 @@
|
||||
<mxCell id="60" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="5" target="9" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="67" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;startArrow=diamondThin;startFill=1;" edge="1" parent="1" source="5" target="66">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="245" y="370"/>
|
||||
<mxPoint x="195" y="370"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Channel" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="230" y="280" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
@@ -161,8 +171,8 @@
|
||||
<mxPoint x="290" y="160" as="targetPoint"/>
|
||||
<mxPoint x="200" y="400" as="sourcePoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="90" y="370"/>
|
||||
<mxPoint x="195" y="370"/>
|
||||
<mxPoint x="-20" y="340"/>
|
||||
<mxPoint x="195" y="340"/>
|
||||
<mxPoint x="195" y="260"/>
|
||||
<mxPoint x="260" y="260"/>
|
||||
<mxPoint x="260" y="220"/>
|
||||
@@ -199,6 +209,9 @@
|
||||
<mxPoint x="320" y="440" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="66" value="Webhook" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="160" y="400" width="70" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
5
crates/core/database/src/models/safety_reports/mod.rs
Normal file
5
crates/core/database/src/models/safety_reports/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
22
crates/core/database/src/models/safety_reports/model.rs
Normal file
22
crates/core/database/src/models/safety_reports/model.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use revolt_models::v0::{ReportStatus, ReportedContent};
|
||||
|
||||
auto_derived!(
|
||||
/// User-generated platform moderation report
|
||||
pub struct Report {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Id of the user creating this report
|
||||
pub author_id: String,
|
||||
/// Reported content
|
||||
pub content: ReportedContent,
|
||||
/// Additional report context
|
||||
pub additional_context: String,
|
||||
/// Status of the report
|
||||
#[serde(flatten)]
|
||||
pub status: ReportStatus,
|
||||
/// Additional notes included on the report
|
||||
#[serde(default)]
|
||||
pub notes: String,
|
||||
}
|
||||
);
|
||||
@@ -1,5 +1,9 @@
|
||||
use crate::models::Report;
|
||||
use crate::Result;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Report;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractReport: Sync + Send {
|
||||
@@ -0,0 +1,16 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::Report;
|
||||
|
||||
use super::AbstractReport;
|
||||
|
||||
static COL: &str = "safety_reports";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractReport for MongoDb {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()> {
|
||||
query!(self, insert_one, COL, &report).map(|_| ())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user