Compare commits
230
Commits
20230810-3
...
20240829-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28960979ec | ||
|
|
1938ebc3fa | ||
|
|
1fb4032d9e | ||
|
|
ae2194cce8 | ||
|
|
949a0cf649 | ||
|
|
f6c57b23b4 | ||
|
|
12ae781621 | ||
|
|
a52d610e6c | ||
|
|
f513a9cb5e | ||
|
|
535f01604b | ||
|
|
f8ec6ba5ff | ||
|
|
b8fdc07508 | ||
|
|
5ce7530206 | ||
|
|
b36bac3225 | ||
|
|
beef06ebe3 | ||
|
|
4fc46f765b | ||
|
|
b45ae2cd4d | ||
|
|
0a8089787b | ||
|
|
475fa6f2e6 | ||
|
|
96bf73cb57 | ||
|
|
87a9fb79f2 | ||
|
|
c50435d499 | ||
|
|
d2f4823ceb | ||
|
|
39230c559a | ||
|
|
32d1d5df2e | ||
|
|
7547fbe245 | ||
|
|
9f13cb6f47 | ||
|
|
d94571fbf9 | ||
|
|
389ecc0e5c | ||
|
|
412f4a99d7 | ||
|
|
caa0795d07 | ||
|
|
5c82343bb3 | ||
|
|
e17af1c064 | ||
|
|
a9e309395e | ||
|
|
8468ce19cb | ||
|
|
27f15f7b02 | ||
|
|
78b9fa168b | ||
|
|
e290d168ac | ||
|
|
78cd89ec32 | ||
|
|
f9d9059e73 | ||
|
|
0954d71f51 | ||
|
|
9ea2bd9f2f | ||
|
|
d6bcb844db | ||
|
|
ce20e689cc | ||
|
|
d9cfc9d0e1 | ||
|
|
64a07d09f4 | ||
|
|
00e881799f | ||
|
|
fa598dd6f8 | ||
|
|
ac4e2cb10b | ||
|
|
ec578ab0ef | ||
|
|
ea6ba59841 | ||
|
|
f903f716e3 | ||
|
|
c5f4b94aa5 | ||
|
|
1d5dae4751 | ||
|
|
ac20b6bc99 | ||
|
|
6ec8007e4e | ||
|
|
e5eea267cf | ||
|
|
de5add09d0 | ||
|
|
1ec8f46c1d | ||
|
|
80666848cc | ||
|
|
e8e9613169 | ||
|
|
eda36436a8 | ||
|
|
93e05e9f18 | ||
|
|
090d8cba71 | ||
|
|
f16e72e329 | ||
|
|
8248a4a5b8 | ||
|
|
cf0b4274c8 | ||
|
|
36ecb48c7b | ||
|
|
ba411fef43 | ||
|
|
4055363cff | ||
|
|
d179d1e695 | ||
|
|
fc9ceb7540 | ||
|
|
a31a3e2e65 | ||
|
|
b12e728514 | ||
|
|
962c7d62c7 | ||
|
|
d677716f93 | ||
|
|
8099310f89 | ||
|
|
5c40f66010 | ||
|
|
8e7dd21bce | ||
|
|
4868205df4 | ||
|
|
96fb0eecca | ||
|
|
2cb20618da | ||
|
|
92e948aabc | ||
|
|
cb4435d700 | ||
|
|
4c6e78e1a5 | ||
|
|
da9a91e05f | ||
|
|
506968634e | ||
|
|
a5d0cdf0dd | ||
|
|
c59aad43c8 | ||
|
|
19e72babc9 | ||
|
|
bfcdb13d6d | ||
|
|
878c1a83ad | ||
|
|
471e0f55e4 | ||
|
|
08127218ce | ||
|
|
b69202c222 | ||
|
|
b3494b12fc | ||
|
|
7703475868 | ||
|
|
4be3bdc4c3 | ||
|
|
3e26e7e89d | ||
|
|
13b95d383e | ||
|
|
3c76005c33 | ||
|
|
3065e153d5 | ||
|
|
a1eeedabb9 | ||
|
|
0fc59b8e2a | ||
|
|
265d3ba8ef | ||
|
|
20d398d02c | ||
|
|
6bc74749d2 | ||
|
|
301676fb54 | ||
|
|
6b488f347e | ||
|
|
1a8e43d280 | ||
|
|
acb4190f70 | ||
|
|
9e42567050 | ||
|
|
78fb6e1982 | ||
|
|
9de963fb56 | ||
|
|
03340a2b5a | ||
|
|
b2d3344ddd | ||
|
|
49bb235938 | ||
|
|
4e7049d51e | ||
|
|
2de7598f8d | ||
|
|
f6a565385e | ||
|
|
569bd1d5e1 | ||
|
|
aca1fe6dff | ||
|
|
aded2e3239 | ||
|
|
54a4eff623 | ||
|
|
50c36dcefd | ||
|
|
f16c275bcb | ||
|
|
74ff072199 | ||
|
|
52431bd1dd | ||
|
|
fa67e87467 | ||
|
|
8f9b18444a | ||
|
|
7b18e983c9 | ||
|
|
c0e64c9052 | ||
|
|
0d54ccd342 | ||
|
|
d4d5b23c7d | ||
|
|
a641034895 | ||
|
|
9dd24526ef | ||
|
|
6d4f772d78 | ||
|
|
4c4dada3da | ||
|
|
cf82972a80 | ||
|
|
ea00f0fec1 | ||
|
|
226dbca6e0 | ||
|
|
e4b5a23f71 | ||
|
|
9663caa1ff | ||
|
|
f9f0f409a8 | ||
|
|
8f4d6c864b | ||
|
|
513461c99a | ||
|
|
41ef76ff3e | ||
|
|
c451e431cd | ||
|
|
09a848f5a6 | ||
|
|
308193377d | ||
|
|
3d3326e68a | ||
|
|
88c2232a6c | ||
|
|
25016efbc2 | ||
|
|
db63ac4064 | ||
|
|
1f593a1708 | ||
|
|
219c16a69c | ||
|
|
aed8f69f34 | ||
|
|
196a856c68 | ||
|
|
9bf60e87e9 | ||
|
|
b90e7a4412 | ||
|
|
cb813e91f0 | ||
|
|
a6bc9fcbb3 | ||
|
|
cd0737911b | ||
|
|
7705354d4d | ||
|
|
39ac8b2278 | ||
|
|
c5494aa1ca | ||
|
|
1a96bf5a4b | ||
|
|
37a1244751 | ||
|
|
bf39f18f8d | ||
|
|
95be7dc87a | ||
|
|
9be171c7b6 | ||
|
|
786fbb8ae4 | ||
|
|
6c0210e8a1 | ||
|
|
456bf7b42a | ||
|
|
21ffea0f9f | ||
|
|
96d90215d2 | ||
|
|
29dcf714f2 | ||
|
|
cb97004d3f | ||
|
|
0b6e5dd9db | ||
|
|
7132877201 | ||
|
|
4d42fb74e7 | ||
|
|
866ace3381 | ||
|
|
0798e81862 | ||
|
|
2fa5ac41ac | ||
|
|
25ae1555a2 | ||
|
|
881888bd1c | ||
|
|
41f20c2239 | ||
|
|
38f1381fea | ||
|
|
6838b0b1f0 | ||
|
|
055deab105 | ||
|
|
6d2073120d | ||
|
|
9789909061 | ||
|
|
895de86f1e | ||
|
|
98b8c5403b | ||
|
|
aa7c75c60e | ||
|
|
4270f0c5d4 | ||
|
|
eb1f45d208 | ||
|
|
9929f7499a | ||
|
|
279d9ef1b5 | ||
|
|
5a9bb9e68d | ||
|
|
a8fc9e928a | ||
|
|
da231ae390 | ||
|
|
7a31da4189 | ||
|
|
7d0b5392b5 | ||
|
|
1e1d44c0e4 | ||
|
|
1c4588967f | ||
|
|
853366a297 | ||
|
|
7568f49755 | ||
|
|
0542788567 | ||
|
|
73f7b8f007 | ||
|
|
c2412190a8 | ||
|
|
5372296dc0 | ||
|
|
41a47bdf8f | ||
|
|
51c26e324d | ||
|
|
3a55d00c6a | ||
|
|
004e84ece9 | ||
|
|
3d0914ce52 | ||
|
|
bde432cb75 | ||
|
|
28d225952a | ||
|
|
2fb9e7a802 | ||
|
|
431046cb4c | ||
|
|
92032cd9bc | ||
|
|
bfdd713d38 | ||
|
|
d5ba4ebf0c | ||
|
|
3bfead4ff1 | ||
|
|
d04621b1d6 | ||
|
|
a681df04bd | ||
|
|
9f3c1036d0 | ||
|
|
c9011ac692 | ||
|
|
42f977f536 |
@@ -0,0 +1,2 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "3s", terminate-after = 2 }
|
||||
@@ -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
|
||||
@@ -80,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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -2,20 +2,21 @@ 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:
|
||||
|
||||
+37
-22
@@ -2,8 +2,16 @@ name: Rust build, test, and generate specification
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/rust.yaml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -20,6 +28,11 @@ 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
|
||||
@@ -28,38 +41,40 @@ jobs:
|
||||
|
||||
- name: Run services in background
|
||||
run: |
|
||||
docker-compose -f docker-compose.db.yml up -d
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
- name: Run cargo test (with MongoDB)
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
MONGODB: mongodb://localhost
|
||||
with:
|
||||
command: test
|
||||
docker compose -f docker-compose.db.yml up -d
|
||||
|
||||
- name: Copy .env.example
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
- name: Run cargo test
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo run --bin revolt-delta &
|
||||
cargo nextest run
|
||||
|
||||
- name: Run cargo test (with MongoDB)
|
||||
env:
|
||||
TEST_DB: MONGODB
|
||||
MONGODB: mongodb://localhost
|
||||
run: |
|
||||
cargo nextest run
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
|
||||
|
||||
- name: Wait for API to go up
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
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' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: revoltchat/api
|
||||
@@ -67,11 +82,11 @@ jobs:
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
with:
|
||||
cwd: "api"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Rocket.toml
|
||||
Revolt.toml
|
||||
|
||||
target
|
||||
.data
|
||||
.env
|
||||
|
||||
Vendored
+5
-3
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+2951
-735
File diff suppressed because it is too large
Load Diff
+12
-2
@@ -1,6 +1,16 @@
|
||||
[workspace]
|
||||
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/delta",
|
||||
"crates/bonfire",
|
||||
"crates/core/*",
|
||||
"crates/services/*",
|
||||
"crates/bindings/*",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||
redis = { git = "https://github.com/insertish/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
|
||||
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" }
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ RUN sh /tmp/build-image-layer.sh tools
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/quark/Cargo.toml ./crates/quark/
|
||||
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/
|
||||
|
||||
@@ -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
|
||||
@@ -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/>
|
||||
|
||||
@@ -2,34 +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
|
||||
|
||||
### Revolt
|
||||
|
||||
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
|
||||
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
|
||||
- [Contribution Guide](https://developers.revolt.chat/contributing)
|
||||
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
Rust 1.70 or higher.
|
||||
Rust 1.76 or higher.
|
||||
|
||||
## Contributing
|
||||
## Development Guide
|
||||
|
||||
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 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/).
|
||||
|
||||
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!**
|
||||
|
||||
+16
-3
@@ -1,15 +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::safety_strikes::ops::AbstractAccountStrikes::insert_account_strike",
|
||||
"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",
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
|
||||
|
||||
# Prefer to use Object::delete(&self)
|
||||
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
target
|
||||
index.node
|
||||
**/node_modules
|
||||
**/.DS_Store
|
||||
npm-debug.log*
|
||||
cargo.log
|
||||
cross.log
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "revolt-nodejs-bindings"
|
||||
version = "0.7.16"
|
||||
description = "Node.js bindings for the Revolt software"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
exclude = ["index.node"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
neon = "1.0.0"
|
||||
neon-serde4 = "1.0.0"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
async-std = "1.12.0"
|
||||
|
||||
revolt-result = { version = "0.7.16", path = "../../core/result" }
|
||||
revolt-database = { version = "0.7.16", path = "../../core/database" }
|
||||
@@ -0,0 +1,92 @@
|
||||
# revolt.node
|
||||
|
||||
**revolt.node:** Node.js bindings for the Revolt software
|
||||
|
||||
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
|
||||
|
||||
## Building revolt.node
|
||||
|
||||
Building revolt.node requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
|
||||
|
||||
To run the build, run:
|
||||
|
||||
```sh
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`.
|
||||
|
||||
## Exploring revolt.node
|
||||
|
||||
After building revolt.node, you can explore its exports at the Node console:
|
||||
|
||||
```sh
|
||||
$ npm i
|
||||
$ npm run build
|
||||
$ node
|
||||
> require('.').hello()
|
||||
'hello node'
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
#### `npm install`
|
||||
|
||||
Installs the project, including running `npm run build`.
|
||||
|
||||
#### `npm run build`
|
||||
|
||||
Builds the Node addon (`index.node`) from source, generating a release build with `cargo --release`.
|
||||
|
||||
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm run build` and similar commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
|
||||
|
||||
```
|
||||
npm run build -- --feature=beetle
|
||||
```
|
||||
|
||||
#### `npm run debug`
|
||||
|
||||
Similar to `npm run build` but generates a debug build with `cargo`.
|
||||
|
||||
#### `npm run cross`
|
||||
|
||||
Similar to `npm run build` but uses [cross-rs](https://github.com/cross-rs/cross) to cross-compile for another platform. Use the [`CARGO_BUILD_TARGET`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget) environment variable to select the build target.
|
||||
|
||||
#### `npm test`
|
||||
|
||||
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
|
||||
|
||||
## Project Layout
|
||||
|
||||
The directory structure of this project is:
|
||||
|
||||
```
|
||||
revolt.node/
|
||||
├── Cargo.toml
|
||||
├── README.md
|
||||
├── src/
|
||||
| └── lib.rs
|
||||
├── index.node
|
||||
├── package.json
|
||||
└── target/
|
||||
```
|
||||
|
||||
| Entry | Purpose |
|
||||
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
|
||||
| `README.md` | This file. |
|
||||
| `src/` | The directory tree containing the Rust source code for the project. |
|
||||
| `lib.rs` | Entry point for the Rust source code. |
|
||||
| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. |
|
||||
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
|
||||
| `target/` | Binary artifacts generated by the Rust build. |
|
||||
|
||||
## Learn More
|
||||
|
||||
Learn more about:
|
||||
|
||||
- [Neon](https://neon-bindings.com).
|
||||
- [Rust](https://www.rust-lang.org).
|
||||
- [Node](https://nodejs.org).
|
||||
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
import { Channel, User } from "revolt-api";
|
||||
|
||||
/**
|
||||
* Opaque type for Revolt database
|
||||
*/
|
||||
export declare interface Database {}
|
||||
|
||||
/**
|
||||
* Opaque type for Revolt database
|
||||
*/
|
||||
export declare interface OpaqueUser {}
|
||||
|
||||
/**
|
||||
* Error type from Revolt backend
|
||||
*/
|
||||
export declare interface Err {
|
||||
type: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a new handle to the Revolt database
|
||||
* @returns {Database} Handle
|
||||
*/
|
||||
export declare function database(): Database;
|
||||
|
||||
/**
|
||||
* Fetch user from database
|
||||
* @param {string} userId User's ID
|
||||
* @this {Database}
|
||||
*/
|
||||
export declare function database_fetch_user(userId: string): OpaqueUser;
|
||||
|
||||
/**
|
||||
* Fetch user from database
|
||||
* @param {string} username Username
|
||||
* @param {string} discriminator Discriminator
|
||||
* @this {Database}
|
||||
*/
|
||||
export declare function database_fetch_user_by_username(
|
||||
username: string,
|
||||
discriminator: string
|
||||
): OpaqueUser;
|
||||
|
||||
/**
|
||||
* Gets model data as JSON
|
||||
* @this {OpaqueUser}
|
||||
*/
|
||||
export declare function model_data(): User;
|
||||
|
||||
/**
|
||||
* Gets error if the model failed to fetch
|
||||
* @this {OpaqueUser}
|
||||
*/
|
||||
export declare function model_error(): Err;
|
||||
|
||||
/**
|
||||
* Open a direct message channel between two users
|
||||
* @param {string} userA User A ID
|
||||
* @param {string} userB User B ID
|
||||
* @returns Existing or newly created channel
|
||||
*/
|
||||
export declare function proc_channels_create_dm(
|
||||
userA: string,
|
||||
userB: string
|
||||
): Promise<Channel & { error: Err }>;
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "revolt-nodejs-bindings",
|
||||
"version": "0.7.15",
|
||||
"description": "Node.js bindings for the Revolt software",
|
||||
"main": "index.node",
|
||||
"scripts": {
|
||||
"test": "cargo test",
|
||||
"cargo-build": "cargo build --message-format=json > cargo.log",
|
||||
"cross-build": "cross build --message-format=json > cross.log",
|
||||
"postcargo-build": "neon dist < cargo.log",
|
||||
"postcross-build": "neon dist -m /target < cross.log",
|
||||
"debug": "npm run cargo-build --",
|
||||
"build": "npm run cargo-build -- --release",
|
||||
"cross": "npm run cross-build -- --release"
|
||||
},
|
||||
"author": "Paul Makles",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@neon-rs/cli": "0.1.73"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/revoltchat/backend"
|
||||
},
|
||||
"keywords": [
|
||||
"revolt",
|
||||
"chat"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/revoltchat/backend/issues"
|
||||
},
|
||||
"homepage": "https://github.com/revoltchat/backend#readme",
|
||||
"dependencies": {
|
||||
"revolt-api": "^0.7.15"
|
||||
}
|
||||
}
|
||||
Generated
+117
@@ -0,0 +1,117 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
revolt-api:
|
||||
specifier: ^0.7.15
|
||||
version: 0.7.15
|
||||
|
||||
devDependencies:
|
||||
'@neon-rs/cli':
|
||||
specifier: 0.1.73
|
||||
version: 0.1.73
|
||||
|
||||
packages:
|
||||
|
||||
/@cargo-messages/android-arm-eabi@0.1.72:
|
||||
resolution: {integrity: sha512-gGZxIM1mj+Y5x+ULND6ZCNr7f70OJi9wDlycSK8hGONy9wrChN6JAIHryddC5cqcwlYAoQ6IDcDFElnhAYbybA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/darwin-arm64@0.1.72:
|
||||
resolution: {integrity: sha512-EAzN5MLaXPljZKZDO5qR+aBs44eSq2ZbEnS7AI/FziE3MzeXbrGOS3fLba5+7yWPFXJyZolXzePm8N1EBv8ovg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/darwin-x64@0.1.72:
|
||||
resolution: {integrity: sha512-RLo6j8s3nYbjdd1LDct4wamfChyRit7zokUuxtIYCu9XOlltkN5vnj1vwnrPvoqCMZ/7CbbuHFwSTn9A71de/w==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/linux-arm-gnueabihf@0.1.72:
|
||||
resolution: {integrity: sha512-tHsRshuzfjrX6SDW3jg6al8vMNLTMgczGnVYl5RuBZf/yrAUuwe30KxA9ge6w6mW6Ox797DyBchzAc9OLgTgmQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/linux-x64-gnu@0.1.72:
|
||||
resolution: {integrity: sha512-VGtL6CCnUbhsP4aYuBNT5kfrAL7o0qjrxw97a+ax13t+nJd26tVEEIKHMu5drvvS/Nm/hn7sLT8zMnnCv0pvHg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/win32-arm64-msvc@0.1.72:
|
||||
resolution: {integrity: sha512-V93Cgz39K+yqa3MveNbhh29pYCp8izK5uEavjPoxlNxAbsMCWH+s0verGDdUcfGxjR1H2V7oZ4FszPqR2SqMRQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/win32-x64-msvc@0.1.72:
|
||||
resolution: {integrity: sha512-knz3uSrO0OSbq3U5VWfCY8FB4NsM43BOWLZ7x4sfaMOC1XWv+IyvDdkLe6DhJx8KUw46KIAimYs9YROrp6l46Q==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@neon-rs/cli@0.1.73:
|
||||
resolution: {integrity: sha512-1kv8S/feB6UQWQQwsnGfkSkEBOtlFDNExnioL81E2BwvUWgjQPaseHgpi2EpWVgsPUgur5eBm4QowmlpWkD4/w==}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
'@cargo-messages/android-arm-eabi': 0.1.72
|
||||
'@cargo-messages/darwin-arm64': 0.1.72
|
||||
'@cargo-messages/darwin-x64': 0.1.72
|
||||
'@cargo-messages/linux-arm-gnueabihf': 0.1.72
|
||||
'@cargo-messages/linux-x64-gnu': 0.1.72
|
||||
'@cargo-messages/win32-arm64-msvc': 0.1.72
|
||||
'@cargo-messages/win32-x64-msvc': 0.1.72
|
||||
dev: true
|
||||
|
||||
/axios@0.26.1:
|
||||
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/follow-redirects@1.15.6:
|
||||
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dev: false
|
||||
|
||||
/lodash.defaultsdeep@4.6.1:
|
||||
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
|
||||
dev: false
|
||||
|
||||
/revolt-api@0.7.15:
|
||||
resolution: {integrity: sha512-rWRnjBFvoXt/RHRxHg9KuP+x1Jk61KdsOpKOczVJg5tIqGDmqp/6QZHw5rJTtG+wCH3Ah3YKwY+MM5NHfg1W3Q==}
|
||||
dependencies:
|
||||
axios: 0.26.1
|
||||
lodash.defaultsdeep: 4.6.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
@@ -0,0 +1,190 @@
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
use neon::prelude::*;
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
struct DatabaseBinding(Database, Channel);
|
||||
impl Finalize for DatabaseBinding {}
|
||||
impl DatabaseBinding {
|
||||
fn take(&self) -> (Database, Channel) {
|
||||
(self.0.clone(), self.1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn js_database(mut cx: FunctionContext) -> JsResult<JsBox<DatabaseBinding>> {
|
||||
let db = async_std::task::block_on(DatabaseInfo::Auto.connect())
|
||||
.or_else(|err| cx.throw_error(err))?;
|
||||
|
||||
let channel = cx.channel();
|
||||
Ok(cx.boxed(DatabaseBinding(db, channel)))
|
||||
}
|
||||
|
||||
// Implementations for models
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Model {
|
||||
User(revolt_database::User),
|
||||
Error(revolt_result::Error),
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn give(&self) -> Model {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Finalize for Model {}
|
||||
|
||||
macro_rules! shim_boxed {
|
||||
($cx: ident, $name: ident, $model: ident, $( $variable: ident $type: ident $id: expr )+, $cmd: ident, $( $arg: expr, )+) => {
|
||||
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||
$(
|
||||
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||
)+
|
||||
|
||||
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||
let (deferred, promise) = cx.promise();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
let result = db.$cmd($($arg,)+).await;
|
||||
deferred.settle_with(&channel, move |mut cx| {
|
||||
Ok(cx.boxed(match result {
|
||||
Ok(value) => Model::$model(value),
|
||||
Err(error) => Model::Error(error)
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
$cx.export_function(stringify!($name), $name)?;
|
||||
};
|
||||
}
|
||||
|
||||
fn js_data(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
match cx.this::<JsBox<Model>>()?.give() {
|
||||
Model::Error(_) => neon_serde4::to_value(&mut cx, &None::<()>),
|
||||
Model::User(user) => neon_serde4::to_value(&mut cx, &user),
|
||||
}
|
||||
.or_else(|e| cx.throw_error(e.to_string()))
|
||||
}
|
||||
|
||||
fn js_error(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
let value = match cx.this::<JsBox<Model>>()?.give() {
|
||||
Model::Error(err) => Some(err),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
neon_serde4::to_value(&mut cx, &value).or_else(|e| cx.throw_error(e.to_string()))
|
||||
}
|
||||
|
||||
// Basic data implementation
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ResultBinding<T> {
|
||||
#[serde(flatten)]
|
||||
value: Option<T>,
|
||||
error: Option<revolt_result::Error>,
|
||||
}
|
||||
|
||||
macro_rules! shim {
|
||||
($cx: ident, $name: ident, $( $variable: ident $type: ident $id: expr )*, $( $model: ident $modelType: ident $modelId: expr )*, | $db: ident | $closure: expr, $( $arg: expr, )+) => {
|
||||
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||
$(
|
||||
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||
)*
|
||||
|
||||
$(
|
||||
let mut $model = if let Model::$modelType(value) = cx.argument::<JsBox<Model>>($modelId)?.give() {
|
||||
value
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
)*
|
||||
|
||||
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||
let (deferred, promise) = cx.promise();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
let result = (|$db: $crate::Database| $closure)(db.clone()).await;
|
||||
deferred.settle_with(&channel, move |mut cx| {
|
||||
neon_serde4::to_value(
|
||||
&mut cx,
|
||||
&match result {
|
||||
Ok(value) => ResultBinding {
|
||||
value: Some(value),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => ResultBinding {
|
||||
value: None,
|
||||
error: Some(error),
|
||||
},
|
||||
},
|
||||
)
|
||||
.or_else(|e| cx.throw_error(e.to_string()))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
$cx.export_function(stringify!($name), $name)?;
|
||||
};
|
||||
}
|
||||
|
||||
#[neon::main]
|
||||
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||
// database & model stuff
|
||||
cx.export_function("database", js_database)?;
|
||||
cx.export_function("model_data", js_data)?;
|
||||
cx.export_function("model_error", js_error)?;
|
||||
|
||||
shim_boxed!(
|
||||
cx,
|
||||
database_fetch_user,
|
||||
User,
|
||||
user_id JsString 0,
|
||||
fetch_user,
|
||||
&user_id,
|
||||
);
|
||||
|
||||
shim_boxed!(
|
||||
cx,
|
||||
database_fetch_user_by_username,
|
||||
User,
|
||||
username JsString 0
|
||||
discriminator JsString 1,
|
||||
fetch_user_by_username,
|
||||
&username, &discriminator,
|
||||
);
|
||||
|
||||
// procedure calls
|
||||
shim!(
|
||||
cx,
|
||||
proc_channels_create_dm,
|
||||
user_a JsString 0
|
||||
user_b JsString 1,
|
||||
,
|
||||
|db| async move {
|
||||
let user_a = db.fetch_user(&user_a).await?;
|
||||
let user_b = db.fetch_user(&user_b).await?;
|
||||
revolt_database::Channel::create_dm(&db, &user_a, &user_b).await
|
||||
},
|
||||
&userA, &userB,
|
||||
);
|
||||
|
||||
shim!(
|
||||
cx,
|
||||
proc_users_suspend,
|
||||
,
|
||||
user User 0,
|
||||
|db| async move {
|
||||
user.suspend(&db).await
|
||||
},
|
||||
&user,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
const Internal = require(".");
|
||||
|
||||
// playing around with class wrapper, not practical
|
||||
class Model {
|
||||
constructor(model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
data() {
|
||||
return Internal.model_data.bind(this.model)();
|
||||
}
|
||||
|
||||
error() {
|
||||
return Internal.model_error.bind(this.model)();
|
||||
}
|
||||
}
|
||||
|
||||
class User extends Model {
|
||||
constructor(db, user) {
|
||||
super(user);
|
||||
this.db = db;
|
||||
}
|
||||
}
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.db = Internal.database();
|
||||
}
|
||||
|
||||
async fetchUser(userId) {
|
||||
return new User(
|
||||
this,
|
||||
await Internal.database_fetch_user.bind(this.db)(userId)
|
||||
);
|
||||
}
|
||||
|
||||
async fetchUserByUsername(username, discriminator) {
|
||||
return new User(
|
||||
this,
|
||||
await Internal.database_fetch_user_by_username.bind(this.db)(
|
||||
username,
|
||||
discriminator
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const db = new Database();
|
||||
db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data()));
|
||||
db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data()));
|
||||
db.fetchUserByUsername("dos", "1111").then((user) =>
|
||||
console.info(user.error())
|
||||
);
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "bindings",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.6.6"
|
||||
version = "0.7.16"
|
||||
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"
|
||||
@@ -32,4 +36,13 @@ async-std = { version = "1.8.0", features = [
|
||||
] }
|
||||
|
||||
# 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.16", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_quark::{Error, Result};
|
||||
use revolt_database::events::client::ReadyPayloadFields;
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Enumeration of supported protocol formats
|
||||
@@ -37,16 +38,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,6 +84,17 @@ impl ProtocolConfiguration {
|
||||
pub fn get_protocol_format(&self) -> &ProtocolFormat {
|
||||
&self.format
|
||||
}
|
||||
|
||||
/// Get ready payload fields
|
||||
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
|
||||
vec![
|
||||
ReadyPayloadFields::Users,
|
||||
ReadyPayloadFields::Servers,
|
||||
ReadyPayloadFields::Channels,
|
||||
ReadyPayloadFields::Members,
|
||||
ReadyPayloadFields::Emoji,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Object holding one side of a channel for receiving the parsed information
|
||||
|
||||
@@ -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,16 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
get_relationship,
|
||||
models::{
|
||||
server_member::FieldsMember,
|
||||
user::{PartialUser, Presence, RelationshipStatus},
|
||||
Channel, Member, User,
|
||||
},
|
||||
perms, Database, Permission, Result,
|
||||
use revolt_database::{
|
||||
events::client::{EventV1, ReadyPayloadFields},
|
||||
util::permissions::DatabasePermissionQuery,
|
||||
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::{
|
||||
client::EventV1,
|
||||
state::{Cache, State},
|
||||
};
|
||||
use super::state::{Cache, State};
|
||||
|
||||
/// Cache Manager
|
||||
impl Cache {
|
||||
@@ -25,20 +20,22 @@ impl Cache {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { 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,
|
||||
}
|
||||
@@ -63,7 +60,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
|
||||
@@ -94,8 +91,13 @@ impl Cache {
|
||||
/// State Manager
|
||||
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();
|
||||
pub async fn generate_ready_payload(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
fields: Vec<ReadyPayloadFields>,
|
||||
) -> Result<EventV1> {
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
@@ -141,7 +143,6 @@ impl State {
|
||||
|
||||
// Fetch presence data for known users.
|
||||
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
|
||||
user.online = Some(true);
|
||||
|
||||
// Fetch user data.
|
||||
let users = db
|
||||
@@ -154,15 +155,36 @@ impl State {
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
|
||||
Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch user settings
|
||||
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
|
||||
.iter()
|
||||
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
|
||||
{
|
||||
Some(db.fetch_user_settings(&user.id, &keys).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch channel unreads
|
||||
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
|
||||
Some(db.fetch_unreads(&user.id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Copy data into local state cache.
|
||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
@@ -176,40 +198,61 @@ impl State {
|
||||
.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,
|
||||
users: if fields.contains(&ReadyPayloadFields::Users) {
|
||||
Some(users)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
servers: if fields.contains(&ReadyPayloadFields::Servers) {
|
||||
Some(servers.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
channels: if fields.contains(&ReadyPayloadFields::Channels) {
|
||||
Some(channels.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
members: if fields.contains(&ReadyPayloadFields::Members) {
|
||||
Some(members.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
|
||||
user_settings,
|
||||
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -243,11 +286,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 });
|
||||
@@ -270,8 +313,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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +339,7 @@ impl State {
|
||||
} {
|
||||
let event = EventV1::UserUpdate {
|
||||
id: self.cache.user_id.clone(),
|
||||
data: PartialUser {
|
||||
data: v0::PartialUser {
|
||||
online: Some(target),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -343,8 +386,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, ..
|
||||
@@ -357,10 +400,10 @@ impl State {
|
||||
|
||||
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) {
|
||||
@@ -374,7 +417,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() };
|
||||
@@ -383,17 +426,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,16 +444,28 @@ impl State {
|
||||
id,
|
||||
server,
|
||||
channels,
|
||||
emojis: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone());
|
||||
self.cache.servers.insert(id.clone(), server.clone());
|
||||
let member = Member::new(id.clone(), self.cache.user_id.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());
|
||||
@@ -420,10 +475,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() {
|
||||
@@ -433,13 +488,13 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -447,11 +502,11 @@ impl State {
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -461,13 +516,13 @@ impl State {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -482,10 +537,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,12 +576,26 @@ impl State {
|
||||
*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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,50 +609,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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod r#impl;
|
||||
pub mod state;
|
||||
@@ -1,8 +1,13 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::models::{Channel, Member, Server, User};
|
||||
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
pub enum SubscriptionStateChange {
|
||||
@@ -31,6 +36,7 @@ pub enum SubscriptionStateChange {
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub user_id: String,
|
||||
pub is_bot: bool,
|
||||
|
||||
pub users: HashMap<String, User>,
|
||||
pub channels: HashMap<String, Channel>,
|
||||
@@ -44,6 +50,7 @@ impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Cache {
|
||||
user_id: Default::default(),
|
||||
is_bot: false,
|
||||
|
||||
users: Default::default(),
|
||||
channels: Default::default(),
|
||||
@@ -59,14 +66,17 @@ impl Default for Cache {
|
||||
pub struct State {
|
||||
pub cache: Cache,
|
||||
|
||||
pub session_id: String,
|
||||
pub private_topic: String,
|
||||
subscribed: HashSet<String>,
|
||||
state: SubscriptionStateChange,
|
||||
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) -> State {
|
||||
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());
|
||||
@@ -81,22 +91,61 @@ impl State {
|
||||
|
||||
State {
|
||||
cache,
|
||||
subscribed,
|
||||
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 fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
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 {
|
||||
self.subscribed.insert(id.clone());
|
||||
subscribed.insert(id.clone());
|
||||
}
|
||||
|
||||
for id in remove {
|
||||
self.subscribed.remove(id);
|
||||
subscribed.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,20 +157,16 @@ impl State {
|
||||
self.cache.users.get(&self.cache.user_id).unwrap().clone()
|
||||
}
|
||||
|
||||
/// Iterate through all subscriptions
|
||||
pub fn iter_subscriptions(&self) -> std::collections::hash_set::Iter<'_, std::string::String> {
|
||||
self.subscribed.iter()
|
||||
}
|
||||
|
||||
/// Reset the current state
|
||||
pub fn reset_state(&mut self) {
|
||||
pub async fn reset_state(&mut self) {
|
||||
self.state = SubscriptionStateChange::Reset;
|
||||
self.subscribed.clear();
|
||||
self.subscribed.write().await.clear();
|
||||
}
|
||||
|
||||
/// Add a new subscription
|
||||
pub fn insert_subscription(&mut self, subscription: String) {
|
||||
if self.subscribed.contains(&subscription) {
|
||||
pub async fn insert_subscription(&mut self, subscription: String) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if subscribed.contains(&subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,12 +183,13 @@ impl State {
|
||||
SubscriptionStateChange::Reset => {}
|
||||
}
|
||||
|
||||
self.subscribed.insert(subscription);
|
||||
subscribed.insert(subscription);
|
||||
}
|
||||
|
||||
/// Remove existing subscription
|
||||
pub fn remove_subscription(&mut self, subscription: &str) {
|
||||
if !self.subscribed.contains(&subscription.to_string()) {
|
||||
pub async fn remove_subscription(&mut self, subscription: &str) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if !subscribed.contains(&subscription.to_string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,6 +206,6 @@ impl State {
|
||||
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
|
||||
}
|
||||
|
||||
self.subscribed.remove(subscription);
|
||||
subscribed.remove(subscription);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use revolt_presence::clear_region;
|
||||
extern crate log;
|
||||
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
|
||||
mod database;
|
||||
mod websocket;
|
||||
@@ -14,7 +15,7 @@ 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.
|
||||
@@ -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:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+504
-230
@@ -1,252 +1,526 @@
|
||||
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_presence::{create_session, delete_session};
|
||||
use revolt_quark::{
|
||||
events::{
|
||||
client::EventV1,
|
||||
server::ClientMessage,
|
||||
state::{State, SubscriptionStateChange},
|
||||
},
|
||||
models::{user::UserHint, User},
|
||||
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,
|
||||
)
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (mut write, mut read) = ws.split();
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
while let Ok(Some(message)) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
write
|
||||
.send(config.encode(&create_error!(InvalidSession)))
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
|
||||
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
write.send(config.encode(&err)).await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user, session_id);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
if 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, config.get_ready_payload_fields())
|
||||
.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()
|
||||
);
|
||||
{
|
||||
Ok(ready_payload) => ready_payload,
|
||||
Err(err) => {
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (write, mut read) = ws.split();
|
||||
let write = Mutex::new(write);
|
||||
if let Err(err) = write.send(config.encode(&ready_payload)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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) = 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 = 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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.7.16"
|
||||
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"
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost/"
|
||||
@@ -0,0 +1,186 @@
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://database"
|
||||
# Redis connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
redis = "redis://redis/"
|
||||
|
||||
[hosts]
|
||||
# Web locations of various services
|
||||
# Defaults assume all services are reverse-proxied
|
||||
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
|
||||
#
|
||||
# Remember to change these to https/wss where appropriate in production!
|
||||
app = "http://local.revolt.chat"
|
||||
api = "http://local.revolt.chat/api"
|
||||
events = "ws://local.revolt.chat/ws"
|
||||
autumn = "http://local.revolt.chat/autumn"
|
||||
january = "http://local.revolt.chat/january"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
[api]
|
||||
|
||||
[api.registration]
|
||||
# Whether an invite should be required for registration
|
||||
# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
|
||||
invite_only = false
|
||||
|
||||
[api.smtp]
|
||||
# Email server configuration for verification
|
||||
# Defaults to no email verification (host field is empty)
|
||||
host = ""
|
||||
username = ""
|
||||
password = ""
|
||||
from_address = "noreply@example.com"
|
||||
reply_to = "noreply@example.com"
|
||||
port = 587
|
||||
use_tls = true
|
||||
|
||||
[api.vapid]
|
||||
# Generate your own keys:
|
||||
# 1. Run `openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem`
|
||||
# 2. Find `private_key` using `base64 vapid_private.pem`
|
||||
# 3. Find `public_key` using `openssl ec -in vapid_private.pem -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n'`
|
||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
||||
|
||||
[api.fcm]
|
||||
# Google Firebase Cloud Messaging Service Account Key
|
||||
# Obtained from the cloud messaging console
|
||||
key_type = ""
|
||||
project_id = ""
|
||||
private_key_id = ""
|
||||
private_key = ""
|
||||
client_email = ""
|
||||
client_id = ""
|
||||
auth_uri = ""
|
||||
token_uri = ""
|
||||
auth_provider_x509_cert_url = ""
|
||||
client_x509_cert_url = ""
|
||||
|
||||
[api.apn]
|
||||
# Apple Push Notifications keys for sending notifications
|
||||
sandbox = false
|
||||
pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
[api.security]
|
||||
# Authifier Shield API key
|
||||
authifier_shield_key = ""
|
||||
# Legacy voice server management token
|
||||
voso_legacy_token = ""
|
||||
# Whether services are behind the Cloudflare network
|
||||
trust_cloudflare = false
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
hcaptcha_key = ""
|
||||
hcaptcha_sitekey = ""
|
||||
|
||||
[api.workers]
|
||||
# Maximum concurrent connections (to proxy server)
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[files]
|
||||
# Encryption key for stored files
|
||||
# Generate your own key using `openssl rand -base64 32`
|
||||
encryption_key = "qcuMA+ssxhMyKaNAKBGFfryfFtUH8NDlamQyDwGW6fU="
|
||||
|
||||
[files.limit]
|
||||
# Minimum image resolution
|
||||
min_resolution = [1, 1]
|
||||
# Maximum MP of images
|
||||
max_mega_pixels = 40
|
||||
# Maximum pixel side of an image
|
||||
max_pixel_side = 10_000
|
||||
|
||||
[files.preview]
|
||||
# Maximum image resolution
|
||||
attachments = [1280, 1280]
|
||||
avatars = [128, 128]
|
||||
backgrounds = [1280, 720]
|
||||
icons = [128, 128]
|
||||
banners = [480, 480]
|
||||
emojis = [128, 128]
|
||||
|
||||
[files.s3]
|
||||
# Configuration for S3
|
||||
# Defaults included for MinIO + self-hosted setup
|
||||
#
|
||||
# Backblaze B2:
|
||||
# - endpoint is listed on the "Buckets" page
|
||||
# - region is `eu-central-003` string from endpoint URL
|
||||
# - access_key_id is keyID generated on the "Application Keys" page
|
||||
# - secret_access_key is token generated on the "Application Keys" page
|
||||
# - default_bucket matches the name of the bucket you've created
|
||||
|
||||
# S3 protocol endpoint
|
||||
endpoint = ""
|
||||
# S3 region name
|
||||
region = ""
|
||||
# S3 protocol key ID
|
||||
access_key_id = ""
|
||||
# S3 protocol access key
|
||||
secret_access_key = ""
|
||||
# Bucket to upload to by default
|
||||
default_bucket = "revolt-uploads"
|
||||
|
||||
[features]
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.global]
|
||||
group_size = 100
|
||||
message_embeds = 5
|
||||
message_replies = 5
|
||||
message_reactions = 20
|
||||
server_emoji = 100
|
||||
server_roles = 200
|
||||
server_channels = 200
|
||||
|
||||
# How many days since creation a user is considered new
|
||||
new_user_days = 3
|
||||
|
||||
# Maximum permissible body size in bytes for uploads
|
||||
# (should be greater than any one file upload limit)
|
||||
body_limit_size = 20_000_000
|
||||
|
||||
[features.limits.new_user]
|
||||
outgoing_friend_requests = 5
|
||||
|
||||
bots = 2
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20_000_000
|
||||
avatar_size = 4_000_000
|
||||
background_size = 6_000_000
|
||||
icon_size = 2_500_000
|
||||
banner_size = 6_000_000
|
||||
emoji_size = 500_000
|
||||
|
||||
[features.limits.default]
|
||||
outgoing_friend_requests = 10
|
||||
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20_000_000
|
||||
avatar_size = 4_000_000
|
||||
background_size = 6_000_000
|
||||
icon_size = 2_500_000
|
||||
banner_size = 6_000_000
|
||||
emoji_size = 500_000
|
||||
|
||||
[sentry]
|
||||
# Configuration for Sentry error reporting
|
||||
api = ""
|
||||
events = ""
|
||||
@@ -0,0 +1,280 @@
|
||||
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 key_type: String,
|
||||
pub project_id: String,
|
||||
pub private_key_id: String,
|
||||
pub private_key: String,
|
||||
pub client_email: String,
|
||||
pub client_id: String,
|
||||
pub auth_uri: String,
|
||||
pub token_uri: String,
|
||||
pub auth_provider_x509_cert_url: String,
|
||||
pub client_x509_cert_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiApn {
|
||||
pub sandbox: bool,
|
||||
pub pkcs8: String,
|
||||
pub key_id: String,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurityCaptcha {
|
||||
pub hcaptcha_key: String,
|
||||
pub hcaptcha_sitekey: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurity {
|
||||
pub authifier_shield_key: String,
|
||||
pub voso_legacy_token: String,
|
||||
pub captcha: ApiSecurityCaptcha,
|
||||
pub trust_cloudflare: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiWorkers {
|
||||
pub max_concurrent_connections: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Api {
|
||||
pub registration: ApiRegistration,
|
||||
pub smtp: ApiSmtp,
|
||||
pub vapid: ApiVapid,
|
||||
pub fcm: ApiFcm,
|
||||
pub apn: ApiApn,
|
||||
pub security: ApiSecurity,
|
||||
pub workers: ApiWorkers,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct 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,
|
||||
|
||||
pub body_limit_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimits {
|
||||
pub outgoing_friend_requests: usize,
|
||||
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
|
||||
pub 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.6.6"
|
||||
version = "0.7.16"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -13,30 +13,42 @@ description = "Revolt Backend: Database Implementation"
|
||||
mongodb = ["dep:mongodb", "bson"]
|
||||
|
||||
# ... Other
|
||||
tasks = ["isahc", "linkify", "url-escape"]
|
||||
async-std-runtime = ["async-std"]
|
||||
rocket-impl = ["rocket", "schemars"]
|
||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||
|
||||
# Default Features
|
||||
default = ["mongodb", "async-std-runtime"]
|
||||
default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.6.6", path = "../result" }
|
||||
revolt-models = { version = "0.6.6", path = "../models" }
|
||||
revolt-presence = { version = "0.6.6", path = "../presence" }
|
||||
revolt-permissions = { version = "0.6.6", path = "../permissions", features = [
|
||||
revolt-config = { version = "0.7.16", path = "../config" }
|
||||
revolt-result = { version = "0.7.16", path = "../result" }
|
||||
revolt-models = { version = "0.7.16", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.16", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.16", 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"
|
||||
@@ -57,6 +69,7 @@ regex = "1"
|
||||
|
||||
# Async Language Features
|
||||
futures = "0.3.19"
|
||||
async-lock = "2.8.0"
|
||||
async-trait = "0.1.51"
|
||||
async-recursion = "1.0.4"
|
||||
|
||||
@@ -68,6 +81,13 @@ schemars = { version = "0.8.8", optional = true }
|
||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
|
||||
"json",
|
||||
], optional = true }
|
||||
revolt_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
||||
|
||||
# Notifications
|
||||
fcm_v1 = "0.3.0"
|
||||
web-push = "0.10.0"
|
||||
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0" }
|
||||
authifier = { version = "1.0.8" }
|
||||
|
||||
@@ -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__"]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,9 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
|
||||
pub use self::mongodb::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
@@ -31,25 +34,44 @@ 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 let Ok(uri) = std::env::var("MONGODB") {
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri,
|
||||
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;
|
||||
.await?
|
||||
} else {
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
DatabaseInfo::Test(database_name) => {
|
||||
if let Ok(uri) = std::env::var("MONGODB") {
|
||||
return DatabaseInfo::MongoDb { uri, database_name }.connect().await;
|
||||
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.connect().await?
|
||||
}
|
||||
DatabaseInfo::Reference => Database::Reference(Default::default()),
|
||||
DatabaseInfo::MongoDb { uri, database_name } => {
|
||||
|
||||
@@ -238,6 +238,6 @@ pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_j
|
||||
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)| (prefix.to_owned() + &k, v))
|
||||
.map(|(k, v)| (format!("{}{}", prefix.to_owned(), k), v))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
|
||||
MemberCompositeKey, Server, ServerBan, User, UserSettings, Webhook,
|
||||
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
@@ -17,14 +18,15 @@ database_derived!(
|
||||
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 files: Arc<Mutex<HashMap<String, File>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub messages: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,9 +2,7 @@ use authifier::AuthifierEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer, FieldsWebhook,
|
||||
MemberCompositeKey, PartialChannel, PartialMember, PartialRole, PartialServer, PartialWebhook,
|
||||
Server, UserSettings, Webhook,
|
||||
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, Webhook
|
||||
};
|
||||
use revolt_result::Error;
|
||||
|
||||
@@ -38,6 +36,19 @@ pub enum ErrorEvent {
|
||||
APIError(Error),
|
||||
}
|
||||
|
||||
/// Fields provided in Ready payload
|
||||
#[derive(PartialEq)]
|
||||
pub enum ReadyPayloadFields {
|
||||
Users,
|
||||
Servers,
|
||||
Channels,
|
||||
Members,
|
||||
Emoji,
|
||||
|
||||
UserSettings(Vec<String>),
|
||||
ChannelUnreads,
|
||||
}
|
||||
|
||||
/// Protocol Events
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -47,18 +58,29 @@ pub enum EventV1 {
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
/* /// Basic data to cache
|
||||
/// Logged out
|
||||
Logout,
|
||||
/// Basic data to cache
|
||||
Ready {
|
||||
users: Vec<User>,
|
||||
servers: Vec<Server>,
|
||||
channels: Vec<Channel>,
|
||||
members: Vec<Member>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
users: Option<Vec<User>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
servers: Option<Vec<Server>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channels: Option<Vec<Channel>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
members: Option<Vec<Member>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_settings: Option<UserSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping },
|
||||
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
@@ -67,6 +89,7 @@ pub enum EventV1 {
|
||||
id: String,
|
||||
channel: String,
|
||||
data: PartialMessage,
|
||||
clear: Vec<FieldsMessage>,
|
||||
},
|
||||
|
||||
/// Append information to existing message
|
||||
@@ -103,12 +126,14 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Bulk delete messages
|
||||
BulkMessageDelete { channel: String, ids: Vec<String> },*/
|
||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
||||
|
||||
/// New server
|
||||
ServerCreate {
|
||||
id: String,
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
emojis: Vec<Emoji>,
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
@@ -132,7 +157,11 @@ pub enum EventV1 {
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave { id: String, user: String },
|
||||
ServerMemberLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
reason: RemovalIntention,
|
||||
},
|
||||
|
||||
/// Server role created or updated
|
||||
ServerRoleUpdate {
|
||||
@@ -145,7 +174,7 @@ pub enum EventV1 {
|
||||
/// Server role deleted
|
||||
ServerRoleDelete { id: String, role_id: String },
|
||||
|
||||
/*/// Update existing user
|
||||
/// Update existing user
|
||||
UserUpdate {
|
||||
id: String,
|
||||
data: PartialUser,
|
||||
@@ -154,16 +183,11 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Relationship with another user changed
|
||||
UserRelationship {
|
||||
id: String,
|
||||
user: User,
|
||||
// ! this field can be deprecated
|
||||
status: RelationshipStatus,
|
||||
},*/
|
||||
UserRelationship { id: String, user: User },
|
||||
/// Settings updated remotely
|
||||
UserSettingsUpdate { id: String, update: UserSettings },
|
||||
|
||||
/*/// User has been platform banned or deleted their account
|
||||
/// User has been platform banned or deleted their account
|
||||
///
|
||||
/// Clients should remove the following associated data:
|
||||
/// - Messages
|
||||
@@ -172,15 +196,15 @@ pub enum EventV1 {
|
||||
/// - 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 }, */
|
||||
UserPlatformWipe { user_id: String, flags: i32 },
|
||||
/// New emoji
|
||||
EmojiCreate(Emoji),
|
||||
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
|
||||
/*/// New report
|
||||
ReportCreate(Report), */
|
||||
/// New report
|
||||
ReportCreate(Report),
|
||||
/// New channel
|
||||
ChannelCreate(Channel),
|
||||
|
||||
@@ -247,11 +271,10 @@ impl EventV1 {
|
||||
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
|
||||
// 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().p(member.id.server).await;
|
||||
self.clone().server(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,6 +284,11 @@ impl EventV1 {
|
||||
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 +1,2 @@
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
|
||||
@@ -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<()> },
|
||||
}
|
||||
@@ -43,7 +43,7 @@ macro_rules! auto_derived {
|
||||
|
||||
macro_rules! auto_derived_partial {
|
||||
( $item:item, $name:expr ) => {
|
||||
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[optional_name = $name]
|
||||
#[opt_skip_serializing_none]
|
||||
@@ -81,8 +81,14 @@ pub mod util;
|
||||
pub use models::*;
|
||||
|
||||
pub mod events;
|
||||
pub mod tasks;
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
}
|
||||
|
||||
/// Utility function to check if an option doesnt contain true
|
||||
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||
t != &Some(true)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ mod tests {
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Migrate the existing database
|
||||
db.migrate_database().await.unwrap()
|
||||
db.migrate_database().await.unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,14 @@ pub async fn create_database(db: &MongoDb) {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
"name": "channel_pinned_compound"
|
||||
},
|
||||
]
|
||||
},
|
||||
None,
|
||||
|
||||
@@ -5,8 +5,9 @@ use crate::{
|
||||
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
||||
options::FindOptions,
|
||||
},
|
||||
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
||||
Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
||||
};
|
||||
use bson::oid::ObjectId;
|
||||
use futures::StreamExt;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
|
||||
@@ -19,7 +20,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 26;
|
||||
pub const LATEST_REVISION: i32 = 28;
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -983,6 +984,116 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
||||
if revision <= 27 {
|
||||
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "messages",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
"name": "channel_pinned_compound"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create message index.");
|
||||
}
|
||||
|
||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::Database;
|
||||
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Bot
|
||||
@@ -49,8 +51,74 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
#[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 {
|
||||
@@ -78,6 +146,10 @@ impl Bot {
|
||||
|
||||
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(())
|
||||
}
|
||||
@@ -96,27 +168,24 @@ mod tests {
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let bot_id = "bot";
|
||||
let user_id = "user";
|
||||
let token = "my_token";
|
||||
let owner = User::create(&db, "Owner".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = User {
|
||||
id: bot_id.to_string(),
|
||||
username: "Bot Name".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
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();
|
||||
|
||||
db.insert_user(&user).await.unwrap();
|
||||
|
||||
let bot = Bot {
|
||||
id: bot_id.to_string(),
|
||||
owner: user_id.to_string(),
|
||||
token: token.to_string(),
|
||||
interactions_url: "some url".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_bot(&bot).await.unwrap();
|
||||
assert!(!bot.interactions_url.is_empty());
|
||||
|
||||
let mut updated_bot = bot.clone();
|
||||
updated_bot
|
||||
@@ -131,9 +200,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched_bot1 = db.fetch_bot(bot_id).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(user_id).await.unwrap();
|
||||
let fetched_bots = db.fetch_bots_by_user(&owner.id).await.unwrap();
|
||||
|
||||
assert!(!bot.public);
|
||||
assert!(fetched_bot1.public);
|
||||
@@ -143,12 +212,12 @@ mod tests {
|
||||
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(user_id).await.unwrap());
|
||||
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(user_id).await.unwrap());
|
||||
assert_eq!(db.fetch_user(bot_id).await.unwrap().flags, Some(2))
|
||||
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,15 +1,16 @@
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use crate::Database;
|
||||
use crate::{Channel, Database, User};
|
||||
|
||||
/* static ALPHABET: [char; 54] = [
|
||||
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 {
|
||||
@@ -56,9 +57,13 @@ impl Invite {
|
||||
}
|
||||
|
||||
/// Create a new invite from given information
|
||||
/*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> {
|
||||
pub async fn create_channel_invite(
|
||||
db: &Database,
|
||||
creator: &User,
|
||||
channel: &Channel,
|
||||
) -> Result<Invite> {
|
||||
let code = nanoid::nanoid!(8, &ALPHABET);
|
||||
let invite = match &target {
|
||||
let invite = match &channel {
|
||||
Channel::Group { id, .. } => Ok(Invite::Group {
|
||||
code,
|
||||
creator: creator.id.clone(),
|
||||
@@ -72,12 +77,12 @@ impl Invite {
|
||||
channel: id.clone(),
|
||||
})
|
||||
}
|
||||
_ => Err(Error::InvalidOperation),
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}?;
|
||||
|
||||
db.insert_invite(&invite).await?;
|
||||
Ok(invite)
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Resolve an invite by its ID or by a public server ID
|
||||
pub async fn find(db: &Database, code: &str) -> Result<Invite> {
|
||||
|
||||
@@ -7,13 +7,13 @@ mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelUnreads: Sync + Send {
|
||||
/// Acknowledge a message.
|
||||
/// Acknowledge a message, and returns updated channel unread.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()>;
|
||||
) -> Result<Option<ChannelUnread>>;
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
|
||||
@@ -28,4 +28,7 @@ pub trait AbstractChannelUnreads: Sync + Send {
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use bson::Document;
|
||||
use mongodb::options::FindOneAndUpdateOptions;
|
||||
use mongodb::options::ReturnDocument;
|
||||
use mongodb::options::UpdateOptions;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
@@ -12,31 +14,35 @@ static COL: &str = "channel_unreads";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelUnreads for MongoDb {
|
||||
/// Acknowledge a message.
|
||||
/// Acknowledge a message, and returns updated channel unread.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
) -> Result<Option<ChannelUnread>> {
|
||||
self.col::<ChannelUnread>(COL)
|
||||
.find_one_and_update(
|
||||
doc! {
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": user_id,
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
"mentions": 1_i32
|
||||
"$pull": {
|
||||
"mentions": {
|
||||
"$lt": message_id
|
||||
}
|
||||
},
|
||||
"$set": {
|
||||
"last_id": message_id
|
||||
}
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
FindOneAndUpdateOptions::builder()
|
||||
.upsert(true)
|
||||
.return_document(ReturnDocument::After)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
@@ -116,4 +122,18 @@ impl AbstractChannelUnreads for MongoDb {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.user": user_id,
|
||||
"_id.channel": channel_id
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,34 +13,35 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()> {
|
||||
) -> Result<Option<ChannelUnread>> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
};
|
||||
|
||||
if let Some(mut unread) = unreads.get_mut(&key) {
|
||||
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,
|
||||
id: key.clone(),
|
||||
last_id: Some(message_id.to_string()),
|
||||
mentions: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(unreads.get(&key).cloned())
|
||||
}
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
|
||||
let current_time = Ulid::new().to_string();
|
||||
for channel_id in channel_ids {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
self.acknowledge_message(channel_id, user_id, ¤t_time)
|
||||
.await?;
|
||||
}
|
||||
@@ -86,4 +87,14 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||
let unreads = self.channel_unreads.lock().await;
|
||||
|
||||
Ok(unreads.get(&ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string()
|
||||
}).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,20 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
#[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<()> {
|
||||
@@ -120,7 +134,7 @@ mod tests {
|
||||
id: webhook_id.to_string(),
|
||||
name: "Webhook Name".to_string(),
|
||||
channel_id: channel_id.to_string(),
|
||||
avatar: Some(Default::default()),
|
||||
avatar: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
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, Database, File, IntoDocumentPath};
|
||||
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 {
|
||||
@@ -164,8 +171,9 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Channel {
|
||||
/// Create a channel
|
||||
/* /// Create a channel
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_channel(self).await?;
|
||||
|
||||
@@ -183,46 +191,203 @@ impl Channel {
|
||||
}
|
||||
|
||||
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_id: &str,
|
||||
_by_id: &str,
|
||||
user: &User,
|
||||
by_id: &str,
|
||||
) -> Result<()> {
|
||||
if let Channel::Group { recipients, .. } = self {
|
||||
if recipients.contains(&String::from(user_id)) {
|
||||
if recipients.contains(&String::from(&user.id)) {
|
||||
return Err(create_error!(AlreadyInGroup));
|
||||
}
|
||||
|
||||
recipients.push(String::from(user_id));
|
||||
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?;
|
||||
db.add_user_to_group(id, &user.id).await?;
|
||||
|
||||
EventV1::ChannelGroupJoin {
|
||||
id: id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user_id.to_string())
|
||||
.await;
|
||||
|
||||
/* TODO: SystemMessage::UserAdded {
|
||||
id: user.to_string(),
|
||||
by: by.to_string(),
|
||||
SystemMessage::UserAdded {
|
||||
id: user.id.to_string(),
|
||||
by: by_id.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.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();
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user.id.to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -251,14 +416,14 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to this channel's id
|
||||
pub fn id(&self) -> String {
|
||||
/// Clone this channel's id
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Channel::DirectMessage { id, .. }
|
||||
| Channel::Group { id, .. }
|
||||
| Channel::SavedMessages { id, .. }
|
||||
| Channel::TextChannel { id, .. }
|
||||
| Channel::VoiceChannel { id, .. } => id.clone(),
|
||||
| Channel::VoiceChannel { id, .. } => id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,23 +629,46 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_id: &str,
|
||||
_by_id: Option<&str>,
|
||||
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) {
|
||||
if &user.id == owner {
|
||||
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
|
||||
db.update_channel(
|
||||
id,
|
||||
&PartialChannel {
|
||||
@@ -491,42 +679,63 @@ impl Channel {
|
||||
)
|
||||
.await?;
|
||||
|
||||
/* TODO: SystemMessage::ChannelOwnershipChanged {
|
||||
SystemMessage::ChannelOwnershipChanged {
|
||||
from: owner.to_string(),
|
||||
to: new_owner.into(),
|
||||
to: new_owner.to_string(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.send(
|
||||
db,
|
||||
MessageAuthor::System {
|
||||
username: name,
|
||||
avatar: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.ok(); */
|
||||
.ok();
|
||||
} else {
|
||||
db.delete_channel(self).await?;
|
||||
return Ok(());
|
||||
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(),
|
||||
user: user.id.to_string(),
|
||||
}
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
if !silent {
|
||||
/* TODO: if let Some(_by) = by_id {
|
||||
if let Some(by) = by_id {
|
||||
SystemMessage::UserRemove {
|
||||
id: user_id.to_string(),
|
||||
id: user.id.to_string(),
|
||||
by: by.to_string(),
|
||||
}
|
||||
} else {
|
||||
SystemMessage::UserLeft {
|
||||
id: user_id.to_string(),
|
||||
id: user.id.to_string(),
|
||||
}
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create(db, self, None)
|
||||
.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();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -540,6 +749,9 @@ impl 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
|
||||
}
|
||||
}
|
||||
@@ -553,3 +765,62 @@ impl IntoDocumentPath for FieldsChannel {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::AbstractChannels;
|
||||
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
||||
use bson::Document;
|
||||
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
||||
use bson::{Bson, Document};
|
||||
use futures::StreamExt;
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
@@ -188,6 +188,125 @@ impl AbstractChannels for MongoDb {
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
|
||||
let id = channel.id().to_string();
|
||||
let server_id = match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Delete invites and unreads.
|
||||
self.delete_associated_channel_objects(Bson::String(id.to_string()))
|
||||
.await?;
|
||||
|
||||
// Delete messages.
|
||||
self.delete_bulk_messages(doc! {
|
||||
"channel": &id
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Remove from server object.
|
||||
if let Some(server) = server_id {
|
||||
let server = self.fetch_server(server).await?;
|
||||
let mut update = doc! {
|
||||
"$pull": {
|
||||
"channels": &id
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(sys) = &server.system_messages {
|
||||
let mut unset = doc! {};
|
||||
|
||||
if let Some(cid) = &sys.user_joined {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_joined", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_left {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_left", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_kicked {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_kicked", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_banned {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_banned", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if !unset.is_empty() {
|
||||
update.insert("$unset", unset);
|
||||
}
|
||||
}
|
||||
|
||||
self.col::<Document>("servers")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": server.id
|
||||
},
|
||||
update,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", "servers"))?;
|
||||
}
|
||||
|
||||
// Delete associated attachments
|
||||
self.delete_many_attachments(doc! {
|
||||
"object_id": &id
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Delete the channel itself
|
||||
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
|
||||
// Delete all invites to these channels.
|
||||
self.col::<Document>("channel_invites")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
|
||||
|
||||
// Delete unread message objects on channels.
|
||||
self.col::<Document>("channel_unreads")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
|
||||
.map(|_| ())?;
|
||||
|
||||
// update many attachments with parent id
|
||||
|
||||
// Delete all webhooks on this channel.
|
||||
self.col::<Document>("webhooks")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "webhooks"))
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ 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()) {
|
||||
if let Entry::Vacant(entry) = channels.entry(channel.id().to_string()) {
|
||||
entry.insert(channel.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -148,7 +148,7 @@ impl AbstractChannels for ReferenceDb {
|
||||
// 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() {
|
||||
if channels.remove(channel.id()).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
|
||||
@@ -24,13 +24,12 @@ impl AbstractEmojis for MongoDb {
|
||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"parent.id": parent_id
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch emoji by their parent ids
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::MongoDb;
|
||||
|
||||
use super::AbstractAttachments;
|
||||
|
||||
static COL: &str = "bots";
|
||||
static COL: &str = "attachments";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAttachments for MongoDb {
|
||||
@@ -115,3 +115,21 @@ impl AbstractAttachments for MongoDb {
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,15 +22,25 @@ impl AbstractAttachments for ReferenceDb {
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
id: &str,
|
||||
_tag: &str,
|
||||
_parent_type: &str,
|
||||
_parent_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) {
|
||||
// TODO: check tag
|
||||
// TODO: set parent ID
|
||||
Ok(file.clone())
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_models::v0::{Embed, MessageSort, MessageWebhook};
|
||||
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::File;
|
||||
use crate::{
|
||||
events::client::EventV1,
|
||||
tasks::{self, ack::AckEvent},
|
||||
util::idempotency::IdempotencyKey,
|
||||
Channel, Database, Emoji, File, User,
|
||||
};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Message
|
||||
@@ -50,6 +65,13 @@ auto_derived_partial!(
|
||||
/// Name and / or avatar overrides for this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub masquerade: Option<Masquerade>,
|
||||
/// Whether or not the message in pinned
|
||||
#[serde(skip_serializing_if = "crate::if_option_false")]
|
||||
pub pinned: Option<bool>,
|
||||
|
||||
/// Bitfield of message flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
},
|
||||
"PartialMessage"
|
||||
);
|
||||
@@ -80,6 +102,10 @@ auto_derived!(
|
||||
ChannelIconChanged { by: String },
|
||||
#[serde(rename = "channel_ownership_changed")]
|
||||
ChannelOwnershipChanged { from: String, to: String },
|
||||
#[serde(rename = "message_pinned")]
|
||||
MessagePinned { id: String, by: String },
|
||||
#[serde(rename = "message_unpinned")]
|
||||
MessageUnpinned { id: String, by: String },
|
||||
}
|
||||
|
||||
/// Name and / or avatar override information
|
||||
@@ -141,6 +167,7 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// Message Filter
|
||||
#[derive(Default)]
|
||||
pub struct MessageFilter {
|
||||
/// Parent channel ID
|
||||
pub channel: Option<String>,
|
||||
@@ -148,6 +175,8 @@ auto_derived!(
|
||||
pub author: Option<String>,
|
||||
/// Search query
|
||||
pub query: Option<String>,
|
||||
/// Search for pinned
|
||||
pub pinned: Option<bool>,
|
||||
}
|
||||
|
||||
/// Message Query
|
||||
@@ -163,34 +192,689 @@ auto_derived!(
|
||||
#[serde(flatten)]
|
||||
pub time_period: MessageTimePeriod,
|
||||
}
|
||||
|
||||
/// Optional fields on message
|
||||
pub enum FieldsMessage {
|
||||
Pinned,
|
||||
}
|
||||
);
|
||||
|
||||
#[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,
|
||||
pinned: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Message {}
|
||||
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().to_string(),
|
||||
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.clone(),
|
||||
member.clone(),
|
||||
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(user, member),
|
||||
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,
|
||||
remove: Vec<FieldsMessage>,
|
||||
) -> Result<()> {
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
db.update_message(&self.id, &partial, remove.clone())
|
||||
.await?;
|
||||
|
||||
EventV1::MessageUpdate {
|
||||
id: self.id.clone(),
|
||||
channel: self.channel.clone(),
|
||||
data: partial.into(),
|
||||
clear: remove.into_iter().map(|field| field.into()).collect(),
|
||||
}
|
||||
.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 { .. } => {}
|
||||
v0::SystemMessage::MessagePinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
v0::SystemMessage::MessageUnpinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsMessage) {
|
||||
match field {
|
||||
FieldsMessage::Pinned => self.pinned = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: &mut PermissionCalculator<'_>,
|
||||
) -> Result<()> {
|
||||
if let Some(reactions) = &self.reactions {
|
||||
permissions.throw_permission(db, Permission::React).await?;
|
||||
pub async fn validate(&self, db: &Database, permissions: &PermissionValue) -> Result<()> {
|
||||
let config = config().await;
|
||||
|
||||
if reactions.len() > 20 {
|
||||
return Err(Error::InvalidOperation);
|
||||
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(Error::InvalidOperation);
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Check if we can use a given emoji to react
|
||||
pub fn can_use(&self, emoji: &str) -> bool {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||
|
||||
// mod mongodb;
|
||||
// mod reference;
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractMessages: Sync + Send {
|
||||
@@ -16,8 +16,11 @@ pub trait AbstractMessages: Sync + Send {
|
||||
/// 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<()>;
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()>;
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
|
||||
@@ -35,5 +38,5 @@ pub trait AbstractMessages: Sync + Send {
|
||||
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<()>;
|
||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,64 +1,250 @@
|
||||
use bson::Document;
|
||||
use bson::{to_bson, Document};
|
||||
use futures::try_join;
|
||||
use mongodb::options::FindOptions;
|
||||
use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::MongoDb;
|
||||
use crate::{
|
||||
AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath, Message, MessageQuery,
|
||||
MessageTimePeriod, MongoDb, PartialMessage,
|
||||
};
|
||||
|
||||
use super::AbstractEmojis;
|
||||
use super::AbstractMessages;
|
||||
|
||||
static COL: &str = "emojis";
|
||||
static COL: &str = "messages";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractEmojis for MongoDb {
|
||||
/// Insert emoji into database.
|
||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
query!(self, insert_one, COL, &emoji).map(|_| ())
|
||||
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 an emoji by its id
|
||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
||||
/// 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 emoji by their parent id
|
||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"parent.id": parent_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! {};
|
||||
|
||||
/// Fetch emoji by their parent ids
|
||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"parent.id": {
|
||||
"$in": parent_ids
|
||||
// 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
|
||||
};
|
||||
|
||||
if let Some(pinned) = query.filter.pinned {
|
||||
filter.insert("pinned", pinned);
|
||||
};
|
||||
|
||||
// 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))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
/// 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,
|
||||
remove: Vec<FieldsMessage>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
message,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
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": &emoji.id
|
||||
"_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! {
|
||||
"$set": {
|
||||
"parent": {
|
||||
"type": "Detached"
|
||||
}
|
||||
"$addToSet": {
|
||||
format!("reactions.{emoji}"): user
|
||||
}
|
||||
},
|
||||
None,
|
||||
@@ -67,4 +253,126 @@ impl AbstractEmojis for MongoDb {
|
||||
.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 IntoDocumentPath for FieldsMessage {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsMessage::Pinned => "pinned",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,289 @@
|
||||
use futures::future::try_join_all;
|
||||
use indexmap::IndexSet;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::EmojiParent;
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
||||
|
||||
use super::AbstractEmojis;
|
||||
use super::AbstractMessages;
|
||||
|
||||
#[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"))
|
||||
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 {
|
||||
emojis.insert(emoji.id.to_string(), emoji.clone());
|
||||
messages.insert(message.id.to_string(), message.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch an emoji by its id
|
||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
||||
let emojis = self.emojis.lock().await;
|
||||
emojis
|
||||
/// 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 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
|
||||
/// 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(|emoji| match &emoji.parent {
|
||||
EmojiParent::Server { id } => id == parent_id,
|
||||
_ => false,
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pinned) = query.filter.pinned {
|
||||
if message.pinned.unwrap_or_default() == pinned {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
.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 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())
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// 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;
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message_data) = messages.get_mut(id) {
|
||||
message_data.apply_options(message.to_owned());
|
||||
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
message_data.remove_field(&field);
|
||||
}
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ mod emojis;
|
||||
mod files;
|
||||
mod messages;
|
||||
mod ratelimit_events;
|
||||
mod safety_reports;
|
||||
mod safety_snapshots;
|
||||
mod server_bans;
|
||||
mod server_members;
|
||||
mod servers;
|
||||
@@ -24,6 +26,8 @@ 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::*;
|
||||
@@ -43,7 +47,10 @@ pub trait AbstractDatabase:
|
||||
+ 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
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
use std::fmt;
|
||||
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
auto_derived!(
|
||||
/// Ratelimit Event
|
||||
pub struct RatelimitEvent {
|
||||
@@ -23,3 +28,20 @@ impl fmt::Display for RatelimitEventType {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +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<()> {
|
||||
// TODO: implement
|
||||
unimplemented!()
|
||||
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,
|
||||
target_id: &str,
|
||||
event_type: RatelimitEventType,
|
||||
period: Duration,
|
||||
count: usize,
|
||||
) -> Result<bool> {
|
||||
// TODO: implement
|
||||
unimplemented!()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Report;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractReport: Sync + Send {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()>;
|
||||
}
|
||||
@@ -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(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::Report;
|
||||
|
||||
use super::AbstractReport;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractReport for ReferenceDb {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()> {
|
||||
let mut reports = self.safety_reports.lock().await;
|
||||
if reports.contains_key(&report.id) {
|
||||
Err(create_database_error!("insert", "report"))
|
||||
} else {
|
||||
reports.insert(report.id.to_string(), report.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
+43
-10
@@ -1,13 +1,44 @@
|
||||
use crate::{
|
||||
models::{
|
||||
message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod},
|
||||
snapshot::SnapshotContent,
|
||||
Message, Server, User,
|
||||
},
|
||||
Database, Result,
|
||||
};
|
||||
use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, Server, User};
|
||||
|
||||
auto_derived!(
|
||||
/// Snapshot of some content
|
||||
pub struct Snapshot {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Report parent Id
|
||||
pub report_id: String,
|
||||
/// Snapshot of content
|
||||
pub content: SnapshotContent,
|
||||
}
|
||||
|
||||
/// Enum to map into different models
|
||||
/// that can be saved in a snapshot
|
||||
#[serde(tag = "_type")]
|
||||
pub enum SnapshotContent {
|
||||
Message {
|
||||
/// Context before the message
|
||||
#[serde(rename = "_prior_context", default)]
|
||||
prior_context: Vec<Message>,
|
||||
|
||||
/// Context after the message
|
||||
#[serde(rename = "_leading_context", default)]
|
||||
leading_context: Vec<Message>,
|
||||
|
||||
/// Message
|
||||
#[serde(flatten)]
|
||||
message: Message,
|
||||
},
|
||||
Server(Server),
|
||||
User(User),
|
||||
}
|
||||
);
|
||||
|
||||
impl SnapshotContent {
|
||||
/// Generate snapshot from a given message
|
||||
pub async fn generate_from_message(
|
||||
db: &Database,
|
||||
message: Message,
|
||||
@@ -54,13 +85,14 @@ impl SnapshotContent {
|
||||
Ok((
|
||||
SnapshotContent::Message {
|
||||
message,
|
||||
prior_context,
|
||||
leading_context,
|
||||
prior_context: prior_context.into_iter().map(Into::into).collect(),
|
||||
leading_context: leading_context.into_iter().map(Into::into).collect(),
|
||||
},
|
||||
files,
|
||||
))
|
||||
}
|
||||
|
||||
/// Generate snapshot from a given server
|
||||
pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec<String>)> {
|
||||
// Collect server's icon and banner
|
||||
let files = [&server.icon, &server.banner]
|
||||
@@ -71,6 +103,7 @@ impl SnapshotContent {
|
||||
Ok((SnapshotContent::Server(server), files))
|
||||
}
|
||||
|
||||
/// Generate snapshot from a given user
|
||||
pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> {
|
||||
// Collect user's avatar and profile background
|
||||
let files = [
|
||||
+6
-5
@@ -1,11 +1,12 @@
|
||||
use crate::models::Snapshot;
|
||||
use crate::Result;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Snapshot;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractSnapshot: Sync + Send {
|
||||
/// Insert a new snapshot into the database
|
||||
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>;
|
||||
|
||||
/// Fetch a snapshots by a report's id
|
||||
async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>>;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::Snapshot;
|
||||
|
||||
use super::AbstractSnapshot;
|
||||
|
||||
static COL: &str = "safety_snapshots";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractSnapshot for MongoDb {
|
||||
/// Insert a new snapshot into the database
|
||||
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
|
||||
query!(self, insert_one, COL, &snapshot).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::Snapshot;
|
||||
|
||||
use super::AbstractSnapshot;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractSnapshot for ReferenceDb {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
|
||||
let mut snapshots = self.safety_snapshots.lock().await;
|
||||
if snapshots.contains_key(&snapshot.id) {
|
||||
Err(create_database_error!("insert", "snapshot"))
|
||||
} else {
|
||||
snapshots.insert(snapshot.id.to_string(), snapshot.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::MemberCompositeKey;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Database, MemberCompositeKey, Server};
|
||||
|
||||
auto_derived!(
|
||||
/// Server Ban
|
||||
@@ -10,3 +12,24 @@ auto_derived!(
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
impl ServerBan {
|
||||
/// Create ban
|
||||
pub async fn create(
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
user_id: &str,
|
||||
reason: Option<String>,
|
||||
) -> Result<ServerBan> {
|
||||
let ban = ServerBan {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
},
|
||||
reason,
|
||||
};
|
||||
|
||||
db.insert_ban(&ban).await?;
|
||||
Ok(ban)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use crate::{Database, File, Server};
|
||||
use crate::{
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
|
||||
Server, SystemMessage, User,
|
||||
};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server Member
|
||||
@@ -11,8 +15,7 @@ auto_derived_partial!(
|
||||
pub id: MemberCompositeKey,
|
||||
|
||||
/// Time at which this user joined the server
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub joined_at: Option<Timestamp>,
|
||||
pub joined_at: Timestamp,
|
||||
|
||||
/// Member's nickname
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -57,7 +60,104 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
impl Default for Member {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Default::default(),
|
||||
joined_at: Timestamp::now_utc(),
|
||||
nickname: None,
|
||||
avatar: None,
|
||||
roles: vec![],
|
||||
timeout: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Member {
|
||||
/// Create a new member in a server
|
||||
pub async fn create(
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
user: &User,
|
||||
channels: Option<Vec<Channel>>,
|
||||
) -> Result<(Member, Vec<Channel>)> {
|
||||
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
|
||||
return Err(create_error!(Banned));
|
||||
}
|
||||
|
||||
if db.fetch_member(&server.id, &user.id).await.is_ok() {
|
||||
return Err(create_error!(AlreadyInServer));
|
||||
}
|
||||
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_member(&member).await?;
|
||||
|
||||
let should_fetch = channels.is_none();
|
||||
let mut channels = channels.unwrap_or_default();
|
||||
|
||||
if should_fetch {
|
||||
let query = DatabasePermissionQuery::new(db, user).server(server);
|
||||
let existing_channels = db.fetch_channels(&server.channels).await?;
|
||||
|
||||
for channel in existing_channels {
|
||||
let mut channel_query = query.clone().channel(&channel);
|
||||
|
||||
if calculate_channel_permissions(&mut channel_query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::ViewChannel)
|
||||
{
|
||||
channels.push(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
|
||||
|
||||
EventV1::ServerMemberJoin {
|
||||
id: server.id.clone(),
|
||||
user: user.id.clone(),
|
||||
}
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
|
||||
EventV1::ServerCreate {
|
||||
id: server.id.clone(),
|
||||
server: server.clone().into(),
|
||||
channels: channels
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|channel| channel.into())
|
||||
.collect(),
|
||||
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
|
||||
}
|
||||
.private(user.id.clone())
|
||||
.await;
|
||||
|
||||
if let Some(id) = server
|
||||
.system_messages
|
||||
.as_ref()
|
||||
.and_then(|x| x.user_joined.as_ref())
|
||||
{
|
||||
SystemMessage::UserJoined {
|
||||
id: user.id.clone(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send_without_notifications(db, None, None, false, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok((member, channels))
|
||||
}
|
||||
|
||||
/// Update member data
|
||||
pub async fn update<'a>(
|
||||
&mut self,
|
||||
@@ -73,13 +173,13 @@ impl Member {
|
||||
|
||||
db.update_member(&self.id, &partial, remove.clone()).await?;
|
||||
|
||||
/* // TODO: EventV1::ServerMemberUpdate {
|
||||
id: self.id.clone(),
|
||||
data: partial,
|
||||
clear: remove,
|
||||
EventV1::ServerMemberUpdate {
|
||||
id: self.id.clone().into(),
|
||||
data: partial.into(),
|
||||
clear: remove.into_iter().map(|field| field.into()).collect(),
|
||||
}
|
||||
.p(self.id.server.clone())
|
||||
.await; */
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -115,4 +215,48 @@ impl Member {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove member from server
|
||||
pub async fn remove(
|
||||
self,
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
intention: RemovalIntention,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
db.delete_member(&self.id).await?;
|
||||
|
||||
EventV1::ServerMemberLeave {
|
||||
id: self.id.server.to_string(),
|
||||
user: self.id.user.to_string(),
|
||||
reason: intention.clone().into(),
|
||||
}
|
||||
.p(self.id.server.to_string())
|
||||
.await;
|
||||
|
||||
if !silent {
|
||||
if let Some(id) = server
|
||||
.system_messages
|
||||
.as_ref()
|
||||
.and_then(|x| match intention {
|
||||
RemovalIntention::Leave => x.user_left.as_ref(),
|
||||
RemovalIntention::Kick => x.user_kicked.as_ref(),
|
||||
RemovalIntention::Ban => x.user_banned.as_ref(),
|
||||
})
|
||||
{
|
||||
match intention {
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user },
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
// TODO: support notifications here in the future?
|
||||
.send_without_notifications(db, None, None, false, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_models::v0::{self, DataCreateServerChannel};
|
||||
use revolt_permissions::{OverrideField, DEFAULT_PERMISSION_SERVER};
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{events::client::EventV1, Database, File};
|
||||
use crate::{events::client::EventV1, Channel, Database, File, User};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server
|
||||
@@ -22,7 +23,7 @@ auto_derived_partial!(
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Channels within this server
|
||||
// ! FIXME: this may be redundant
|
||||
// TODO: investigate if this is redundant and can be removed
|
||||
pub channels: Vec<String>,
|
||||
/// Categories for this server
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -128,10 +129,55 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Server {
|
||||
/// Create a server
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_server(self).await
|
||||
pub async fn create(
|
||||
db: &Database,
|
||||
data: v0::DataCreateServer,
|
||||
owner: &User,
|
||||
create_default_channels: bool,
|
||||
) -> Result<(Server, Vec<Channel>)> {
|
||||
let mut server = Server {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
owner: owner.id.to_string(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
channels: vec![],
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
default_permissions: *DEFAULT_PERMISSION_SERVER as i64,
|
||||
|
||||
analytics: false,
|
||||
banner: None,
|
||||
categories: None,
|
||||
discoverable: false,
|
||||
flags: None,
|
||||
icon: None,
|
||||
roles: HashMap::new(),
|
||||
system_messages: None,
|
||||
};
|
||||
|
||||
let channels: Vec<Channel> = if create_default_channels {
|
||||
vec![
|
||||
Channel::create_server_channel(
|
||||
db,
|
||||
&mut server,
|
||||
DataCreateServerChannel {
|
||||
channel_type: v0::LegacyServerChannelType::Text,
|
||||
name: "General".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await?,
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
server.channels = channels.iter().map(|c| c.id().to_string()).collect();
|
||||
db.insert_server(&server).await?;
|
||||
Ok((server, channels))
|
||||
}
|
||||
|
||||
/// Update server data
|
||||
@@ -207,144 +253,6 @@ impl Server {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/* /// Create a new member in a server
|
||||
pub async fn create_member(
|
||||
&self,
|
||||
db: &Database,
|
||||
user: User,
|
||||
channels: Option<Vec<Channel>>,
|
||||
) -> Result<Vec<Channel>> {
|
||||
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
|
||||
return Err(Error::Banned);
|
||||
}
|
||||
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: self.id.clone(),
|
||||
user: user.id.clone(),
|
||||
},
|
||||
joined_at: Timestamp::now_utc(),
|
||||
nickname: None,
|
||||
avatar: None,
|
||||
roles: vec![],
|
||||
timeout: None,
|
||||
};
|
||||
|
||||
db.insert_member(&member).await?;
|
||||
|
||||
let should_fetch = channels.is_none();
|
||||
let mut channels = channels.unwrap_or_default();
|
||||
|
||||
if should_fetch {
|
||||
let perm = perms(&user).server(self).member(&member);
|
||||
let existing_channels = db.fetch_channels(&self.channels).await?;
|
||||
for channel in existing_channels {
|
||||
if perm
|
||||
.clone()
|
||||
.channel(&channel)
|
||||
.has_permission(db, Permission::ViewChannel)
|
||||
.await?
|
||||
{
|
||||
channels.push(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* // TODO: EventV1::ServerMemberJoin {
|
||||
id: self.id.clone(),
|
||||
user: user.id.clone(),
|
||||
}
|
||||
.p(self.id.clone())
|
||||
.await;
|
||||
|
||||
EventV1::ServerCreate {
|
||||
id: self.id.clone(),
|
||||
server: self.clone(),
|
||||
channels: channels.clone(),
|
||||
}
|
||||
.private(user.id.clone())
|
||||
.await; */
|
||||
|
||||
if let Some(id) = self
|
||||
.system_messages
|
||||
.as_ref()
|
||||
.and_then(|x| x.user_joined.as_ref())
|
||||
{
|
||||
SystemMessage::UserJoined {
|
||||
id: user.id.clone(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create_no_web_push(db, id, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
/// Remove a member from a server
|
||||
pub async fn remove_member(
|
||||
&self,
|
||||
db: &Database,
|
||||
member: Member,
|
||||
intention: RemovalIntention,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
db.delete_member(&member.id).await?;
|
||||
|
||||
/* // TODO: EventV1::ServerMemberLeave {
|
||||
id: self.id.to_string(),
|
||||
user: member.id.user.clone(),
|
||||
}
|
||||
.p(member.id.server)
|
||||
.await; */
|
||||
|
||||
if !silent {
|
||||
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
|
||||
RemovalIntention::Leave => x.user_left.as_ref(),
|
||||
RemovalIntention::Kick => x.user_kicked.as_ref(),
|
||||
RemovalIntention::Ban => x.user_banned.as_ref(),
|
||||
}) {
|
||||
match intention {
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create_no_web_push(db, id, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create ban
|
||||
pub async fn ban_user(
|
||||
self,
|
||||
db: &Database,
|
||||
id: MemberCompositeKey,
|
||||
reason: Option<String>,
|
||||
) -> Result<ServerBan> {
|
||||
let ban = ServerBan { id, reason };
|
||||
db.insert_ban(&ban).await?;
|
||||
Ok(ban)
|
||||
}
|
||||
|
||||
/// Ban a member from a server
|
||||
pub async fn ban_member(
|
||||
self,
|
||||
db: &Database,
|
||||
member: Member,
|
||||
reason: Option<String>,
|
||||
) -> Result<ServerBan> {
|
||||
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
|
||||
.await?;
|
||||
|
||||
self.ban_user(db, member.id, reason).await
|
||||
} */
|
||||
}
|
||||
|
||||
impl Role {
|
||||
@@ -449,3 +357,36 @@ impl SystemMessageChannels {
|
||||
ids
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
|
||||
|
||||
use crate::{fixture, util::permissions::DatabasePermissionQuery};
|
||||
|
||||
#[async_std::test]
|
||||
async fn permissions() {
|
||||
database_test!(|db| async move {
|
||||
fixture!(db, "server_with_roles",
|
||||
owner user 0
|
||||
moderator user 1
|
||||
user user 2
|
||||
server server 4);
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &owner).server(&server);
|
||||
assert!(calculate_server_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::GrantAllSafe));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &moderator).server(&server);
|
||||
assert!(calculate_server_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::BanMembers));
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(&db, &user).server(&server);
|
||||
assert!(!calculate_server_permissions(&mut query)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::BanMembers));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use bson::{to_document, Document};
|
||||
use bson::{to_document, Bson, Document};
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
@@ -67,6 +67,7 @@ impl AbstractServers for MongoDb {
|
||||
|
||||
/// Delete a server by its id
|
||||
async fn delete_server(&self, id: &str) -> Result<()> {
|
||||
self.delete_associated_server_objects(id).await?;
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
|
||||
@@ -181,3 +182,88 @@ impl IntoDocumentPath for FieldsRole {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_associated_server_objects(&self, server_id: &str) -> Result<()> {
|
||||
// Find all channels
|
||||
let channels: Vec<String> = self
|
||||
.col::<Document>("channels")
|
||||
.find(
|
||||
doc! {
|
||||
"server": server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", "channels"))?
|
||||
.filter_map(|s| async {
|
||||
s.map(|d| d.get_str("_id").map(|s| s.to_string()).ok())
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// Check if there are any attachments we need to delete.
|
||||
self.delete_bulk_messages(doc! {
|
||||
"channel": {
|
||||
"$in": &channels
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Delete all emoji.
|
||||
self.col::<Document>("emojis")
|
||||
.update_many(
|
||||
doc! {
|
||||
"parent.id": &server_id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"parent": {
|
||||
"type": "Detached"
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_many", "emojis"))?;
|
||||
|
||||
// Delete all channels.
|
||||
self.col::<Document>("channels")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"server": &server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channels"))?;
|
||||
|
||||
// Delete any associated objects, e.g. unreads and invites.
|
||||
self.delete_associated_channel_objects(Bson::Document(doc! { "$in": &channels }))
|
||||
.await?;
|
||||
|
||||
// Delete members and bans.
|
||||
for with in &["server_members", "server_bans"] {
|
||||
self.col::<Document>(with)
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.server": &server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", with))?;
|
||||
}
|
||||
|
||||
// Update many attachments with parent id.
|
||||
self.delete_many_attachments(doc! {
|
||||
"object_id": &server_id
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::Database;
|
||||
use crate::{events::client::EventV1, Database};
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
@@ -16,12 +16,12 @@ impl UserSettingsImpl for UserSettings {
|
||||
async fn set(self, db: &Database, user: &str) -> Result<()> {
|
||||
db.set_user_settings(user, &self).await?;
|
||||
|
||||
/* // TODO: EventV1::UserSettingsUpdate {
|
||||
EventV1::UserSettingsUpdate {
|
||||
id: user.to_string(),
|
||||
update: self,
|
||||
}
|
||||
.private(user.to_string())
|
||||
.await; */
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2,8 +2,12 @@ mod model;
|
||||
mod ops;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod rocket;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod schema;
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::rocket::*;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::schema::*;
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
use std::collections::HashSet;
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
|
||||
use crate::{Database, File};
|
||||
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_result::{Error, ErrorType, Result};
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0;
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::{create_error, Result};
|
||||
use ulid::Ulid;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// # User
|
||||
@@ -49,6 +54,16 @@ auto_derived_partial!(
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Optional fields on user object
|
||||
pub enum FieldsUser {
|
||||
Avatar,
|
||||
StatusText,
|
||||
StatusPresence,
|
||||
ProfileContent,
|
||||
ProfileBackground,
|
||||
DisplayName,
|
||||
}
|
||||
|
||||
/// User's relationship with another user (or themselves)
|
||||
pub enum RelationshipStatus {
|
||||
None,
|
||||
@@ -82,20 +97,22 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// User's active status
|
||||
#[derive(Default)]
|
||||
pub struct UserStatus {
|
||||
/// Custom status text
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
/// Current presence option
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub presence: Option<Presence>,
|
||||
}
|
||||
|
||||
/// User's profile
|
||||
#[derive(Default)]
|
||||
pub struct UserProfile {
|
||||
/// Text content on user's profile
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
/// Background visible on user's profile
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<File>,
|
||||
@@ -107,27 +124,487 @@ auto_derived!(
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
/// Optional fields on user object
|
||||
pub enum FieldsUser {
|
||||
Avatar,
|
||||
StatusText,
|
||||
StatusPresence,
|
||||
ProfileContent,
|
||||
ProfileBackground,
|
||||
/// Enumeration providing a hint to the type of user we are handling
|
||||
pub enum UserHint {
|
||||
/// Could be either a user or a bot
|
||||
Any,
|
||||
/// Only match bots
|
||||
Bot,
|
||||
/// Only match users
|
||||
User,
|
||||
}
|
||||
);
|
||||
|
||||
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
let mut set = (2..9999)
|
||||
.map(|v| format!("{:0>4}", v))
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
for discrim in [
|
||||
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
|
||||
] {
|
||||
set.remove(&format!("{:0>4}", discrim));
|
||||
}
|
||||
|
||||
set.into_iter().collect()
|
||||
});
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: Default::default(),
|
||||
username: Default::default(),
|
||||
discriminator: Default::default(),
|
||||
display_name: Default::default(),
|
||||
avatar: Default::default(),
|
||||
relations: Default::default(),
|
||||
badges: Default::default(),
|
||||
status: Default::default(),
|
||||
profile: Default::default(),
|
||||
flags: Default::default(),
|
||||
privileged: Default::default(),
|
||||
bot: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl User {
|
||||
/// Check whether a username is already in use by another user
|
||||
#[allow(dead_code)]
|
||||
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
|
||||
match db.fetch_user_by_username(username).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(Error {
|
||||
error_type: ErrorType::NotFound,
|
||||
..
|
||||
}) => Ok(false),
|
||||
Err(error) => Err(error),
|
||||
/// Create a new user
|
||||
pub async fn create<I, D>(
|
||||
db: &Database,
|
||||
username: String,
|
||||
account_id: I,
|
||||
data: D,
|
||||
) -> Result<User>
|
||||
where
|
||||
I: Into<Option<String>>,
|
||||
D: Into<Option<PartialUser>>,
|
||||
{
|
||||
let username = User::validate_username(username)?;
|
||||
let mut user = User {
|
||||
id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()),
|
||||
discriminator: User::find_discriminator(db, &username, None).await?,
|
||||
username,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(data) = data.into() {
|
||||
user.apply_options(data);
|
||||
}
|
||||
|
||||
db.insert_user(&user).await?;
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Get limits for this user
|
||||
pub async fn limits(&self) -> FeaturesLimits {
|
||||
let config = config().await;
|
||||
if ulid::Ulid::from_str(&self.id)
|
||||
.expect("`ulid`")
|
||||
.datetime()
|
||||
.elapsed()
|
||||
.expect("time went backwards")
|
||||
<= Duration::from_secs(86400u64 * config.features.limits.global.new_user_days as u64)
|
||||
{
|
||||
config.features.limits.new_user
|
||||
} else {
|
||||
config.features.limits.default
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the relationship with another user
|
||||
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
|
||||
if self.id == user_b {
|
||||
return RelationshipStatus::User;
|
||||
}
|
||||
|
||||
if let Some(relations) = &self.relations {
|
||||
if let Some(relationship) = relations.iter().find(|x| x.id == user_b) {
|
||||
return relationship.status.clone();
|
||||
}
|
||||
}
|
||||
|
||||
RelationshipStatus::None
|
||||
}
|
||||
|
||||
pub fn is_friends_with(&self, user_b: &str) -> bool {
|
||||
matches!(
|
||||
self.relationship_with(user_b),
|
||||
RelationshipStatus::Friend | RelationshipStatus::User
|
||||
)
|
||||
}
|
||||
|
||||
/// Check whether two users have a mutual connection
|
||||
///
|
||||
/// This will check if user and user_b share a server or a group.
|
||||
pub async fn has_mutual_connection(&self, db: &Database, user_b: &str) -> Result<bool> {
|
||||
Ok(!db
|
||||
.fetch_mutual_server_ids(&self.id, user_b)
|
||||
.await?
|
||||
.is_empty()
|
||||
|| !db
|
||||
.fetch_mutual_channel_ids(&self.id, user_b)
|
||||
.await?
|
||||
.is_empty())
|
||||
}
|
||||
|
||||
/// Check if this user can acquire another server
|
||||
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
|
||||
if db.fetch_server_count(&self.id).await? <= self.limits().await.servers {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(TooManyServers {
|
||||
max: self.limits().await.servers
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitise and validate a username can be used
|
||||
pub fn validate_username(username: String) -> Result<String> {
|
||||
// Copy the username for validation
|
||||
let username_lowercase = username.to_lowercase();
|
||||
|
||||
// Block homoglyphs
|
||||
if decancer::cure(&username_lowercase).into_str() != username_lowercase {
|
||||
return Err(create_error!(InvalidUsername));
|
||||
}
|
||||
|
||||
// Ensure the username itself isn't blocked
|
||||
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"];
|
||||
|
||||
for username in BLOCKED_USERNAMES {
|
||||
if username_lowercase == *username {
|
||||
return Err(create_error!(InvalidUsername));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure none of the following substrings show up in the username
|
||||
const BLOCKED_SUBSTRINGS: &[&str] = &["```"];
|
||||
|
||||
for substr in BLOCKED_SUBSTRINGS {
|
||||
if username_lowercase.contains(substr) {
|
||||
return Err(create_error!(InvalidUsername));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
/// Find a user and session ID from a given token and hint
|
||||
#[async_recursion]
|
||||
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<(User, String)> {
|
||||
match hint {
|
||||
UserHint::Bot => Ok((
|
||||
db.fetch_user(
|
||||
&db.fetch_bot_by_token(token)
|
||||
.await
|
||||
.map_err(|_| create_error!(InvalidSession))?
|
||||
.id,
|
||||
)
|
||||
.await?,
|
||||
String::new(),
|
||||
)),
|
||||
UserHint::User => {
|
||||
let session = db.fetch_session_by_token(token).await?;
|
||||
Ok((db.fetch_user(&session.user_id).await?, session.id))
|
||||
}
|
||||
UserHint::Any => {
|
||||
if let Ok(result) = User::from_token(db, token, UserHint::User).await {
|
||||
Ok(result)
|
||||
} else {
|
||||
User::from_token(db, token, UserHint::Bot).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to fetch many users as a mutually connected user
|
||||
/// (while optimising the online ID query)
|
||||
pub async fn fetch_many_ids_as_mutuals(
|
||||
db: &Database,
|
||||
perspective: &User,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<v0::User>> {
|
||||
let online_ids = filter_online(ids).await;
|
||||
|
||||
Ok(db
|
||||
.fetch_users(ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|user| {
|
||||
let is_online = online_ids.contains(&user.id);
|
||||
user.into_known(perspective, is_online)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Find a free discriminator for a given username
|
||||
pub async fn find_discriminator(
|
||||
db: &Database,
|
||||
username: &str,
|
||||
preferred: Option<(String, String)>,
|
||||
) -> Result<String> {
|
||||
let search_space: &HashSet<String> = &DISCRIMINATOR_SEARCH_SPACE;
|
||||
let used_discriminators: HashSet<String> = db
|
||||
.fetch_discriminators_in_use(username)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let available_discriminators: Vec<&String> =
|
||||
search_space.difference(&used_discriminators).collect();
|
||||
|
||||
if available_discriminators.is_empty() {
|
||||
return Err(create_error!(UsernameTaken));
|
||||
}
|
||||
|
||||
if let Some((preferred, target_id)) = preferred {
|
||||
if available_discriminators.contains(&&preferred) {
|
||||
return Ok(preferred);
|
||||
} else {
|
||||
if db
|
||||
.has_ratelimited(
|
||||
&target_id,
|
||||
crate::RatelimitEventType::DiscriminatorChange,
|
||||
Duration::from_secs(60 * 60 * 24),
|
||||
1,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(create_error!(DiscriminatorChangeRatelimited));
|
||||
}
|
||||
|
||||
RatelimitEvent::create(
|
||||
db,
|
||||
target_id,
|
||||
crate::RatelimitEventType::DiscriminatorChange,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
Ok(available_discriminators
|
||||
.choose(&mut rng)
|
||||
.expect("we can assert this has an element")
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Update a user's username
|
||||
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
|
||||
let username = User::validate_username(username)?;
|
||||
if self.username.to_lowercase() == username.to_lowercase() {
|
||||
self.update(
|
||||
db,
|
||||
PartialUser {
|
||||
username: Some(username),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
self.update(
|
||||
db,
|
||||
PartialUser {
|
||||
discriminator: Some(
|
||||
User::find_discriminator(
|
||||
db,
|
||||
&username,
|
||||
Some((self.discriminator.to_string(), self.id.clone())),
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
username: Some(username),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a relationship to another user
|
||||
pub async fn set_relationship(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
user_b: &User,
|
||||
status: RelationshipStatus,
|
||||
) -> Result<()> {
|
||||
db.set_relationship(&self.id, &user_b.id, &status).await?;
|
||||
|
||||
if let RelationshipStatus::None | RelationshipStatus::User = status {
|
||||
if let Some(relations) = &mut self.relations {
|
||||
relations.retain(|relation| relation.id != user_b.id);
|
||||
}
|
||||
} else {
|
||||
let relation = Relationship {
|
||||
id: user_b.id.to_string(),
|
||||
status,
|
||||
};
|
||||
|
||||
if let Some(relations) = &mut self.relations {
|
||||
relations.retain(|relation| relation.id != user_b.id);
|
||||
relations.push(relation);
|
||||
} else {
|
||||
self.relations = Some(vec![relation]);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a certain relationship between two users
|
||||
pub async fn apply_relationship(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
target: &mut User,
|
||||
local: RelationshipStatus,
|
||||
remote: RelationshipStatus,
|
||||
) -> Result<()> {
|
||||
target.set_relationship(db, self, remote).await?;
|
||||
self.set_relationship(db, target, local).await?;
|
||||
|
||||
EventV1::UserRelationship {
|
||||
id: target.id.clone(),
|
||||
user: self.clone().into(db, Some(&*target)).await,
|
||||
}
|
||||
.private(target.id.clone())
|
||||
.await;
|
||||
|
||||
EventV1::UserRelationship {
|
||||
id: self.id.clone(),
|
||||
user: target.clone().into(db, Some(&*self)).await,
|
||||
}
|
||||
.private(self.id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add another user as a friend
|
||||
pub async fn add_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
|
||||
match self.relationship_with(&target.id) {
|
||||
RelationshipStatus::User => Err(create_error!(NoEffect)),
|
||||
RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
|
||||
RelationshipStatus::Outgoing => Err(create_error!(AlreadySentRequest)),
|
||||
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
|
||||
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
|
||||
RelationshipStatus::Incoming => {
|
||||
// Accept incoming friend request
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
RelationshipStatus::Friend,
|
||||
RelationshipStatus::Friend,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RelationshipStatus::None => {
|
||||
// Get this user's current count of outgoing friend requests
|
||||
let count = self
|
||||
.relations
|
||||
.as_ref()
|
||||
.map(|relations| {
|
||||
relations
|
||||
.iter()
|
||||
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
|
||||
.count()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// If we're over the limit, don't allow creating more requests
|
||||
if count >= self.limits().await.outgoing_friend_requests {
|
||||
return Err(create_error!(TooManyPendingFriendRequests {
|
||||
max: self.limits().await.outgoing_friend_requests
|
||||
}));
|
||||
}
|
||||
|
||||
// Send the friend request
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
RelationshipStatus::Outgoing,
|
||||
RelationshipStatus::Incoming,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove another user as a friend
|
||||
pub async fn remove_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
|
||||
match self.relationship_with(&target.id) {
|
||||
RelationshipStatus::Friend
|
||||
| RelationshipStatus::Outgoing
|
||||
| RelationshipStatus::Incoming => {
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
RelationshipStatus::None,
|
||||
RelationshipStatus::None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => Err(create_error!(NoEffect)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Block another user
|
||||
pub async fn block_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
|
||||
match self.relationship_with(&target.id) {
|
||||
RelationshipStatus::User | RelationshipStatus::Blocked => Err(create_error!(NoEffect)),
|
||||
RelationshipStatus::BlockedOther => {
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
RelationshipStatus::Blocked,
|
||||
RelationshipStatus::Blocked,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RelationshipStatus::None
|
||||
| RelationshipStatus::Friend
|
||||
| RelationshipStatus::Incoming
|
||||
| RelationshipStatus::Outgoing => {
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
RelationshipStatus::Blocked,
|
||||
RelationshipStatus::BlockedOther,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unblock another user
|
||||
pub async fn unblock_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
|
||||
match self.relationship_with(&target.id) {
|
||||
RelationshipStatus::Blocked => match target.relationship_with(&self.id) {
|
||||
RelationshipStatus::Blocked => {
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
RelationshipStatus::BlockedOther,
|
||||
RelationshipStatus::Blocked,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RelationshipStatus::BlockedOther => {
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
RelationshipStatus::None,
|
||||
RelationshipStatus::None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
_ => Err(create_error!(InternalError)),
|
||||
},
|
||||
_ => Err(create_error!(NoEffect)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,13 +622,14 @@ impl User {
|
||||
self.apply_options(partial.clone());
|
||||
db.update_user(&self.id, &partial, remove.clone()).await?;
|
||||
|
||||
/* // TODO: EventV1::UserUpdate {
|
||||
EventV1::UserUpdate {
|
||||
id: self.id.clone(),
|
||||
data: partial,
|
||||
clear: remove,
|
||||
data: partial.into(),
|
||||
clear: remove.into_iter().map(|v| v.into()).collect(),
|
||||
event_id: Some(Ulid::new().to_string()),
|
||||
}
|
||||
.p_user(self.id.clone(), db)
|
||||
.await; */
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -162,7 +640,7 @@ impl User {
|
||||
FieldsUser::Avatar => self.avatar = None,
|
||||
FieldsUser::StatusText => {
|
||||
if let Some(x) = self.status.as_mut() {
|
||||
x.text = String::new();
|
||||
x.text = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::StatusPresence => {
|
||||
@@ -172,7 +650,7 @@ impl User {
|
||||
}
|
||||
FieldsUser::ProfileContent => {
|
||||
if let Some(x) = self.profile.as_mut() {
|
||||
x.content = String::new();
|
||||
x.content = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::ProfileBackground => {
|
||||
@@ -180,9 +658,18 @@ impl User {
|
||||
x.background = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::DisplayName => self.display_name = None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Suspend the user
|
||||
pub async fn suspend(&mut self, db: &Database) -> Result<()> {
|
||||
// Remove sessions (logout all)
|
||||
// Mark user as suspended
|
||||
// Disable account
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark as deleted
|
||||
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
|
||||
self.update(
|
||||
@@ -203,17 +690,3 @@ impl User {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
let mut set = (2..9999)
|
||||
.map(|v| format!("{:0>4}", v))
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
for discrim in [
|
||||
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
|
||||
] {
|
||||
set.remove(&format!("{:0>4}", discrim));
|
||||
}
|
||||
|
||||
set.into_iter().collect()
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use authifier::models::Session;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
@@ -14,14 +15,17 @@ pub trait AbstractUsers: Sync + Send {
|
||||
async fn fetch_user(&self, id: &str) -> Result<User>;
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User>;
|
||||
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, token: &str) -> Result<Session>;
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
|
||||
|
||||
/// Fetch all discriminators in use for a username
|
||||
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>>;
|
||||
|
||||
/// Fetch ids of users that both users are friends with
|
||||
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
|
||||
|
||||
@@ -54,4 +58,7 @@ pub trait AbstractUsers: Sync + Send {
|
||||
|
||||
/// Delete a user by their id
|
||||
async fn delete_user(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Remove push subscription for a session by session id (TODO: remove)
|
||||
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -25,13 +25,14 @@ impl AbstractUsers for MongoDb {
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
|
||||
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
|
||||
query!(
|
||||
self,
|
||||
find_one_with_options,
|
||||
COL,
|
||||
doc! {
|
||||
"username": username
|
||||
"username": username,
|
||||
"discriminator": discriminator
|
||||
},
|
||||
FindOneOptions::builder()
|
||||
.collation(
|
||||
@@ -45,10 +46,9 @@ impl AbstractUsers for MongoDb {
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
|
||||
let session = self
|
||||
.col::<Session>("sessions")
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
|
||||
self.col::<Session>("sessions")
|
||||
.find_one(
|
||||
doc! {
|
||||
"token": token
|
||||
@@ -57,9 +57,7 @@ impl AbstractUsers for MongoDb {
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find_one", "sessions"))?
|
||||
.ok_or_else(|| create_error!(InvalidSession))?;
|
||||
|
||||
self.fetch_user(&session.id).await
|
||||
.ok_or_else(|| create_error!(InvalidSession))
|
||||
}
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
@@ -87,6 +85,39 @@ impl AbstractUsers for MongoDb {
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch all discriminators in use for a username
|
||||
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
|
||||
#[derive(Deserialize)]
|
||||
struct UserDocument {
|
||||
discriminator: String,
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.col::<UserDocument>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"username": username
|
||||
},
|
||||
FindOptions::builder()
|
||||
.collation(
|
||||
Collation::builder()
|
||||
.locale("en")
|
||||
.strength(CollationStrength::Secondary)
|
||||
.build(),
|
||||
)
|
||||
.projection(doc! { "_id": 0, "discriminator": 1 })
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async { s.ok() })
|
||||
.collect::<Vec<UserDocument>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| user.discriminator)
|
||||
.collect::<Vec<String>>())
|
||||
}
|
||||
|
||||
/// Fetch ids of users that both users are friends with
|
||||
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
|
||||
Ok(self
|
||||
@@ -285,6 +316,25 @@ impl AbstractUsers for MongoDb {
|
||||
async fn delete_user(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
|
||||
/// Remove push subscription for a session by session id (TODO: remove)
|
||||
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()> {
|
||||
self.col::<User>("sessions")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": session_id
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
"subscription": 1
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsUser {
|
||||
@@ -295,6 +345,7 @@ impl IntoDocumentPath for FieldsUser {
|
||||
FieldsUser::ProfileContent => "profile.content",
|
||||
FieldsUser::StatusPresence => "status.presence",
|
||||
FieldsUser::StatusText => "status.text",
|
||||
FieldsUser::DisplayName => "display_name",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use authifier::models::Session;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
use crate::{ReferenceDb, Relationship};
|
||||
|
||||
use super::AbstractUsers;
|
||||
|
||||
@@ -28,18 +29,20 @@ impl AbstractUsers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
|
||||
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
|
||||
let users = self.users.lock().await;
|
||||
let lowercase = username.to_lowercase();
|
||||
users
|
||||
.values()
|
||||
.find(|user| user.username.to_lowercase() == lowercase)
|
||||
.find(|user| {
|
||||
user.username.to_lowercase() == lowercase && user.discriminator == discriminator
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> {
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, _token: &str) -> Result<Session> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -56,6 +59,18 @@ impl AbstractUsers for ReferenceDb {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fetch all discriminators in use for a username
|
||||
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
|
||||
let users = self.users.lock().await;
|
||||
let lowercase = username.to_lowercase();
|
||||
Ok(users
|
||||
.values()
|
||||
.filter(|user| user.username.to_lowercase() == lowercase)
|
||||
.map(|user| &user.discriminator)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch ids of users that both users are friends with
|
||||
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
||||
todo!()
|
||||
@@ -94,19 +109,49 @@ impl AbstractUsers for ReferenceDb {
|
||||
|
||||
/// Set relationship with another user
|
||||
///
|
||||
/// This should use pull_relationship if relationship is None.
|
||||
/// This should use pull_relationship if relationship is None or User.
|
||||
async fn set_relationship(
|
||||
&self,
|
||||
_user_id: &str,
|
||||
_target_id: &str,
|
||||
_relationship: &RelationshipStatus,
|
||||
user_id: &str,
|
||||
target_id: &str,
|
||||
relationship: &RelationshipStatus,
|
||||
) -> Result<()> {
|
||||
todo!()
|
||||
if let RelationshipStatus::User | RelationshipStatus::None = &relationship {
|
||||
self.pull_relationship(user_id, target_id).await
|
||||
} else {
|
||||
let mut users = self.users.lock().await;
|
||||
let user = users
|
||||
.get_mut(user_id)
|
||||
.ok_or_else(|| create_error!(NotFound))?;
|
||||
|
||||
let relation = Relationship {
|
||||
id: target_id.to_string(),
|
||||
status: relationship.clone(),
|
||||
};
|
||||
|
||||
if let Some(relations) = &mut user.relations {
|
||||
relations.retain(|relation| relation.id != target_id);
|
||||
relations.push(relation);
|
||||
} else {
|
||||
user.relations = Some(vec![relation]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove relationship with another user
|
||||
async fn pull_relationship(&self, _user_id: &str, _target_id: &str) -> Result<()> {
|
||||
todo!()
|
||||
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
|
||||
let mut users = self.users.lock().await;
|
||||
let user = users
|
||||
.get_mut(user_id)
|
||||
.ok_or_else(|| create_error!(NotFound))?;
|
||||
|
||||
if let Some(relations) = &mut user.relations {
|
||||
relations.retain(|relation| relation.id != target_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a user by their id
|
||||
@@ -118,4 +163,9 @@ impl AbstractUsers for ReferenceDb {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove push subscription for a session by session id (TODO: remove)
|
||||
async fn remove_push_subscription_by_session_id(&self, _session_id: &str) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,19 @@ impl<'r> FromRequest<'r> for User {
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
let _header_bot_token = request
|
||||
let header_bot_token = request
|
||||
.headers()
|
||||
.get("x-bot-token")
|
||||
.next()
|
||||
.map(|x| x.to_string());
|
||||
|
||||
/* if let Some(bot_token) = header_bot_token {
|
||||
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await {
|
||||
return Some(user);
|
||||
if let Some(bot_token) = header_bot_token {
|
||||
if let Ok(bot) = db.fetch_bot_by_token(&bot_token).await {
|
||||
if let Ok(user) = db.fetch_user(&bot.id).await {
|
||||
return Some(user);
|
||||
}
|
||||
}
|
||||
} else */
|
||||
if let Outcome::Success(session) = request.guard::<Session>().await {
|
||||
// This uses a guard so can't really easily be refactored into from_token at this stage.
|
||||
} else if let Outcome::Success(session) = request.guard::<Session>().await {
|
||||
if let Ok(user) = db.fetch_user(&session.user_id).await {
|
||||
return Some(user);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
|
||||
use revolt_rocket_okapi::{
|
||||
gen::OpenApiGenerator,
|
||||
request::{OpenApiFromRequest, RequestHeaderInput},
|
||||
};
|
||||
|
||||
use crate::User;
|
||||
|
||||
impl<'r> OpenApiFromRequest<'r> for User {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
_name: String,
|
||||
_required: bool,
|
||||
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
||||
let mut requirements = schemars::Map::new();
|
||||
requirements.insert("Session Token".to_owned(), vec![]);
|
||||
|
||||
Ok(RequestHeaderInput::Security(
|
||||
"Session Token".to_owned(),
|
||||
SecurityScheme {
|
||||
data: SecuritySchemeData::ApiKey {
|
||||
name: "x-session-token".to_owned(),
|
||||
location: "header".to_owned(),
|
||||
},
|
||||
description: Some("Used to authenticate as a user.".to_owned()),
|
||||
extensions: schemars::Map::new(),
|
||||
},
|
||||
requirements,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ use deadqueue::limited::Queue;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use super::DelayedTask;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::{apple_notifications::{self, ApnJob}, DelayedTask};
|
||||
|
||||
/// Enumeration of possible events
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@@ -52,8 +54,43 @@ pub async fn queue(channel: String, user: String, event: AckEvent) {
|
||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||
}
|
||||
|
||||
pub async fn handle_ack_event(event: &AckEvent, db: &Database, authifier_db: &authifier::Database, user: &str, channel: &str) -> Result<()> {
|
||||
match &event {
|
||||
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
|
||||
AckEvent::AckMessage { id } => {
|
||||
let unread = db.fetch_unread(user, channel).await?;
|
||||
let updated = db.acknowledge_message(channel, user, id).await?;
|
||||
|
||||
if let (Some(before), Some(after)) = (unread, updated) {
|
||||
let before_mentions = before.mentions.unwrap_or_default().len();
|
||||
let after_mentions = after.mentions.unwrap_or_default().len();
|
||||
|
||||
let mentions_acked = before_mentions - after_mentions;
|
||||
|
||||
if mentions_acked > 0 {
|
||||
if let Ok(sessions) = authifier_db.find_sessions(user).await {
|
||||
for session in sessions {
|
||||
if let Some(sub) = session.subscription {
|
||||
if sub.endpoint == "apn" {
|
||||
apple_notifications::queue(ApnJob::from_ack(session.id, user.to_string(), sub.auth)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
},
|
||||
AckEvent::AddMention { ids } => {
|
||||
db.add_mention_to_unread(channel, user, ids).await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
pub async fn worker(db: Database, authifier_db: authifier::Database) {
|
||||
let mut tasks = HashMap::<(String, String), DelayedTask<Task>>::new();
|
||||
let mut keys = vec![];
|
||||
|
||||
@@ -71,12 +108,7 @@ pub async fn worker(db: Database) {
|
||||
let Task { event } = task.data;
|
||||
let (user, channel) = key;
|
||||
|
||||
if let Err(err) = match &event {
|
||||
AckEvent::AckMessage { id } => db.acknowledge_message(channel, user, id).await,
|
||||
AckEvent::AddMention { ids } => {
|
||||
db.add_mention_to_unread(channel, user, ids).await
|
||||
}
|
||||
} {
|
||||
if let Err(err) = handle_ack_event(&event, &db, &authifier_db, user, channel).await {
|
||||
error!("{err:?} for {event:?}. ({user}, {channel})");
|
||||
} else {
|
||||
info!("User {user} ack in {channel} with {event:?}");
|
||||
@@ -0,0 +1,312 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use base64::{
|
||||
engine::{self},
|
||||
Engine as _,
|
||||
};
|
||||
use deadqueue::limited::Queue;
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_a2::{
|
||||
request::{
|
||||
notification::{DefaultAlert, NotificationOptions},
|
||||
payload::{APSAlert, APSSound, PayloadLike, APS},
|
||||
},
|
||||
Client, ClientConfig, Endpoint, Error, ErrorBody, ErrorReason, Priority, PushType, Response,
|
||||
};
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::{Message, PushNotification};
|
||||
|
||||
use crate::Database;
|
||||
|
||||
/// Payload information, before assembly
|
||||
#[derive(Debug)]
|
||||
pub struct ApnPayload {
|
||||
message: Message,
|
||||
url: String,
|
||||
authorAvatar: String,
|
||||
authorDisplayName: String,
|
||||
channelName: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct Payload<'a> {
|
||||
aps: APS<'a>,
|
||||
#[serde(skip_serializing)]
|
||||
options: NotificationOptions<'a>,
|
||||
#[serde(skip_serializing)]
|
||||
device_token: &'a str,
|
||||
|
||||
message: &'a Message,
|
||||
url: &'a str,
|
||||
authorAvatar: &'a str,
|
||||
authorDisplayName: &'a str,
|
||||
channelName: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> PayloadLike for Payload<'a> {
|
||||
fn get_device_token(&self) -> &'a str {
|
||||
self.device_token
|
||||
}
|
||||
fn get_options(&self) -> &NotificationOptions {
|
||||
&self.options
|
||||
}
|
||||
}
|
||||
|
||||
/// Task information
|
||||
#[derive(Debug)]
|
||||
pub struct AlertJob {
|
||||
/// Session Id
|
||||
session_id: String,
|
||||
|
||||
/// Device token
|
||||
device_token: String,
|
||||
|
||||
/// User Id
|
||||
user_id: String,
|
||||
|
||||
/// Title
|
||||
title: String,
|
||||
|
||||
/// Body
|
||||
body: String,
|
||||
|
||||
/// Thread Id
|
||||
thread_id: String,
|
||||
|
||||
/// Category (informs the client what kind of notification is being sent.)
|
||||
category: String,
|
||||
|
||||
/// Payload used by the iOS client to modify the notification
|
||||
custom_payload: ApnPayload,
|
||||
}
|
||||
|
||||
impl AlertJob {
|
||||
fn format_title(notification: &PushNotification) -> String {
|
||||
// ideally this changes depending on context
|
||||
// in a server, it would look like "Sendername, #channelname in servername"
|
||||
// in a group, it would look like "Sendername in groupname"
|
||||
// in a dm it should just be "Sendername".
|
||||
// not sure how feasible all those are given the PushNotification object as it currently stands.
|
||||
format!(
|
||||
"{} in {}",
|
||||
notification.author, notification.message.channel
|
||||
) // TODO: this absolutely needs a channel name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BadgeJob {
|
||||
/// Session Id
|
||||
session_id: String,
|
||||
|
||||
/// Device token
|
||||
device_token: String,
|
||||
|
||||
/// User Id
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum JobType {
|
||||
Alert(AlertJob),
|
||||
Badge(BadgeJob),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApnJob {
|
||||
job_type: JobType,
|
||||
}
|
||||
|
||||
impl ApnJob {
|
||||
pub fn from_notification(
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
device_token: String,
|
||||
notification: &PushNotification,
|
||||
) -> ApnJob {
|
||||
ApnJob {
|
||||
job_type: JobType::Alert(AlertJob {
|
||||
session_id,
|
||||
device_token,
|
||||
user_id,
|
||||
title: AlertJob::format_title(notification),
|
||||
body: notification.body.to_string(),
|
||||
thread_id: notification.tag.to_string(),
|
||||
category: "ALERT_MESSAGE".to_string(),
|
||||
custom_payload: ApnPayload {
|
||||
message: notification.message.clone(),
|
||||
url: notification.url.clone(),
|
||||
authorAvatar: notification.icon.clone(),
|
||||
authorDisplayName: notification.author.clone(),
|
||||
channelName: "#fetchchannelnamehere".to_string(), // TODO: get actual channel name
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_ack(session_id: String, user_id: String, device_token: String) -> ApnJob {
|
||||
ApnJob {
|
||||
job_type: JobType::Badge(BadgeJob {
|
||||
session_id,
|
||||
device_token,
|
||||
user_id,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AssembledPayload<'a> {
|
||||
Alert(Payload<'a>),
|
||||
Default(revolt_a2::request::payload::Payload<'a>),
|
||||
}
|
||||
|
||||
static Q: Lazy<Queue<ApnJob>> = Lazy::new(|| Queue::new(10_000));
|
||||
|
||||
/// Queue a new task for a worker
|
||||
pub async fn queue(task: ApnJob) {
|
||||
Q.try_push(task).ok();
|
||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||
}
|
||||
|
||||
async fn get_badge_count(db: &Database, user: &str) -> Option<u32> {
|
||||
if let Ok(unreads) = db.fetch_unreads(user).await {
|
||||
let mut mention_count = 0;
|
||||
for channel in unreads {
|
||||
if let Some(mentions) = channel.mentions {
|
||||
mention_count += mentions.len() as u32
|
||||
}
|
||||
}
|
||||
|
||||
return Some(mention_count);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
let config = config().await;
|
||||
if config.api.apn.pkcs8.is_empty()
|
||||
|| config.api.apn.key_id.is_empty()
|
||||
|| config.api.apn.team_id.is_empty()
|
||||
{
|
||||
eprintln!("Missing APN keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
let endpoint = if config.api.apn.sandbox {
|
||||
Endpoint::Sandbox
|
||||
} else {
|
||||
Endpoint::Production
|
||||
};
|
||||
|
||||
let pkcs8 = engine::general_purpose::STANDARD
|
||||
.decode(config.api.apn.pkcs8)
|
||||
.expect("valid `pcks8`");
|
||||
|
||||
let client_config = ClientConfig::new(endpoint);
|
||||
|
||||
let client = Client::token(
|
||||
&mut Cursor::new(pkcs8),
|
||||
config.api.apn.key_id,
|
||||
config.api.apn.team_id,
|
||||
client_config,
|
||||
)
|
||||
.expect("could not create APN client");
|
||||
|
||||
let payload_options = NotificationOptions {
|
||||
apns_id: None,
|
||||
apns_push_type: Some(PushType::Alert),
|
||||
apns_expiration: None,
|
||||
apns_priority: Some(Priority::High),
|
||||
apns_topic: Some("chat.revolt.app"),
|
||||
apns_collapse_id: None,
|
||||
};
|
||||
|
||||
loop {
|
||||
let task = Q.pop().await;
|
||||
let payload: AssembledPayload;
|
||||
|
||||
match task.job_type {
|
||||
JobType::Alert(ref alert) => {
|
||||
payload = AssembledPayload::Alert(Payload {
|
||||
aps: APS {
|
||||
alert: Some(APSAlert::Default(DefaultAlert {
|
||||
title: Some(&alert.title),
|
||||
subtitle: None,
|
||||
body: Some(&alert.body),
|
||||
title_loc_key: None,
|
||||
title_loc_args: None,
|
||||
action_loc_key: None,
|
||||
loc_key: None,
|
||||
loc_args: None,
|
||||
launch_image: None,
|
||||
})),
|
||||
badge: get_badge_count(&db, &alert.user_id).await,
|
||||
sound: Some(APSSound::Sound("default")),
|
||||
thread_id: Some(&alert.thread_id),
|
||||
content_available: None,
|
||||
category: Some(&alert.category),
|
||||
mutable_content: Some(1),
|
||||
url_args: None,
|
||||
},
|
||||
device_token: &alert.device_token,
|
||||
options: payload_options.clone(),
|
||||
message: &alert.custom_payload.message,
|
||||
url: &alert.custom_payload.url,
|
||||
authorAvatar: &alert.custom_payload.authorAvatar,
|
||||
authorDisplayName: &alert.custom_payload.authorDisplayName,
|
||||
channelName: &alert.custom_payload.channelName,
|
||||
});
|
||||
}
|
||||
JobType::Badge(ref alert) => {
|
||||
payload = AssembledPayload::Default(revolt_a2::request::payload::Payload {
|
||||
aps: APS {
|
||||
alert: None,
|
||||
badge: get_badge_count(&db, &alert.user_id).await,
|
||||
sound: None,
|
||||
thread_id: None,
|
||||
content_available: None,
|
||||
category: None,
|
||||
mutable_content: None,
|
||||
url_args: None,
|
||||
},
|
||||
device_token: &alert.device_token,
|
||||
options: payload_options.clone(),
|
||||
data: std::collections::BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let resp = match payload {
|
||||
AssembledPayload::Alert(p) => client.send(p).await,
|
||||
AssembledPayload::Default(p) => client.send(p).await,
|
||||
};
|
||||
//println!("response from APNS: {:?}", resp);
|
||||
|
||||
if let Err(err) = resp {
|
||||
match err {
|
||||
Error::ResponseError(Response {
|
||||
error:
|
||||
Some(ErrorBody {
|
||||
reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered,
|
||||
..
|
||||
}),
|
||||
..
|
||||
}) => {
|
||||
if let Err(err) = db
|
||||
.remove_push_subscription_by_session_id(match task.job_type {
|
||||
JobType::Alert(ref a) => &a.session_id.as_str(),
|
||||
JobType::Badge(ref a) => &a.session_id.as_str(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
}
|
||||
err => {
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
// Queue Type: Debounced
|
||||
use crate::{models::channel::PartialChannel, Database};
|
||||
|
||||
use deadqueue::limited::Queue;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use crate::{Database, PartialChannel};
|
||||
|
||||
use super::DelayedTask;
|
||||
|
||||
/// Task information
|
||||
@@ -8,17 +8,20 @@ use std::time::Instant;
|
||||
const WORKER_COUNT: usize = 5;
|
||||
|
||||
pub mod ack;
|
||||
pub mod apple_notifications;
|
||||
pub mod last_message_id;
|
||||
pub mod process_embeds;
|
||||
pub mod web_push;
|
||||
|
||||
/// Spawn background workers
|
||||
pub async fn start_workers(db: Database) {
|
||||
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
|
||||
task::spawn(apple_notifications::worker(db.clone()));
|
||||
|
||||
for _ in 0..WORKER_COUNT {
|
||||
task::spawn(ack::worker(db.clone()));
|
||||
task::spawn(ack::worker(db.clone(), authifier_db.clone()));
|
||||
task::spawn(last_message_id::worker(db.clone()));
|
||||
task::spawn(process_embeds::worker(db.clone()));
|
||||
task::spawn(web_push::worker(db.clone().into()));
|
||||
task::spawn(web_push::worker(authifier_db.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
use crate::{models::Message, AppendMessage, Database};
|
||||
|
||||
use futures::future::join_all;
|
||||
use linkify::{LinkFinder, LinkKind};
|
||||
use regex::Regex;
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
|
||||
use async_lock::Semaphore;
|
||||
use async_std::task::spawn;
|
||||
use deadqueue::limited::Queue;
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_models::v0::Embed;
|
||||
use std::{collections::HashSet, sync::Arc};
|
||||
|
||||
use isahc::prelude::*;
|
||||
|
||||
/// Task information
|
||||
#[derive(Debug)]
|
||||
struct EmbedTask {
|
||||
/// Channel we're processing the event in
|
||||
channel: String,
|
||||
/// ID of the message we're processing
|
||||
id: String,
|
||||
/// Content of the message
|
||||
content: String,
|
||||
}
|
||||
|
||||
static Q: Lazy<Queue<EmbedTask>> = Lazy::new(|| Queue::new(10_000));
|
||||
|
||||
/// Queue a new task for a worker
|
||||
pub async fn queue(channel: String, id: String, content: String) {
|
||||
Q.try_push(EmbedTask {
|
||||
channel,
|
||||
id,
|
||||
content,
|
||||
})
|
||||
.ok();
|
||||
|
||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
let semaphore = Arc::new(Semaphore::new(
|
||||
config().await.api.workers.max_concurrent_connections,
|
||||
));
|
||||
|
||||
loop {
|
||||
let task = Q.pop().await;
|
||||
let db = db.clone();
|
||||
let semaphore = semaphore.clone();
|
||||
|
||||
spawn(async move {
|
||||
let config = config().await;
|
||||
let embeds = generate(
|
||||
task.content,
|
||||
&config.hosts.january,
|
||||
config.features.limits.global.message_embeds,
|
||||
semaphore,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Ok(embeds) = embeds {
|
||||
if let Err(err) = Message::append(
|
||||
&db,
|
||||
task.id,
|
||||
task.channel,
|
||||
AppendMessage {
|
||||
embeds: Some(embeds),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Encountered an error appending to message: {:?}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static RE_CODE: Lazy<Regex> = Lazy::new(|| Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap());
|
||||
static RE_IGNORED: Lazy<Regex> = Lazy::new(|| Regex::new("(<http.+>)").unwrap());
|
||||
|
||||
pub async fn generate(
|
||||
content: String,
|
||||
host: &str,
|
||||
max_embeds: usize,
|
||||
semaphore: Arc<Semaphore>,
|
||||
) -> Result<Vec<Embed>> {
|
||||
// Ignore code blocks.
|
||||
let content = RE_CODE.replace_all(&content, "");
|
||||
|
||||
// Ignore all content between angle brackets starting with http.
|
||||
let content = RE_IGNORED.replace_all(&content, "");
|
||||
|
||||
let content = content
|
||||
// Ignore quoted lines.
|
||||
.split('\n')
|
||||
.map(|v| {
|
||||
if let Some(c) = v.chars().next() {
|
||||
if c == '>' {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
v
|
||||
})
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
let mut finder = LinkFinder::new();
|
||||
finder.kinds(&[LinkKind::Url]);
|
||||
|
||||
// Process all links, stripping anchors and
|
||||
// only taking up to `max_embeds` of links.
|
||||
let links: Vec<String> = finder
|
||||
.links(&content)
|
||||
.map(|x| {
|
||||
x.as_str()
|
||||
.chars()
|
||||
.take_while(|&ch| ch != '#')
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.take(max_embeds)
|
||||
.collect();
|
||||
|
||||
// If no links, fail out.
|
||||
if links.is_empty() {
|
||||
return Err(create_error!(LabelMe));
|
||||
}
|
||||
|
||||
// TODO: batch request to january
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for link in links {
|
||||
let semaphore = semaphore.clone();
|
||||
let host = host.to_string();
|
||||
tasks.push(spawn(async move {
|
||||
let guard = semaphore.acquire().await;
|
||||
|
||||
if let Ok(mut response) = isahc::get_async(format!(
|
||||
"{host}/embed?url={}",
|
||||
url_escape::encode_component(&link)
|
||||
))
|
||||
.await
|
||||
{
|
||||
drop(guard);
|
||||
response.json::<Embed>().await.ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let embeds = join_all(tasks)
|
||||
.await
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<Embed>>();
|
||||
|
||||
// Prevent database update when no embeds are found.
|
||||
if !embeds.is_empty() {
|
||||
Ok(embeds)
|
||||
} else {
|
||||
Err(create_error!(LabelMe))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use authifier::Database;
|
||||
use base64::{
|
||||
engine::{self},
|
||||
Engine as _,
|
||||
};
|
||||
use deadqueue::limited::Queue;
|
||||
use fcm_v1::auth::{Authenticator, ServiceAccountKey};
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::PushNotification;
|
||||
use revolt_presence::filter_online;
|
||||
use serde_json::json;
|
||||
use web_push::{
|
||||
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
|
||||
WebPushClient, WebPushMessageBuilder,
|
||||
};
|
||||
|
||||
use super::apple_notifications;
|
||||
|
||||
/// Task information
|
||||
#[derive(Debug)]
|
||||
struct PushTask {
|
||||
/// User IDs of the targets that are to receive this notification
|
||||
recipients: Vec<String>,
|
||||
/// Push Notification
|
||||
payload: PushNotification,
|
||||
}
|
||||
|
||||
static Q: Lazy<Queue<PushTask>> = Lazy::new(|| Queue::new(10_000));
|
||||
|
||||
/// Queue a new task for a worker
|
||||
pub async fn queue(recipients: Vec<String>, payload: PushNotification) {
|
||||
if recipients.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let online_ids = filter_online(&recipients).await;
|
||||
let recipients = (&recipients.into_iter().collect::<HashSet<String>>() - &online_ids)
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
Q.try_push(PushTask {
|
||||
recipients,
|
||||
payload,
|
||||
})
|
||||
.ok();
|
||||
|
||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
let config = config().await;
|
||||
|
||||
let web_push_client = IsahcWebPushClient::new().unwrap();
|
||||
let fcm_client = if config.api.fcm.key_type.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(fcm_v1::Client::new(
|
||||
Authenticator::service_account::<&str>(ServiceAccountKey {
|
||||
key_type: Some(config.api.fcm.key_type),
|
||||
project_id: Some(config.api.fcm.project_id.clone()),
|
||||
private_key_id: Some(config.api.fcm.private_key_id),
|
||||
private_key: config.api.fcm.private_key,
|
||||
client_email: config.api.fcm.client_email,
|
||||
client_id: Some(config.api.fcm.client_id),
|
||||
auth_uri: Some(config.api.fcm.auth_uri),
|
||||
token_uri: config.api.fcm.token_uri,
|
||||
auth_provider_x509_cert_url: Some(config.api.fcm.auth_provider_x509_cert_url),
|
||||
client_x509_cert_url: Some(config.api.fcm.client_x509_cert_url),
|
||||
})
|
||||
.await
|
||||
.unwrap(),
|
||||
config.api.fcm.project_id,
|
||||
false,
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
};
|
||||
|
||||
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(config.api.vapid.private_key)
|
||||
.expect("valid `VAPID_PRIVATE_KEY`");
|
||||
|
||||
loop {
|
||||
let task = Q.pop().await;
|
||||
|
||||
if let Ok(sessions) = db.find_sessions_with_subscription(&task.recipients).await {
|
||||
for session in sessions {
|
||||
if let Some(sub) = session.subscription {
|
||||
if sub.endpoint == "fcm" {
|
||||
// Use Firebase Cloud Messaging
|
||||
if let Some(client) = &fcm_client {
|
||||
let PushNotification {
|
||||
author,
|
||||
icon,
|
||||
image,
|
||||
body,
|
||||
tag: _,
|
||||
timestamp,
|
||||
url: _,
|
||||
message,
|
||||
} = &task.payload;
|
||||
|
||||
let message = fcm_v1::message::Message {
|
||||
token: Some(sub.auth),
|
||||
data: Some(HashMap::from([
|
||||
(
|
||||
"author".to_owned(),
|
||||
serde_json::Value::String(author.clone()),
|
||||
),
|
||||
("icon".to_owned(), serde_json::Value::String(icon.clone())),
|
||||
(
|
||||
"image".to_owned(),
|
||||
if let Some(image) = image {
|
||||
serde_json::Value::String(image.clone())
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
},
|
||||
),
|
||||
("body".to_owned(), serde_json::Value::String(body.clone())),
|
||||
("timestamp".to_owned(), json!(timestamp)),
|
||||
("message".to_owned(), json!(&message)),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Err(err) = client.send(&message).await {
|
||||
error!("Failed to send FCM notification! {:?}", err);
|
||||
} else {
|
||||
info!("Sent FCM notification to {:?}.", session.id);
|
||||
}
|
||||
} else {
|
||||
info!("No FCM token was specified!");
|
||||
}
|
||||
} else if sub.endpoint == "apn" {
|
||||
apple_notifications::queue(apple_notifications::ApnJob::from_notification(
|
||||
session.id,
|
||||
session.user_id,
|
||||
sub.auth,
|
||||
&task.payload,
|
||||
))
|
||||
.await;
|
||||
} else {
|
||||
// Use Web Push Standard
|
||||
let subscription = SubscriptionInfo {
|
||||
endpoint: sub.endpoint,
|
||||
keys: SubscriptionKeys {
|
||||
auth: sub.auth,
|
||||
p256dh: sub.p256dh,
|
||||
},
|
||||
};
|
||||
|
||||
match VapidSignatureBuilder::from_pem(
|
||||
std::io::Cursor::new(&web_push_private_key),
|
||||
&subscription,
|
||||
) {
|
||||
Ok(sig_builder) => match sig_builder.build() {
|
||||
Ok(signature) => {
|
||||
let mut builder = WebPushMessageBuilder::new(&subscription);
|
||||
builder.set_vapid_signature(signature);
|
||||
|
||||
let payload = json!(task.payload).to_string();
|
||||
builder
|
||||
.set_payload(ContentEncoding::AesGcm, payload.as_bytes());
|
||||
|
||||
match builder.build() {
|
||||
Ok(msg) => match web_push_client.send(msg).await {
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"Sent Web Push notification to {:?}.",
|
||||
session.id
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Hit error sending Web Push! {:?}", err)
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to build message for {}! {:?}",
|
||||
session.user_id, err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => error!(
|
||||
"Failed to build signature for {}! {:?}",
|
||||
session.user_id, err
|
||||
),
|
||||
},
|
||||
Err(err) => error!(
|
||||
"Failed to create signature builder for {}! {:?}",
|
||||
session.user_id, err
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
use revolt_models::v0::*;
|
||||
use revolt_permissions::{calculate_user_permissions, UserPermission};
|
||||
|
||||
use crate::{util::permissions::DatabasePermissionQuery, Database};
|
||||
|
||||
impl crate::Bot {
|
||||
pub fn into_public_bot(self, user: crate::User) -> PublicBot {
|
||||
@@ -12,6 +15,7 @@ impl crate::Bot {
|
||||
description: user
|
||||
.profile
|
||||
.map(|profile| profile.content)
|
||||
.flatten()
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
@@ -34,6 +38,24 @@ impl From<crate::Bot> for Bot {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsBot> for crate::FieldsBot {
|
||||
fn from(value: FieldsBot) -> Self {
|
||||
match value {
|
||||
FieldsBot::InteractionsURL => crate::FieldsBot::InteractionsURL,
|
||||
FieldsBot::Token => crate::FieldsBot::Token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsBot> for FieldsBot {
|
||||
fn from(value: crate::FieldsBot) -> Self {
|
||||
match value {
|
||||
crate::FieldsBot::InteractionsURL => FieldsBot::InteractionsURL,
|
||||
crate::FieldsBot::Token => FieldsBot::Token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Invite> for Invite {
|
||||
fn from(value: crate::Invite) -> Self {
|
||||
match value {
|
||||
@@ -198,6 +220,86 @@ impl From<crate::Channel> for Channel {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Channel> for crate::Channel {
|
||||
fn from(value: Channel) -> crate::Channel {
|
||||
match value {
|
||||
Channel::SavedMessages { id, user } => crate::Channel::SavedMessages { id, user },
|
||||
Channel::DirectMessage {
|
||||
id,
|
||||
active,
|
||||
recipients,
|
||||
last_message_id,
|
||||
} => crate::Channel::DirectMessage {
|
||||
id,
|
||||
active,
|
||||
recipients,
|
||||
last_message_id,
|
||||
},
|
||||
Channel::Group {
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
recipients,
|
||||
icon,
|
||||
last_message_id,
|
||||
permissions,
|
||||
nsfw,
|
||||
} => crate::Channel::Group {
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
recipients,
|
||||
icon: icon.map(|file| file.into()),
|
||||
last_message_id,
|
||||
permissions,
|
||||
nsfw,
|
||||
},
|
||||
Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
last_message_id,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
} => crate::Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon: icon.map(|file| file.into()),
|
||||
last_message_id,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
},
|
||||
Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
} => crate::Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon: icon.map(|file| file.into()),
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialChannel> for PartialChannel {
|
||||
fn from(value: crate::PartialChannel) -> Self {
|
||||
PartialChannel {
|
||||
@@ -215,6 +317,23 @@ impl From<crate::PartialChannel> for PartialChannel {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialChannel> for crate::PartialChannel {
|
||||
fn from(value: PartialChannel) -> crate::PartialChannel {
|
||||
crate::PartialChannel {
|
||||
name: value.name,
|
||||
owner: value.owner,
|
||||
description: value.description,
|
||||
icon: value.icon.map(|file| file.into()),
|
||||
nsfw: value.nsfw,
|
||||
active: value.active,
|
||||
permissions: value.permissions,
|
||||
role_permissions: value.role_permissions,
|
||||
default_permissions: value.default_permissions,
|
||||
last_message_id: value.last_message_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsChannel> for crate::FieldsChannel {
|
||||
fn from(value: FieldsChannel) -> Self {
|
||||
match value {
|
||||
@@ -257,6 +376,15 @@ impl From<crate::EmojiParent> for EmojiParent {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmojiParent> for crate::EmojiParent {
|
||||
fn from(value: EmojiParent) -> Self {
|
||||
match value {
|
||||
EmojiParent::Detached => crate::EmojiParent::Detached,
|
||||
EmojiParent::Server { id } => crate::EmojiParent::Server { id },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::File> for File {
|
||||
fn from(value: crate::File) -> Self {
|
||||
File {
|
||||
@@ -276,6 +404,25 @@ impl From<crate::File> for File {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for crate::File {
|
||||
fn from(value: File) -> crate::File {
|
||||
crate::File {
|
||||
id: value.id,
|
||||
tag: value.tag,
|
||||
filename: value.filename,
|
||||
metadata: value.metadata.into(),
|
||||
content_type: value.content_type,
|
||||
size: value.size,
|
||||
deleted: value.deleted,
|
||||
reported: value.reported,
|
||||
message_id: value.message_id,
|
||||
user_id: value.user_id,
|
||||
server_id: value.server_id,
|
||||
object_id: value.object_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Metadata> for Metadata {
|
||||
fn from(value: crate::Metadata) -> Self {
|
||||
match value {
|
||||
@@ -294,26 +441,48 @@ impl From<crate::Metadata> for Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Message> for Message {
|
||||
fn from(value: crate::Message) -> Self {
|
||||
impl From<Metadata> for crate::Metadata {
|
||||
fn from(value: Metadata) -> crate::Metadata {
|
||||
match value {
|
||||
Metadata::File => crate::Metadata::File,
|
||||
Metadata::Text => crate::Metadata::Text,
|
||||
Metadata::Image { width, height } => crate::Metadata::Image {
|
||||
width: width as isize,
|
||||
height: height as isize,
|
||||
},
|
||||
Metadata::Video { width, height } => crate::Metadata::Video {
|
||||
width: width as isize,
|
||||
height: height as isize,
|
||||
},
|
||||
Metadata::Audio => crate::Metadata::Audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Message {
|
||||
pub fn into_model(self, user: Option<User>, member: Option<Member>) -> Message {
|
||||
Message {
|
||||
id: value.id,
|
||||
nonce: value.nonce,
|
||||
channel: value.channel,
|
||||
author: value.author,
|
||||
webhook: value.webhook,
|
||||
content: value.content,
|
||||
system: value.system.map(|system| system.into()),
|
||||
attachments: value
|
||||
id: self.id,
|
||||
nonce: self.nonce,
|
||||
channel: self.channel,
|
||||
author: self.author,
|
||||
user,
|
||||
member,
|
||||
webhook: self.webhook,
|
||||
content: self.content,
|
||||
system: self.system.map(Into::into),
|
||||
attachments: self
|
||||
.attachments
|
||||
.map(|v| v.into_iter().map(|f| f.into()).collect()),
|
||||
edited: value.edited,
|
||||
embeds: value.embeds,
|
||||
mentions: value.mentions,
|
||||
replies: value.replies,
|
||||
reactions: value.reactions,
|
||||
interactions: value.interactions.into(),
|
||||
masquerade: value.masquerade.map(|masq| masq.into()),
|
||||
edited: self.edited,
|
||||
embeds: self.embeds,
|
||||
mentions: self.mentions,
|
||||
replies: self.replies,
|
||||
reactions: self.reactions,
|
||||
interactions: self.interactions.into(),
|
||||
masquerade: self.masquerade.map(Into::into),
|
||||
flags: self.flags.map(|flags| flags as u32).unwrap_or_default(),
|
||||
pinned: self.pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,9 +494,11 @@ impl From<crate::PartialMessage> for PartialMessage {
|
||||
nonce: value.nonce,
|
||||
channel: value.channel,
|
||||
author: value.author,
|
||||
user: None,
|
||||
member: None,
|
||||
webhook: value.webhook,
|
||||
content: value.content,
|
||||
system: value.system.map(|system| system.into()),
|
||||
system: value.system.map(Into::into),
|
||||
attachments: value
|
||||
.attachments
|
||||
.map(|v| v.into_iter().map(|f| f.into()).collect()),
|
||||
@@ -336,8 +507,10 @@ impl From<crate::PartialMessage> for PartialMessage {
|
||||
mentions: value.mentions,
|
||||
replies: value.replies,
|
||||
reactions: value.reactions,
|
||||
interactions: value.interactions.map(|interactions| interactions.into()),
|
||||
masquerade: value.masquerade.map(|masq| masq.into()),
|
||||
interactions: value.interactions.map(Into::into),
|
||||
masquerade: value.masquerade.map(Into::into),
|
||||
flags: value.flags.map(|flags| flags as u32),
|
||||
pinned: value.pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,6 +533,8 @@ impl From<crate::SystemMessage> for SystemMessage {
|
||||
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
|
||||
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
|
||||
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
|
||||
crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by },
|
||||
crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -367,12 +542,33 @@ impl From<crate::SystemMessage> for SystemMessage {
|
||||
impl From<crate::Interactions> for Interactions {
|
||||
fn from(value: crate::Interactions) -> Self {
|
||||
Interactions {
|
||||
reactions: value.reactions,
|
||||
reactions: value
|
||||
.reactions
|
||||
.map(|reactions| reactions.into_iter().collect()),
|
||||
restrict_reactions: value.restrict_reactions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Interactions> for crate::Interactions {
|
||||
fn from(value: Interactions) -> Self {
|
||||
crate::Interactions {
|
||||
reactions: value
|
||||
.reactions
|
||||
.map(|reactions| reactions.into_iter().collect()),
|
||||
restrict_reactions: value.restrict_reactions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::AppendMessage> for AppendMessage {
|
||||
fn from(value: crate::AppendMessage) -> Self {
|
||||
AppendMessage {
|
||||
embeds: value.embeds,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Masquerade> for Masquerade {
|
||||
fn from(value: crate::Masquerade) -> Self {
|
||||
Masquerade {
|
||||
@@ -383,6 +579,29 @@ impl From<crate::Masquerade> for Masquerade {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Masquerade> for crate::Masquerade {
|
||||
fn from(value: Masquerade) -> Self {
|
||||
crate::Masquerade {
|
||||
name: value.name,
|
||||
avatar: value.avatar,
|
||||
colour: value.colour,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Report> for Report {
|
||||
fn from(value: crate::Report) -> Self {
|
||||
Report {
|
||||
id: value.id,
|
||||
author_id: value.author_id,
|
||||
content: value.content,
|
||||
additional_context: value.additional_context,
|
||||
status: value.status,
|
||||
notes: value.notes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::ServerBan> for ServerBan {
|
||||
fn from(value: crate::ServerBan) -> Self {
|
||||
ServerBan {
|
||||
@@ -405,6 +624,19 @@ impl From<crate::Member> for Member {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Member> for crate::Member {
|
||||
fn from(value: Member) -> crate::Member {
|
||||
crate::Member {
|
||||
id: value.id.into(),
|
||||
joined_at: value.joined_at,
|
||||
nickname: value.nickname,
|
||||
avatar: value.avatar.map(|f| f.into()),
|
||||
roles: value.roles,
|
||||
timeout: value.timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialMember> for PartialMember {
|
||||
fn from(value: crate::PartialMember) -> Self {
|
||||
PartialMember {
|
||||
@@ -418,6 +650,19 @@ impl From<crate::PartialMember> for PartialMember {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialMember> for crate::PartialMember {
|
||||
fn from(value: PartialMember) -> crate::PartialMember {
|
||||
crate::PartialMember {
|
||||
id: value.id.map(|id| id.into()),
|
||||
joined_at: value.joined_at,
|
||||
nickname: value.nickname,
|
||||
avatar: value.avatar.map(|f| f.into()),
|
||||
roles: value.roles,
|
||||
timeout: value.timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::MemberCompositeKey> for MemberCompositeKey {
|
||||
fn from(value: crate::MemberCompositeKey) -> Self {
|
||||
MemberCompositeKey {
|
||||
@@ -427,6 +672,15 @@ impl From<crate::MemberCompositeKey> for MemberCompositeKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MemberCompositeKey> for crate::MemberCompositeKey {
|
||||
fn from(value: MemberCompositeKey) -> crate::MemberCompositeKey {
|
||||
crate::MemberCompositeKey {
|
||||
server: value.server,
|
||||
user: value.user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsMember> for FieldsMember {
|
||||
fn from(value: crate::FieldsMember) -> Self {
|
||||
match value {
|
||||
@@ -438,6 +692,17 @@ impl From<crate::FieldsMember> for FieldsMember {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsMember> for crate::FieldsMember {
|
||||
fn from(value: FieldsMember) -> crate::FieldsMember {
|
||||
match value {
|
||||
FieldsMember::Avatar => crate::FieldsMember::Avatar,
|
||||
FieldsMember::Nickname => crate::FieldsMember::Nickname,
|
||||
FieldsMember::Roles => crate::FieldsMember::Roles,
|
||||
FieldsMember::Timeout => crate::FieldsMember::Timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::RemovalIntention> for RemovalIntention {
|
||||
fn from(value: crate::RemovalIntention) -> Self {
|
||||
match value {
|
||||
@@ -476,6 +741,34 @@ impl From<crate::Server> for Server {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Server> for crate::Server {
|
||||
fn from(value: Server) -> crate::Server {
|
||||
crate::Server {
|
||||
id: value.id,
|
||||
owner: value.owner,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
channels: value.channels,
|
||||
categories: value
|
||||
.categories
|
||||
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
|
||||
system_messages: value.system_messages.map(|v| v.into()),
|
||||
roles: value
|
||||
.roles
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.into()))
|
||||
.collect(),
|
||||
default_permissions: value.default_permissions,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
banner: value.banner.map(|f| f.into()),
|
||||
flags: Some(value.flags as i32),
|
||||
nsfw: value.nsfw,
|
||||
analytics: value.analytics,
|
||||
discoverable: value.discoverable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialServer> for PartialServer {
|
||||
fn from(value: crate::PartialServer) -> Self {
|
||||
PartialServer {
|
||||
@@ -502,6 +795,32 @@ impl From<crate::PartialServer> for PartialServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialServer> for crate::PartialServer {
|
||||
fn from(value: PartialServer) -> crate::PartialServer {
|
||||
crate::PartialServer {
|
||||
id: value.id,
|
||||
owner: value.owner,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
channels: value.channels,
|
||||
categories: value
|
||||
.categories
|
||||
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
|
||||
system_messages: value.system_messages.map(|v| v.into()),
|
||||
roles: value
|
||||
.roles
|
||||
.map(|roles| roles.into_iter().map(|(k, v)| (k, v.into())).collect()),
|
||||
default_permissions: value.default_permissions,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
banner: value.banner.map(|f| f.into()),
|
||||
flags: value.flags.map(|v| v as i32),
|
||||
nsfw: value.nsfw,
|
||||
analytics: value.analytics,
|
||||
discoverable: value.discoverable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsServer> for FieldsServer {
|
||||
fn from(value: crate::FieldsServer) -> Self {
|
||||
match value {
|
||||
@@ -514,6 +833,18 @@ impl From<crate::FieldsServer> for FieldsServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsServer> for crate::FieldsServer {
|
||||
fn from(value: FieldsServer) -> crate::FieldsServer {
|
||||
match value {
|
||||
FieldsServer::Banner => crate::FieldsServer::Banner,
|
||||
FieldsServer::Categories => crate::FieldsServer::Categories,
|
||||
FieldsServer::Description => crate::FieldsServer::Description,
|
||||
FieldsServer::Icon => crate::FieldsServer::Icon,
|
||||
FieldsServer::SystemMessages => crate::FieldsServer::SystemMessages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Category> for Category {
|
||||
fn from(value: crate::Category) -> Self {
|
||||
Category {
|
||||
@@ -524,6 +855,16 @@ impl From<crate::Category> for Category {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Category> for crate::Category {
|
||||
fn from(value: Category) -> Self {
|
||||
crate::Category {
|
||||
id: value.id,
|
||||
title: value.title,
|
||||
channels: value.channels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::SystemMessageChannels> for SystemMessageChannels {
|
||||
fn from(value: crate::SystemMessageChannels) -> Self {
|
||||
SystemMessageChannels {
|
||||
@@ -535,6 +876,17 @@ impl From<crate::SystemMessageChannels> for SystemMessageChannels {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemMessageChannels> for crate::SystemMessageChannels {
|
||||
fn from(value: SystemMessageChannels) -> Self {
|
||||
crate::SystemMessageChannels {
|
||||
user_joined: value.user_joined,
|
||||
user_left: value.user_left,
|
||||
user_kicked: value.user_kicked,
|
||||
user_banned: value.user_banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Role> for Role {
|
||||
fn from(value: crate::Role) -> Self {
|
||||
Role {
|
||||
@@ -547,6 +899,18 @@ impl From<crate::Role> for Role {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for crate::Role {
|
||||
fn from(value: Role) -> crate::Role {
|
||||
crate::Role {
|
||||
name: value.name,
|
||||
permissions: value.permissions,
|
||||
colour: value.colour,
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialRole> for PartialRole {
|
||||
fn from(value: crate::PartialRole) -> Self {
|
||||
PartialRole {
|
||||
@@ -559,6 +923,18 @@ impl From<crate::PartialRole> for PartialRole {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialRole> for crate::PartialRole {
|
||||
fn from(value: PartialRole) -> crate::PartialRole {
|
||||
crate::PartialRole {
|
||||
name: value.name,
|
||||
permissions: value.permissions,
|
||||
colour: value.colour,
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsRole> for FieldsRole {
|
||||
fn from(value: crate::FieldsRole) -> Self {
|
||||
match value {
|
||||
@@ -567,26 +943,122 @@ impl From<crate::FieldsRole> for FieldsRole {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsRole> for crate::FieldsRole {
|
||||
fn from(value: FieldsRole) -> Self {
|
||||
match value {
|
||||
FieldsRole::Colour => crate::FieldsRole::Colour,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::User {
|
||||
pub async fn into<P>(self, perspective: P) -> User
|
||||
pub async fn into<'a, P>(self, db: &Database, perspective: P) -> User
|
||||
where
|
||||
P: Into<Option<crate::User>>,
|
||||
P: Into<Option<&'a crate::User>>,
|
||||
{
|
||||
let relationship = if let Some(perspective) = perspective.into() {
|
||||
perspective
|
||||
.relations
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.find(|relationship| relationship.id == self.id)
|
||||
.map(|relationship| relationship.status.into())
|
||||
.unwrap_or_default()
|
||||
let perspective = perspective.into();
|
||||
let (relationship, can_see_profile) = if self.bot.is_some() {
|
||||
(RelationshipStatus::None, true)
|
||||
} else if let Some(perspective) = perspective {
|
||||
let mut query = DatabasePermissionQuery::new(db, perspective).user(&self);
|
||||
|
||||
if perspective.id == self.id {
|
||||
(RelationshipStatus::User, true)
|
||||
} else {
|
||||
(
|
||||
perspective
|
||||
.relations
|
||||
.as_ref()
|
||||
.map(|relations| {
|
||||
relations
|
||||
.iter()
|
||||
.find(|relationship| relationship.id == self.id)
|
||||
.map(|relationship| relationship.status.clone().into())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
calculate_user_permissions(&mut query)
|
||||
.await
|
||||
.has_user_permission(UserPermission::ViewProfile),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RelationshipStatus::None
|
||||
(RelationshipStatus::None, false)
|
||||
};
|
||||
|
||||
// do permission stuff here
|
||||
// TODO: implement permissions =)
|
||||
let can_see_profile = false;
|
||||
User {
|
||||
username: self.username,
|
||||
discriminator: self.discriminator,
|
||||
display_name: self.display_name,
|
||||
avatar: self.avatar.map(|file| file.into()),
|
||||
relations: if let Some(crate::User { id, .. }) = perspective {
|
||||
if id == &self.id {
|
||||
self.relations
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|relation| relation.into())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: can_see_profile
|
||||
&& revolt_presence::is_online(&self.id).await
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: if can_see_profile {
|
||||
self.status.map(|status| status.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert user object into user model assuming mutual connection
|
||||
///
|
||||
/// Relations will never be included, i.e. when we process ourselves
|
||||
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
|
||||
where
|
||||
P: Into<Option<&'a crate::User>>,
|
||||
{
|
||||
let perspective = perspective.into();
|
||||
let (relationship, can_see_profile) = if self.bot.is_some() {
|
||||
(RelationshipStatus::None, true)
|
||||
} else if let Some(perspective) = perspective {
|
||||
if perspective.id == self.id {
|
||||
(RelationshipStatus::User, true)
|
||||
} else {
|
||||
let relationship = perspective
|
||||
.relations
|
||||
.as_ref()
|
||||
.map(|relations| {
|
||||
relations
|
||||
.iter()
|
||||
.find(|relationship| relationship.id == self.id)
|
||||
.map(|relationship| relationship.status.clone().into())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let can_see_profile = relationship != RelationshipStatus::BlockedOther;
|
||||
(relationship, can_see_profile)
|
||||
}
|
||||
} else {
|
||||
(RelationshipStatus::None, false)
|
||||
};
|
||||
|
||||
User {
|
||||
username: self.username,
|
||||
@@ -595,16 +1067,163 @@ impl crate::User {
|
||||
avatar: self.avatar.map(|file| file.into()),
|
||||
relations: vec![],
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
status: None,
|
||||
profile: None,
|
||||
online: can_see_profile
|
||||
&& is_online
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: if can_see_profile {
|
||||
self.status.map(|status| status.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && revolt_presence::is_online(&self.id).await,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert user object into user model without presence information
|
||||
pub fn into_known_static<'a>(self, is_online: bool) -> User {
|
||||
User {
|
||||
username: self.username,
|
||||
discriminator: self.discriminator,
|
||||
display_name: self.display_name,
|
||||
avatar: self.avatar.map(|file| file.into()),
|
||||
relations: vec![],
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: is_online
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: self.status.map(|status| status.into()),
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship: RelationshipStatus::None, // events client will populate this from cache
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn into_self(self, force_online: bool) -> User {
|
||||
User {
|
||||
username: self.username,
|
||||
discriminator: self.discriminator,
|
||||
display_name: self.display_name,
|
||||
avatar: self.avatar.map(|file| file.into()),
|
||||
relations: self
|
||||
.relations
|
||||
.map(|relationships| {
|
||||
relationships
|
||||
.into_iter()
|
||||
.map(|relationship| relationship.into())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: (force_online || revolt_presence::is_online(&self.id).await)
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: self.status.map(|status| status.into()),
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship: RelationshipStatus::User,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_author_for_system(&self) -> MessageAuthor {
|
||||
MessageAuthor::System {
|
||||
username: &self.username,
|
||||
avatar: self.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<User> for crate::User {
|
||||
fn from(value: User) -> crate::User {
|
||||
crate::User {
|
||||
id: value.id,
|
||||
username: value.username,
|
||||
discriminator: value.discriminator,
|
||||
display_name: value.display_name,
|
||||
avatar: value.avatar.map(Into::into),
|
||||
relations: None,
|
||||
badges: Some(value.badges as i32),
|
||||
status: value.status.map(Into::into),
|
||||
profile: None,
|
||||
flags: Some(value.flags as i32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialUser> for PartialUser {
|
||||
fn from(value: crate::PartialUser) -> Self {
|
||||
PartialUser {
|
||||
username: value.username,
|
||||
discriminator: value.discriminator,
|
||||
display_name: value.display_name,
|
||||
avatar: value.avatar.map(|file| file.into()),
|
||||
relations: value.relations.map(|relationships| {
|
||||
relationships
|
||||
.into_iter()
|
||||
.map(|relationship| relationship.into())
|
||||
.collect()
|
||||
}),
|
||||
badges: value.badges.map(|badges| badges as u32),
|
||||
status: value.status.map(|status| status.into()),
|
||||
flags: value.flags.map(|flags| flags as u32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(|bot| bot.into()),
|
||||
relationship: None,
|
||||
online: None,
|
||||
id: value.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsUser> for crate::FieldsUser {
|
||||
fn from(value: FieldsUser) -> Self {
|
||||
match value {
|
||||
FieldsUser::Avatar => crate::FieldsUser::Avatar,
|
||||
FieldsUser::ProfileBackground => crate::FieldsUser::ProfileBackground,
|
||||
FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent,
|
||||
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
|
||||
FieldsUser::StatusText => crate::FieldsUser::StatusText,
|
||||
FieldsUser::DisplayName => crate::FieldsUser::DisplayName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsUser> for FieldsUser {
|
||||
fn from(value: crate::FieldsUser) -> Self {
|
||||
match value {
|
||||
crate::FieldsUser::Avatar => FieldsUser::Avatar,
|
||||
crate::FieldsUser::ProfileBackground => FieldsUser::ProfileBackground,
|
||||
crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent,
|
||||
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
|
||||
crate::FieldsUser::StatusText => FieldsUser::StatusText,
|
||||
crate::FieldsUser::DisplayName => FieldsUser::DisplayName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::RelationshipStatus> for RelationshipStatus {
|
||||
@@ -642,6 +1261,18 @@ impl From<crate::Presence> for Presence {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Presence> for crate::Presence {
|
||||
fn from(value: Presence) -> crate::Presence {
|
||||
match value {
|
||||
Presence::Online => crate::Presence::Online,
|
||||
Presence::Idle => crate::Presence::Idle,
|
||||
Presence::Focus => crate::Presence::Focus,
|
||||
Presence::Busy => crate::Presence::Busy,
|
||||
Presence::Invisible => crate::Presence::Invisible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::UserStatus> for UserStatus {
|
||||
fn from(value: crate::UserStatus) -> Self {
|
||||
UserStatus {
|
||||
@@ -651,6 +1282,15 @@ impl From<crate::UserStatus> for UserStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserStatus> for crate::UserStatus {
|
||||
fn from(value: UserStatus) -> crate::UserStatus {
|
||||
crate::UserStatus {
|
||||
text: value.text,
|
||||
presence: value.presence.map(|presence| presence.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::UserProfile> for UserProfile {
|
||||
fn from(value: crate::UserProfile) -> Self {
|
||||
UserProfile {
|
||||
@@ -660,6 +1300,15 @@ impl From<crate::UserProfile> for UserProfile {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserProfile> for crate::UserProfile {
|
||||
fn from(value: UserProfile) -> crate::UserProfile {
|
||||
crate::UserProfile {
|
||||
content: value.content,
|
||||
background: value.background.map(|file| file.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::BotInformation> for BotInformation {
|
||||
fn from(value: crate::BotInformation) -> Self {
|
||||
BotInformation {
|
||||
@@ -667,3 +1316,26 @@ impl From<crate::BotInformation> for BotInformation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BotInformation> for crate::BotInformation {
|
||||
fn from(value: BotInformation) -> crate::BotInformation {
|
||||
crate::BotInformation {
|
||||
owner: value.owner_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsMessage> for FieldsMessage {
|
||||
fn from(value: crate::FieldsMessage) -> Self {
|
||||
match value {
|
||||
crate::FieldsMessage::Pinned => FieldsMessage::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<FieldsMessage> for crate::FieldsMessage {
|
||||
fn from(value: FieldsMessage) -> Self {
|
||||
match value {
|
||||
FieldsMessage::Pinned => crate::FieldsMessage::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-17
@@ -1,32 +1,34 @@
|
||||
use crate::{Error, Result};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use revolt_result::Error;
|
||||
|
||||
use async_std::sync::Mutex;
|
||||
use revolt_rocket_okapi::gen::OpenApiGenerator;
|
||||
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Validate, Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct IdempotencyKey {
|
||||
#[validate(length(min = 1, max = 64))]
|
||||
key: String,
|
||||
}
|
||||
|
||||
static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> = Lazy::new(|| Mutex::new(lru::LruCache::new(100)));
|
||||
static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
|
||||
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
|
||||
|
||||
impl IdempotencyKey {
|
||||
pub fn unchecked_from_string(key: String) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
|
||||
// Backwards compatibility.
|
||||
// Issue #109
|
||||
pub async fn consume_nonce(&mut self, v: Option<String>) -> Result<()> {
|
||||
if let Some(v) = v {
|
||||
let mut cache = TOKEN_CACHE.lock().await;
|
||||
if cache.get(&v).is_some() {
|
||||
return Err(Error::DuplicateNonce);
|
||||
return Err(create_error!(DuplicateNonce));
|
||||
}
|
||||
|
||||
cache.put(v.clone(), ());
|
||||
@@ -41,6 +43,17 @@ impl IdempotencyKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use revolt_rocket_okapi::{
|
||||
gen::OpenApiGenerator,
|
||||
request::{OpenApiFromRequest, RequestHeaderInput},
|
||||
revolt_okapi::openapi3::{Parameter, ParameterValue},
|
||||
};
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
@@ -70,6 +83,13 @@ impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use rocket::{
|
||||
http::Status,
|
||||
request::{FromRequest, Outcome},
|
||||
};
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for IdempotencyKey {
|
||||
type Error = Error;
|
||||
@@ -81,14 +101,19 @@ impl<'r> FromRequest<'r> for IdempotencyKey {
|
||||
.next()
|
||||
.map(|k| k.to_string())
|
||||
{
|
||||
let idempotency = IdempotencyKey { key };
|
||||
if let Err(error) = idempotency.validate() {
|
||||
return Outcome::Failure((Status::BadRequest, Error::FailedValidation { error }));
|
||||
if key.len() > 64 {
|
||||
return Outcome::Failure((
|
||||
Status::BadRequest,
|
||||
create_error!(FailedValidation {
|
||||
error: "idempotency key too long".to_string(),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
let idempotency = IdempotencyKey { key };
|
||||
let mut cache = TOKEN_CACHE.lock().await;
|
||||
if cache.get(&idempotency.key).is_some() {
|
||||
return Outcome::Failure((Status::Conflict, Error::DuplicateNonce));
|
||||
return Outcome::Failure((Status::Conflict, create_error!(DuplicateNonce)));
|
||||
}
|
||||
|
||||
cache.put(idempotency.key.clone(), ());
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user