forked from jmug/stoatchat
Compare commits
179 Commits
20230810-3
...
20240625-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
2
.config/nextest.toml
Normal file
2
.config/nextest.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "3s", terminate-after = 2 }
|
||||
25
.env.example
25
.env.example
@@ -1,5 +1,7 @@
|
||||
# MongoDB URI
|
||||
MONGODB=mongodb://localhost
|
||||
AUTUMN_MONGO_URI=mongodb://database
|
||||
REDIS_URI=redis://localhost/
|
||||
|
||||
# URL to where the Revolt app is publicly accessible
|
||||
REVOLT_APP_URL=http://local.revolt.chat:5000
|
||||
@@ -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
|
||||
|
||||
37
.github/workflows/book.yml
vendored
Normal file
37
.github/workflows/book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Build documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pages: write # To push to a GitHub Pages site
|
||||
id-token: write # To update the deployment status
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install latest mdbook
|
||||
run: |
|
||||
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
|
||||
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
mkdir mdbook
|
||||
curl -sSL $url | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Build Book
|
||||
run: |
|
||||
cd doc
|
||||
mdbook build
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "doc/book"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
36
.github/workflows/cla.yml
vendored
36
.github/workflows/cla.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened,closed,synchronize]
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.1.3-beta
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PAT }}
|
||||
with:
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-document: 'https://github.com/revoltchat/cla/blob/master/CLA.md' # e.g. a CLA or a DCO document
|
||||
# branch should not be protected
|
||||
branch: 'master'
|
||||
allowlist: insertish,bot*
|
||||
|
||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||
remote-organization-name: revoltchat
|
||||
remote-repository-name: cla
|
||||
create-file-commit-message: 'cla(create): creating file for storing CLA Signatures'
|
||||
signed-commit-message: 'cla(sign): $contributorName has signed the CLA in #$pullRequestNo'
|
||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
5
.github/workflows/docker.yaml
vendored
5
.github/workflows/docker.yaml
vendored
@@ -3,19 +3,20 @@ name: Docker Test & Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "main"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "main"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
57
.github/workflows/rust.yaml
vendored
57
.github/workflows/rust.yaml
vendored
@@ -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
|
||||
@@ -30,36 +43,38 @@ jobs:
|
||||
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
|
||||
|
||||
- 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"
|
||||
|
||||
4
.github/workflows/triage_pr.yml
vendored
4
.github/workflows/triage_pr.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
projectV2(number: 5) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
Rocket.toml
|
||||
Revolt.toml
|
||||
|
||||
target
|
||||
.data
|
||||
.env
|
||||
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy"
|
||||
}
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"nixEnvSelector.suggestion": false,
|
||||
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
|
||||
}
|
||||
|
||||
1785
Cargo.lock
generated
1785
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
||||
[workspace]
|
||||
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
|
||||
resolver = "2"
|
||||
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
|
||||
|
||||
[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" }
|
||||
|
||||
@@ -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/
|
||||
|
||||
28
Dockerfile.useCurrentArch
Normal file
28
Dockerfile.useCurrentArch
Normal file
@@ -0,0 +1,28 @@
|
||||
# Build Stage
|
||||
FROM rust:1.70.0-slim
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
# Install build requirements
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
make \
|
||||
pkg-config \
|
||||
libssl-dev
|
||||
COPY scripts/build-image-layer.sh /tmp/
|
||||
|
||||
# Build all dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
COPY crates ./crates
|
||||
RUN sh /tmp/build-image-layer.sh apps
|
||||
5
LICENSE
5
LICENSE
@@ -1,4 +1,7 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
With the exception of crates that specify their own LICENSE file,
|
||||
the following license applies to the source code of this project.
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
|
||||
156
README.md
156
README.md
@@ -2,34 +2,150 @@
|
||||
|
||||
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.
|
||||
|
||||
## 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 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=MONGOBD 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!**
|
||||
|
||||
19
clippy.toml
19
clippy.toml
@@ -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",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.6.6"
|
||||
version = "0.7.11"
|
||||
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.11", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_quark::{Error, Result};
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Enumeration of supported protocol formats
|
||||
@@ -37,16 +37,16 @@ impl ProtocolConfiguration {
|
||||
match self.format {
|
||||
ProtocolFormat::Json => {
|
||||
if let Message::Text(text) = msg {
|
||||
serde_json::from_str(text).map_err(|_| Error::InternalError)
|
||||
serde_json::from_str(text).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
ProtocolFormat::Msgpack => {
|
||||
if let Message::Binary(buf) = msg {
|
||||
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError)
|
||||
rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use revolt_quark::{Database, DatabaseInfo};
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
static DBCONN: OnceCell<Database> = OnceCell::new();
|
||||
|
||||
@@ -10,7 +10,9 @@ pub async fn connect() {
|
||||
.await
|
||||
.expect("Failed to connect to the database.");
|
||||
|
||||
DBCONN.set(database).expect("Setting `Database`");
|
||||
if DBCONN.set(database).is_err() {
|
||||
panic!("couldn't set database")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the current database.
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
get_relationship,
|
||||
models::{
|
||||
server_member::FieldsMember,
|
||||
user::{PartialUser, Presence, RelationshipStatus},
|
||||
Channel, Member, User,
|
||||
},
|
||||
perms, Database, Permission, Result,
|
||||
use revolt_database::{
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
|
||||
MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::{
|
||||
client::EventV1,
|
||||
state::{Cache, State},
|
||||
};
|
||||
use super::state::{Cache, State};
|
||||
|
||||
/// Cache Manager
|
||||
impl Cache {
|
||||
@@ -25,20 +19,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 +59,7 @@ impl Cache {
|
||||
/// Check whether we can subscribe to another user
|
||||
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
|
||||
if let Some(user) = self.users.get(&self.user_id) {
|
||||
match get_relationship(user, user_id) {
|
||||
match user.relationship_with(user_id) {
|
||||
RelationshipStatus::Friend
|
||||
| RelationshipStatus::Incoming
|
||||
| RelationshipStatus::Outgoing
|
||||
@@ -95,7 +91,8 @@ impl Cache {
|
||||
impl State {
|
||||
/// Generate a Ready packet for the current user
|
||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
||||
let mut user = self.clone_user();
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
@@ -141,7 +138,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 +150,14 @@ impl State {
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
let emojis = db
|
||||
.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
.await?;
|
||||
|
||||
// Copy data into local state cache.
|
||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
@@ -176,40 +171,43 @@ 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,
|
||||
servers: servers.into_iter().map(Into::into).collect(),
|
||||
channels: channels.into_iter().map(Into::into).collect(),
|
||||
members: members.into_iter().map(Into::into).collect(),
|
||||
emojis: emojis.into_iter().map(Into::into).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -243,11 +241,11 @@ impl State {
|
||||
let mut bulk_events = vec![];
|
||||
|
||||
for id in added_channels {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
for id in removed_channels {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
self.cache.channels.remove(&id);
|
||||
|
||||
bulk_events.push(EventV1::ChannelDelete { id });
|
||||
@@ -270,8 +268,8 @@ impl State {
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
bulk_events.push(EventV1::ChannelCreate(channel));
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
bulk_events.push(EventV1::ChannelCreate(channel.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +294,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 +341,8 @@ impl State {
|
||||
match event {
|
||||
EventV1::ChannelCreate(channel) => {
|
||||
let id = channel.id().to_string();
|
||||
self.insert_subscription(id.clone());
|
||||
self.cache.channels.insert(id, channel.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
self.cache.channels.insert(id, channel.clone().into());
|
||||
}
|
||||
EventV1::ChannelUpdate {
|
||||
id, data, clear, ..
|
||||
@@ -357,10 +355,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 +372,7 @@ impl State {
|
||||
if could_view != can_view {
|
||||
if can_view {
|
||||
queue_add = Some(id.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone().into());
|
||||
} else {
|
||||
queue_remove = Some(id.clone());
|
||||
*event = EventV1::ChannelDelete { id: id.clone() };
|
||||
@@ -383,17 +381,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
EventV1::ChannelDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
self.cache.channels.remove(id);
|
||||
}
|
||||
EventV1::ChannelGroupJoin { user, .. } => {
|
||||
self.insert_subscription(user.clone());
|
||||
self.insert_subscription(user.clone()).await;
|
||||
}
|
||||
EventV1::ChannelGroupLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
} else if !self.cache.can_subscribe_to_user(user) {
|
||||
self.remove_subscription(user);
|
||||
self.remove_subscription(user).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,16 +399,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 +430,10 @@ impl State {
|
||||
} => {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
for field in clear {
|
||||
server.remove(field);
|
||||
server.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
server.apply_options(data.clone());
|
||||
server.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.default_permissions.is_some() {
|
||||
@@ -435,11 +445,11 @@ impl State {
|
||||
}
|
||||
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 +457,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 +471,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 +492,10 @@ impl State {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
if let Some(role) = server.roles.get_mut(role_id) {
|
||||
for field in &clear.clone() {
|
||||
role.remove(field);
|
||||
role.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
role.apply_options(data.clone());
|
||||
role.apply_options(data.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,12 +531,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 +564,13 @@ impl State {
|
||||
|
||||
// Sub / unsub accordingly.
|
||||
if let Some(id) = queue_add {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
if let Some(id) = queue_remove {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
/// Publish helper wrapper
|
||||
pub async fn p(self, channel: String) {
|
||||
#[cfg(not(debug_assertions))]
|
||||
redis_kiss::p(channel, self).await;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("Publishing event to {channel}: {self:?}");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
redis_kiss::publish(channel, self).await.unwrap();
|
||||
}
|
||||
|
||||
/// Publish user event
|
||||
pub async fn p_user(self, id: String, db: &Database) {
|
||||
self.clone().p(id.clone()).await;
|
||||
|
||||
// ! FIXME: this should be captured by member list in the future
|
||||
// ! and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().p(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish private event
|
||||
pub async fn private(self, id: String) {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
}
|
||||
}
|
||||
2
crates/bonfire/src/events/mod.rs
Normal file
2
crates/bonfire/src/events/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod r#impl;
|
||||
pub mod state;
|
||||
@@ -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:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +1,523 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use revolt_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,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
if let Ok(mut config) = receiver.await {
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (write, mut read) = ws.split();
|
||||
let write = Mutex::new(write);
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
'outer: while let Ok(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
config.set_session_token(token);
|
||||
break 'outer;
|
||||
}
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (mut write, mut read) = ws.split();
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
while let Ok(Some(message)) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
write
|
||||
.send(config.encode(&create_error!(InvalidSession)))
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
|
||||
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
write.send(config.encode(&err)).await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user, session_id);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
if let Err(err) = write.send(config.encode(&EventV1::Authenticated)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
let ready_payload = match state.generate_ready_payload(db).await {
|
||||
Ok(ready_payload) => ready_payload,
|
||||
Err(err) => {
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = write.send(config.encode(&ready_payload)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
{
|
||||
// Setup channels and mutexes
|
||||
let write = Mutex::new(write);
|
||||
let subscribed = state.subscribed.clone();
|
||||
let active_servers = state.active_servers.clone();
|
||||
let (topic_signal_s, topic_signal_r) = async_channel::unbounded();
|
||||
|
||||
// TODO: this needs to be rewritten
|
||||
// Create channels through which the tasks can signal to each other they need to clean up
|
||||
let (kill_signal_1_s, kill_signal_1_r) = async_channel::bounded(1);
|
||||
let (kill_signal_2_s, kill_signal_2_r) = async_channel::bounded(1);
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener_with_kill_signal(
|
||||
db,
|
||||
&mut state,
|
||||
addr,
|
||||
&config,
|
||||
topic_signal_r,
|
||||
kill_signal_1_r,
|
||||
&write,
|
||||
kill_signal_2_s,
|
||||
);
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker_with_kill_signal(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id.clone(),
|
||||
&config,
|
||||
topic_signal_s,
|
||||
kill_signal_2_r,
|
||||
read,
|
||||
&write,
|
||||
kill_signal_1_s,
|
||||
);
|
||||
|
||||
join!(listener, worker);
|
||||
}
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
|
||||
// If this was the last session, notify other users that we just went offline.
|
||||
if last_session {
|
||||
state.broadcast_presence_change(false).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn listener_with_kill_signal(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
listener(
|
||||
db,
|
||||
state,
|
||||
addr,
|
||||
config,
|
||||
topic_signal_r,
|
||||
kill_signal_r,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
async fn listener(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
||||
let subscriber = match fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
{
|
||||
Ok(subscriber) => subscriber,
|
||||
Err(err) => {
|
||||
error!("Failed to build a subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = subscriber.init().await {
|
||||
error!("Failed to init subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
};
|
||||
|
||||
// Handle Redis connection dropping
|
||||
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
||||
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
|
||||
subscriber.on_error(move |err| {
|
||||
if let RedisErrorKind::Canceled = err.kind() {
|
||||
let clean_up_s = clean_up_s.clone();
|
||||
spawn(async move {
|
||||
clean_up_s.lock().await.send(()).await.ok();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
'out: loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state().await {
|
||||
SubscriptionStateChange::Reset => {
|
||||
if let Err(err) = subscriber.unsubscribe_all().await {
|
||||
error!("Unsubscribe all failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
|
||||
let subscribed = state.subscribed.read().await;
|
||||
for id in subscribed.iter() {
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
if let Some(token) = config.get_session_token().as_ref() {
|
||||
match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => {
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} has reset their subscriptions");
|
||||
}
|
||||
SubscriptionStateChange::Change { add, remove } => {
|
||||
for id in remove {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
if let Err(err) = subscriber.unsubscribe(id).await {
|
||||
error!("Unsubscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = 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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
crates/core/config/Cargo.toml
Normal file
34
crates/core/config/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.7.11"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Configuration"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
test = ["async-std"]
|
||||
default = ["test"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
dotenv = "0.15.0"
|
||||
config = "0.13.3"
|
||||
cached = "0.44.0"
|
||||
once_cell = "1.18.0"
|
||||
|
||||
# Serde
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
# Async
|
||||
futures-locks = "0.7.1"
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
|
||||
# Logging
|
||||
log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
# Sentry
|
||||
sentry = "0.31.5"
|
||||
9
crates/core/config/LICENSE
Normal file
9
crates/core/config/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
3
crates/core/config/Revolt.test.toml
Normal file
3
crates/core/config/Revolt.test.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost/"
|
||||
92
crates/core/config/Revolt.toml
Normal file
92
crates/core/config/Revolt.toml
Normal file
@@ -0,0 +1,92 @@
|
||||
[database]
|
||||
mongodb = "mongodb://database"
|
||||
redis = "redis://redis/"
|
||||
|
||||
[hosts]
|
||||
app = "http://local.revolt.chat"
|
||||
api = "http://local.revolt.chat/api"
|
||||
events = "ws://local.revolt.chat/ws"
|
||||
autumn = "http://local.revolt.chat/autumn"
|
||||
january = "http://local.revolt.chat/january"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
[api]
|
||||
|
||||
[api.registration]
|
||||
invite_only = false
|
||||
|
||||
[api.smtp]
|
||||
host = ""
|
||||
username = ""
|
||||
password = ""
|
||||
from_address = ""
|
||||
|
||||
[api.vapid]
|
||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
||||
|
||||
[api.fcm]
|
||||
api_key = ""
|
||||
|
||||
[api.security]
|
||||
authifier_shield_key = ""
|
||||
voso_legacy_token = ""
|
||||
trust_cloudflare = false
|
||||
|
||||
[api.security.captcha]
|
||||
hcaptcha_key = ""
|
||||
hcaptcha_sitekey = ""
|
||||
|
||||
[api.workers]
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[features]
|
||||
webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.global]
|
||||
group_size = 100
|
||||
message_embeds = 5
|
||||
message_replies = 5
|
||||
message_reactions = 20
|
||||
server_emoji = 100
|
||||
server_roles = 200
|
||||
server_channels = 200
|
||||
|
||||
new_user_days = 3
|
||||
|
||||
[features.limits.new_user]
|
||||
outgoing_friend_requests = 5
|
||||
|
||||
bots = 2
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20000000
|
||||
avatar_size = 4000000
|
||||
background_size = 6000000
|
||||
icon_size = 2500000
|
||||
banner_size = 6000000
|
||||
emoji_size = 500000
|
||||
|
||||
[features.limits.default]
|
||||
outgoing_friend_requests = 10
|
||||
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20000000
|
||||
avatar_size = 4000000
|
||||
background_size = 6000000
|
||||
icon_size = 2500000
|
||||
banner_size = 6000000
|
||||
emoji_size = 500000
|
||||
|
||||
[sentry]
|
||||
api = ""
|
||||
events = ""
|
||||
258
crates/core/config/src/lib.rs
Normal file
258
crates/core/config/src/lib.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
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;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::env;
|
||||
|
||||
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
RwLock::new({
|
||||
let mut builder = Config::builder().add_source(File::from_str(
|
||||
include_str!("../Revolt.toml"),
|
||||
FileFormat::Toml,
|
||||
));
|
||||
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
builder = builder.add_source(File::from_str(
|
||||
include_str!("../Revolt.test.toml"),
|
||||
FileFormat::Toml,
|
||||
));
|
||||
} else if std::path::Path::new("Revolt.toml").exists() {
|
||||
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
|
||||
}
|
||||
|
||||
builder.build().unwrap()
|
||||
})
|
||||
});
|
||||
|
||||
// https://gifbox.me/view/gT5mqxYKCZv-twilight-meow
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Database {
|
||||
pub mongodb: String,
|
||||
pub redis: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Hosts {
|
||||
pub app: String,
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
pub autumn: String,
|
||||
pub january: String,
|
||||
pub voso_legacy: String,
|
||||
pub voso_legacy_ws: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiRegistration {
|
||||
pub invite_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSmtp {
|
||||
pub host: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub from_address: String,
|
||||
pub reply_to: Option<String>,
|
||||
pub port: Option<i32>,
|
||||
pub use_tls: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiVapid {
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiFcm {
|
||||
pub api_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct 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 security: ApiSecurity,
|
||||
pub workers: ApiWorkers,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct GlobalLimits {
|
||||
pub group_size: usize,
|
||||
pub message_embeds: usize,
|
||||
pub message_replies: usize,
|
||||
pub message_reactions: usize,
|
||||
pub server_emoji: usize,
|
||||
pub server_roles: usize,
|
||||
pub server_channels: usize,
|
||||
|
||||
pub new_user_days: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimits {
|
||||
pub outgoing_friend_requests: usize,
|
||||
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
|
||||
pub attachment_size: usize,
|
||||
pub avatar_size: usize,
|
||||
pub background_size: usize,
|
||||
pub icon_size: usize,
|
||||
pub banner_size: usize,
|
||||
pub emoji_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimitsCollection {
|
||||
pub global: GlobalLimits,
|
||||
|
||||
pub new_user: FeaturesLimits,
|
||||
pub default: FeaturesLimits,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub roles: HashMap<String, FeaturesLimits>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Features {
|
||||
pub limits: FeaturesLimitsCollection,
|
||||
pub webhooks_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Sentry {
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Settings {
|
||||
pub database: Database,
|
||||
pub hosts: Hosts,
|
||||
pub api: Api,
|
||||
pub features: Features,
|
||||
pub sentry: Sentry,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn preflight_checks(&self) {
|
||||
if self.api.smtp.host.is_empty() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
|
||||
panic!("Running in production without email is not recommended, set REVOLT_UNSAFE_NO_EMAIL=1 to override.");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
log::warn!("No SMTP settings specified! Remember to configure email.");
|
||||
}
|
||||
|
||||
if self.api.security.captcha.hcaptcha_key.is_empty() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
|
||||
panic!("Running in production without CAPTCHA is not recommended, set REVOLT_UNSAFE_NO_CAPTCHA=1 to override.");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init() {
|
||||
println!(
|
||||
":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m",
|
||||
config().await
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn read() -> Config {
|
||||
CONFIG_BUILDER.read().await.clone()
|
||||
}
|
||||
|
||||
#[cached(time = 30)]
|
||||
pub async fn config() -> Settings {
|
||||
read().await.try_deserialize::<Settings>().unwrap()
|
||||
}
|
||||
|
||||
/// Configure logging and common Rust variables
|
||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
if std::env::var("ROCKET_ADDRESS").is_err() {
|
||||
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
|
||||
}
|
||||
|
||||
pretty_env_logger::init();
|
||||
log::info!("Starting {release}");
|
||||
|
||||
if dsn.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(sentry::init((
|
||||
dsn,
|
||||
sentry::ClientOptions {
|
||||
release: Some(release.into()),
|
||||
..Default::default()
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! configure {
|
||||
($application: ident) => {
|
||||
let config = $crate::config().await;
|
||||
let _sentry = $crate::setup_logging(
|
||||
concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")),
|
||||
config.sentry.$application,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::init;
|
||||
|
||||
#[async_std::test]
|
||||
async fn it_works() {
|
||||
init().await;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.6.6"
|
||||
version = "0.7.11"
|
||||
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.11", path = "../config" }
|
||||
revolt-result = { version = "0.7.11", path = "../result" }
|
||||
revolt-models = { version = "0.7.11", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.11", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.11", 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,12 @@ schemars = { version = "0.8.8", optional = true }
|
||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
|
||||
"json",
|
||||
], optional = true }
|
||||
revolt_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
||||
|
||||
# Notifications
|
||||
fcm = "0.9.2"
|
||||
web-push = "0.10.0"
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0" }
|
||||
authifier = { version = "1.0.8" }
|
||||
|
||||
28
crates/core/database/fixtures/group_with_members.json
Normal file
28
crates/core/database/fixtures/group_with_members.json
Normal file
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Member",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "Member",
|
||||
"discriminator": "0002"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "Group",
|
||||
"name": "My Group",
|
||||
"owner": "__ID:0__",
|
||||
"recipients": ["__ID:0__", "__ID:1__"]
|
||||
}
|
||||
]
|
||||
89
crates/core/database/fixtures/server_with_roles.json
Normal file
89
crates/core/database/fixtures/server_with_roles.json
Normal file
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Moderator",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "User",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "TextChannel",
|
||||
"name": "General",
|
||||
"server": "__ID:4__",
|
||||
"default_permissions": {
|
||||
"a": 0,
|
||||
"d": 1048576
|
||||
},
|
||||
"role_permissions": {
|
||||
"__ID:5__": {
|
||||
"a": 1048576,
|
||||
"d": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_object_type": "Server",
|
||||
"_id": "__ID:4__",
|
||||
"owner": "__ID:0__",
|
||||
"name": "Server",
|
||||
"channels": ["__ID:3__"],
|
||||
"roles": {
|
||||
"__ID:5__": {
|
||||
"name": "Moderator",
|
||||
"permissions": {
|
||||
"a": 545270208,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 3
|
||||
},
|
||||
"__ID:6__": {
|
||||
"name": "Owner",
|
||||
"permissions": {
|
||||
"a": 0,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 0
|
||||
}
|
||||
},
|
||||
"default_permissions": 4000322560
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:0__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": ["__ID:6__"],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:1__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": ["__ID:5__"],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:2__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"joined_at": 1698318340195
|
||||
}
|
||||
]
|
||||
@@ -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,10 @@ 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, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
|
||||
FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
use revolt_result::Error;
|
||||
|
||||
@@ -47,18 +48,19 @@ 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>,
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
emojis: Vec<Emoji>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping },
|
||||
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
@@ -103,12 +105,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
|
||||
@@ -145,7 +149,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 +158,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 +171,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 +246,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 +259,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,6 +81,7 @@ 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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = 27;
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -983,6 +984,93 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
// 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,69 @@ 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>
|
||||
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();
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Remove a field from this object
|
||||
pub fn remove_field(&mut self, field: &FieldsBot) {
|
||||
match field {
|
||||
@@ -78,6 +141,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 +163,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 +195,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 +207,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> {
|
||||
|
||||
@@ -20,7 +20,7 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
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 {
|
||||
@@ -41,6 +41,7 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
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?;
|
||||
}
|
||||
|
||||
@@ -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,7 +416,7 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to this channel's id
|
||||
/// Clone this channel's id
|
||||
pub fn id(&self) -> String {
|
||||
match self {
|
||||
Channel::DirectMessage { 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,61 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
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 +747,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 +763,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<()> {
|
||||
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(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, 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
|
||||
@@ -141,6 +156,7 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// Message Filter
|
||||
#[derive(Default)]
|
||||
pub struct MessageFilter {
|
||||
/// Parent channel ID
|
||||
pub channel: Option<String>,
|
||||
@@ -165,32 +181,636 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 restrict_reactions is not specified without reactions list
|
||||
if let Some(interactions) = &data.interactions {
|
||||
if interactions.restrict_reactions {
|
||||
let disallowed = if let Some(list) = &interactions.reactions {
|
||||
list.is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if disallowed {
|
||||
return Err(create_error!(InvalidProperty));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (author_id, webhook) = match &author {
|
||||
MessageAuthor::User(user) => (user.id.clone(), None),
|
||||
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
|
||||
MessageAuthor::System { .. } => ("00000000000000000000000000".to_string(), None),
|
||||
};
|
||||
|
||||
// Start constructing the message
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: channel.id(),
|
||||
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
|
||||
interactions: data
|
||||
.interactions
|
||||
.map(|interactions| interactions.into())
|
||||
.unwrap_or_default(),
|
||||
author: author_id,
|
||||
webhook: webhook.map(|w| w.into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Parse mentions in message.
|
||||
let mut mentions = HashSet::new();
|
||||
if allow_mentions {
|
||||
if let Some(content) = &data.content {
|
||||
for capture in RE_MENTION.captures_iter(content) {
|
||||
if let Some(mention) = capture.get(1) {
|
||||
mentions.insert(mention.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > config.features.limits.global.message_replies {
|
||||
return Err(create_error!(TooManyReplies {
|
||||
max: config.features.limits.global.message_replies,
|
||||
}));
|
||||
}
|
||||
|
||||
for ReplyIntent { id, mention } in entries {
|
||||
let message = db.fetch_message(&id).await?;
|
||||
|
||||
if mention && allow_mentions {
|
||||
mentions.insert(message.author.to_owned());
|
||||
}
|
||||
|
||||
replies.insert(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
if !mentions.is_empty() {
|
||||
message.mentions.replace(mentions.into_iter().collect());
|
||||
}
|
||||
|
||||
if !replies.is_empty() {
|
||||
message
|
||||
.replies
|
||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
||||
}
|
||||
|
||||
// Add attachments to message.
|
||||
let mut attachments = vec![];
|
||||
if data
|
||||
.attachments
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > limits.message_attachments)
|
||||
{
|
||||
return Err(create_error!(TooManyAttachments {
|
||||
max: limits.message_attachments,
|
||||
}));
|
||||
}
|
||||
|
||||
if data
|
||||
.embeds
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > config.features.limits.global.message_embeds)
|
||||
{
|
||||
return Err(create_error!(TooManyEmbeds {
|
||||
max: config.features.limits.global.message_embeds,
|
||||
}));
|
||||
}
|
||||
|
||||
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
|
||||
attachments.push(
|
||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
if !attachments.is_empty() {
|
||||
message.attachments.replace(attachments);
|
||||
}
|
||||
|
||||
// Process included embeds.
|
||||
for sendable_embed in data.embeds.unwrap_or_default() {
|
||||
message.attach_sendable_embed(db, sendable_embed).await?;
|
||||
}
|
||||
|
||||
// Set content
|
||||
message.content = data.content;
|
||||
|
||||
// Pass-through nonce value for clients
|
||||
message.nonce = Some(idempotency.into_key());
|
||||
|
||||
// Send the message
|
||||
message
|
||||
.send(db, author, user, member, &channel, generate_embeds)
|
||||
.await?;
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Send a message without any notifications
|
||||
pub async fn send_without_notifications(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
is_dm: bool,
|
||||
generate_embeds: bool,
|
||||
) -> Result<()> {
|
||||
db.insert_message(self).await?;
|
||||
|
||||
// Fan out events
|
||||
EventV1::Message(self.clone().into_model(user, member))
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Update last_message_id
|
||||
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
|
||||
|
||||
// Add mentions for affected users
|
||||
if let Some(mentions) = &self.mentions {
|
||||
for user in mentions {
|
||||
tasks::ack::queue(
|
||||
self.channel.to_string(),
|
||||
user.to_string(),
|
||||
AckEvent::AddMention {
|
||||
ids: vec![self.id.to_string()],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate embeds
|
||||
if generate_embeds {
|
||||
if let Some(content) = &self.content {
|
||||
tasks::process_embeds::queue(
|
||||
self.channel.to_string(),
|
||||
self.id.to_string(),
|
||||
content.clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send a message
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
author: MessageAuthor<'_>,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
channel: &Channel,
|
||||
generate_embeds: bool,
|
||||
) -> Result<()> {
|
||||
self.send_without_notifications(
|
||||
db,
|
||||
user,
|
||||
member,
|
||||
matches!(channel, Channel::DirectMessage { .. }),
|
||||
generate_embeds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Push out Web Push notifications
|
||||
crate::tasks::web_push::queue(
|
||||
{
|
||||
match channel {
|
||||
Channel::DirectMessage { recipients, .. }
|
||||
| Channel::Group { recipients, .. } => recipients.clone(),
|
||||
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
|
||||
_ => vec![],
|
||||
}
|
||||
},
|
||||
PushNotification::from(
|
||||
self.clone().into_model(None, None),
|
||||
Some(author),
|
||||
&channel.id(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create text embed from sendable embed
|
||||
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
|
||||
embed.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
let media = if let Some(id) = embed.media {
|
||||
Some(
|
||||
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Embed::Text(Text {
|
||||
icon_url: embed.icon_url,
|
||||
url: embed.url,
|
||||
title: embed.title,
|
||||
description: embed.description,
|
||||
media: media.map(|m| m.into()),
|
||||
colour: embed.colour,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Update message data
|
||||
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
|
||||
self.apply_options(partial.clone());
|
||||
db.update_message(&self.id, &partial).await?;
|
||||
|
||||
EventV1::MessageUpdate {
|
||||
id: self.id.clone(),
|
||||
channel: self.channel.clone(),
|
||||
data: partial.into(),
|
||||
}
|
||||
.p(self.channel.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to fetch many messages with users
|
||||
pub async fn fetch_with_users(
|
||||
db: &Database,
|
||||
query: MessageQuery,
|
||||
perspective: &User,
|
||||
include_users: Option<bool>,
|
||||
server_id: Option<String>,
|
||||
) -> Result<BulkMessageResponse> {
|
||||
let messages: Vec<v0::Message> = db
|
||||
.fetch_messages(query)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|msg| msg.into_model(None, None))
|
||||
.collect();
|
||||
|
||||
if let Some(true) = include_users {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.flat_map(|m| {
|
||||
let mut users = vec![m.author.clone()];
|
||||
if let Some(system) = &m.system {
|
||||
match system {
|
||||
v0::SystemMessage::ChannelDescriptionChanged { by } => {
|
||||
users.push(by.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelIconChanged { by } => users.push(by.clone()),
|
||||
v0::SystemMessage::ChannelOwnershipChanged { from, to, .. } => {
|
||||
users.push(from.clone());
|
||||
users.push(to.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelRenamed { by, .. } => users.push(by.clone()),
|
||||
v0::SystemMessage::UserAdded { by, id, .. }
|
||||
| v0::SystemMessage::UserRemove { by, id, .. } => {
|
||||
users.push(by.clone());
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::UserBanned { id, .. }
|
||||
| v0::SystemMessage::UserKicked { id, .. }
|
||||
| v0::SystemMessage::UserJoined { id, .. }
|
||||
| v0::SystemMessage::UserLeft { id, .. } => {
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::Text { .. } => {}
|
||||
}
|
||||
}
|
||||
users
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
let users = User::fetch_many_ids_as_mutuals(db, perspective, &user_ids).await?;
|
||||
|
||||
Ok(BulkMessageResponse::MessagesAndUsers {
|
||||
messages,
|
||||
users,
|
||||
members: if let Some(server_id) = server_id {
|
||||
Some(
|
||||
db.fetch_members(&server_id, &user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
} else {
|
||||
Ok(BulkMessageResponse::JustMessages(messages))
|
||||
}
|
||||
}
|
||||
|
||||
/// Append content to message
|
||||
pub async fn append(
|
||||
db: &Database,
|
||||
id: String,
|
||||
channel: String,
|
||||
append: AppendMessage,
|
||||
) -> Result<()> {
|
||||
db.append_message(&id, &append).await?;
|
||||
|
||||
EventV1::MessageAppend {
|
||||
id,
|
||||
channel: channel.to_string(),
|
||||
append: append.into(),
|
||||
}
|
||||
.p(channel)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert sendable embed to text embed and attach to message
|
||||
pub async fn attach_sendable_embed(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
embed: v0::SendableEmbed,
|
||||
) -> Result<()> {
|
||||
let media: Option<v0::File> = if let Some(id) = embed.media {
|
||||
Some(
|
||||
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
|
||||
.await?
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let embed = v0::Embed::Text(v0::Text {
|
||||
icon_url: embed.icon_url,
|
||||
url: embed.url,
|
||||
title: embed.title,
|
||||
description: embed.description,
|
||||
media,
|
||||
colour: embed.colour,
|
||||
});
|
||||
|
||||
if let Some(embeds) = &mut self.embeds {
|
||||
embeds.push(embed);
|
||||
} else {
|
||||
self.embeds = Some(vec![embed]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a reaction to a message
|
||||
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
|
||||
// Check how many reactions are already on the message
|
||||
let config = config().await;
|
||||
if self.reactions.len() >= config.features.limits.global.message_reactions
|
||||
&& !self.reactions.contains_key(emoji)
|
||||
{
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Check if the emoji is whitelisted
|
||||
if !self.interactions.can_use(emoji) {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Check if the emoji is usable by us
|
||||
if !Emoji::can_use(db, emoji).await? {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Send reaction event
|
||||
EventV1::MessageReact {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
user_id: user.id.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Add emoji
|
||||
db.add_reaction(&self.id, emoji, &user.id).await
|
||||
}
|
||||
|
||||
/// Validate the sum of content of a message is under threshold
|
||||
pub fn validate_sum(
|
||||
content: &Option<String>,
|
||||
embeds: &[SendableEmbed],
|
||||
max_length: usize,
|
||||
) -> Result<()> {
|
||||
let mut running_total = 0;
|
||||
if let Some(content) = content {
|
||||
running_total += content.len();
|
||||
}
|
||||
|
||||
for embed in embeds {
|
||||
if let Some(desc) = &embed.description {
|
||||
running_total += desc.len();
|
||||
}
|
||||
}
|
||||
|
||||
if running_total <= max_length {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(PayloadTooLarge))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a message
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
let file_ids: Vec<String> = self
|
||||
.attachments
|
||||
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
if !file_ids.is_empty() {
|
||||
db.mark_attachments_as_deleted(&file_ids).await?;
|
||||
}
|
||||
|
||||
db.delete_message(&self.id).await?;
|
||||
|
||||
EventV1::MessageDelete {
|
||||
id: self.id,
|
||||
channel: self.channel.clone(),
|
||||
}
|
||||
.p(self.channel)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bulk delete messages
|
||||
pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec<String>) -> Result<()> {
|
||||
let valid_ids = db
|
||||
.fetch_messages_by_id(&ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|msg| msg.channel == channel)
|
||||
.map(|msg| msg.id)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
db.delete_messages(channel, &valid_ids).await?;
|
||||
EventV1::BulkMessageDelete {
|
||||
channel: channel.to_string(),
|
||||
ids: valid_ids,
|
||||
}
|
||||
.p(channel.to_string())
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
|
||||
// Check if it actually exists
|
||||
let empty = if let Some(users) = self.reactions.get(emoji) {
|
||||
if !users.contains(user) {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
users.len() == 1
|
||||
} else {
|
||||
return Err(create_error!(NotFound));
|
||||
};
|
||||
|
||||
// Send reaction event
|
||||
EventV1::MessageUnreact {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
user_id: user.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
if empty {
|
||||
// If empty, remove the reaction entirely
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
} else {
|
||||
// Otherwise only remove that one reaction
|
||||
db.remove_reaction(&self.id, emoji, user).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
|
||||
// Send reaction event
|
||||
EventV1::MessageRemoveReaction {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Write to database
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemMessage {
|
||||
pub fn into_message(self, channel: String) -> Message {
|
||||
Message {
|
||||
id: Ulid::new().to_string(),
|
||||
channel,
|
||||
author: "00000000000000000000000000".to_string(),
|
||||
system: Some(self),
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactions {
|
||||
/// Validate interactions info is correct
|
||||
/* pub async fn validate(
|
||||
&self,
|
||||
db: &Database,
|
||||
permissions: &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 {
|
||||
|
||||
@@ -2,8 +2,8 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
|
||||
|
||||
// mod mongodb;
|
||||
// mod reference;
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractMessages: Sync + Send {
|
||||
@@ -16,6 +16,9 @@ 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<()>;
|
||||
|
||||
@@ -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,231 @@
|
||||
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, 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
|
||||
};
|
||||
|
||||
// 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) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
||||
let mut query = doc! {};
|
||||
|
||||
if let Some(embeds) = &append.embeds {
|
||||
if !embeds.is_empty() {
|
||||
query.insert(
|
||||
"$push",
|
||||
doc! {
|
||||
"embeds": {
|
||||
"$each": to_bson(embeds)
|
||||
.map_err(|_| create_database_error!("to_bson", "embeds"))?
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if query.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &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 +234,118 @@ 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 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,278 @@
|
||||
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, 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;
|
||||
}
|
||||
}
|
||||
|
||||
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) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message_data) = messages.get_mut(id) {
|
||||
message_data.apply_options(message.to_owned());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message_data) = messages.get_mut(id) {
|
||||
if let Some(embeds) = &append.embeds {
|
||||
if !embeds.is_empty() {
|
||||
if let Some(embeds_data) = &mut message_data.embeds {
|
||||
embeds_data.extend(embeds.clone());
|
||||
} else {
|
||||
message_data.embeds = Some(embeds.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new reaction to a message
|
||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message) = messages.get_mut(id) {
|
||||
if let Some(users) = message.reactions.get_mut(emoji) {
|
||||
users.insert(user.to_string());
|
||||
} else {
|
||||
message
|
||||
.reactions
|
||||
.insert(emoji.to_string(), IndexSet::from([user.to_string()]));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message) = messages.get_mut(id) {
|
||||
if let Some(users) = message.reactions.get_mut(emoji) {
|
||||
users.remove(&user.to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove reaction from a message
|
||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message) = messages.get_mut(id) {
|
||||
message.reactions.remove(emoji);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a message from the database by its id
|
||||
async fn delete_message(&self, id: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if messages.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete messages from a channel by their ids and corresponding channel id
|
||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
|
||||
self.messages
|
||||
.lock()
|
||||
.await
|
||||
.retain(|id, message| message.channel != channel && !ids.contains(id));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
5
crates/core/database/src/models/safety_reports/mod.rs
Normal file
5
crates/core/database/src/models/safety_reports/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
22
crates/core/database/src/models/safety_reports/model.rs
Normal file
22
crates/core/database/src/models/safety_reports/model.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use revolt_models::v0::{ReportStatus, ReportedContent};
|
||||
|
||||
auto_derived!(
|
||||
/// User-generated platform moderation report
|
||||
pub struct Report {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Id of the user creating this report
|
||||
pub author_id: String,
|
||||
/// Reported content
|
||||
pub content: ReportedContent,
|
||||
/// Additional report context
|
||||
pub additional_context: String,
|
||||
/// Status of the report
|
||||
#[serde(flatten)]
|
||||
pub status: ReportStatus,
|
||||
/// Additional notes included on the report
|
||||
#[serde(default)]
|
||||
pub notes: String,
|
||||
}
|
||||
);
|
||||
12
crates/core/database/src/models/safety_reports/ops.rs
Normal file
12
crates/core/database/src/models/safety_reports/ops.rs
Normal file
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/safety_snapshots/mod.rs
Normal file
5
crates/core/database/src/models/safety_snapshots/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -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 = [
|
||||
@@ -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<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(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,47 @@ 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(),
|
||||
}
|
||||
.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()).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,81 @@ 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")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"parent.id": &server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_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,15 @@ auto_derived_partial!(
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Optional fields on user object
|
||||
pub enum FieldsUser {
|
||||
Avatar,
|
||||
StatusText,
|
||||
StatusPresence,
|
||||
ProfileContent,
|
||||
ProfileBackground,
|
||||
}
|
||||
|
||||
/// User's relationship with another user (or themselves)
|
||||
pub enum RelationshipStatus {
|
||||
None,
|
||||
@@ -82,20 +96,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 +123,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 +621,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 +639,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 +649,7 @@ impl User {
|
||||
}
|
||||
FieldsUser::ProfileContent => {
|
||||
if let Some(x) = self.profile.as_mut() {
|
||||
x.content = String::new();
|
||||
x.content = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::ProfileBackground => {
|
||||
@@ -203,17 +680,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>>;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
31
crates/core/database/src/models/users/schema.rs
Normal file
31
crates/core/database/src/models/users/schema.rs
Normal file
@@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ pub async fn worker(db: Database) {
|
||||
let (user, channel) = key;
|
||||
|
||||
if let Err(err) = match &event {
|
||||
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
|
||||
AckEvent::AckMessage { id } => db.acknowledge_message(channel, user, id).await,
|
||||
AckEvent::AddMention { ids } => {
|
||||
db.add_mention_to_unread(channel, user, ids).await
|
||||
@@ -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
|
||||
@@ -13,12 +13,12 @@ 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) {
|
||||
for _ in 0..WORKER_COUNT {
|
||||
task::spawn(ack::worker(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()));
|
||||
}
|
||||
}
|
||||
|
||||
170
crates/core/database/src/tasks/process_embeds.rs
Normal file
170
crates/core/database/src/tasks/process_embeds.rs
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
162
crates/core/database/src/tasks/web_push.rs
Normal file
162
crates/core/database/src/tasks/web_push.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use authifier::Database;
|
||||
use base64::{
|
||||
engine::{self},
|
||||
Engine as _,
|
||||
};
|
||||
use deadqueue::limited::Queue;
|
||||
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,
|
||||
};
|
||||
|
||||
/// 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.api_key.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(fcm::Client::new())
|
||||
};
|
||||
|
||||
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: _,
|
||||
} = &task.payload;
|
||||
|
||||
let mut notification = fcm::NotificationBuilder::new();
|
||||
notification.title(author);
|
||||
notification.icon(icon);
|
||||
notification.body(body);
|
||||
notification.tag(tag);
|
||||
// TODO: expand support for fields
|
||||
let notification = notification.finalize();
|
||||
|
||||
let mut message_builder =
|
||||
fcm::MessageBuilder::new(&config.api.fcm.api_key, &sub.auth);
|
||||
message_builder.notification(notification);
|
||||
|
||||
if let Err(err) = client.send(message_builder.finalize()).await {
|
||||
error!("Failed to send FCM notification! {:?}", err);
|
||||
} else {
|
||||
info!("Sent FCM notification to {:?}.", session.id);
|
||||
}
|
||||
} else {
|
||||
info!("No FCM token was specified!");
|
||||
}
|
||||
} 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,46 @@ 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(|system| system.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(|masq| masq.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -325,6 +492,8 @@ 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()),
|
||||
@@ -367,12 +536,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 +573,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 +618,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 +644,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 +666,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 +686,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 +735,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 +789,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 +827,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 +849,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 +870,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 +893,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 +917,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,27 +937,164 @@ 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
|
||||
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,
|
||||
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
|
||||
&& 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,
|
||||
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,
|
||||
@@ -595,16 +1102,130 @@ 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: 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,
|
||||
online: can_see_profile && revolt_presence::is_online(&self.id).await,
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::RelationshipStatus> for RelationshipStatus {
|
||||
@@ -642,6 +1263,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 +1284,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 +1302,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 +1318,11 @@ impl From<crate::BotInformation> for BotInformation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BotInformation> for crate::BotInformation {
|
||||
fn from(value: BotInformation) -> crate::BotInformation {
|
||||
crate::BotInformation {
|
||||
owner: value.owner_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
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 {
|
||||
// Backwards compatibility.
|
||||
@@ -26,7 +24,7 @@ impl IdempotencyKey {
|
||||
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 +39,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 +79,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 +97,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(), ());
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod bridge;
|
||||
pub mod idempotency;
|
||||
pub mod permissions;
|
||||
pub mod reference;
|
||||
pub mod test_fixtures;
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use revolt_permissions::{
|
||||
calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus,
|
||||
calculate_user_permissions, ChannelType, Override, PermissionQuery, PermissionValue,
|
||||
RelationshipStatus, DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
||||
};
|
||||
|
||||
use crate::{Database, User};
|
||||
use crate::{Channel, Database, Member, Server, User};
|
||||
|
||||
/// Permissions calculator
|
||||
pub struct PermissionCalculator<'a> {
|
||||
#[derive(Clone)]
|
||||
pub struct DatabasePermissionQuery<'a> {
|
||||
#[allow(dead_code)]
|
||||
database: &'a Database,
|
||||
|
||||
perspective: &'a User,
|
||||
user: Option<Cow<'a, User>>,
|
||||
// pub channel: Cow<'a, Channel>,
|
||||
// pub server: Cow<'a, Server>,
|
||||
// pub member: Cow<'a, Member>,
|
||||
channel: Option<Cow<'a, Channel>>,
|
||||
server: Option<Cow<'a, Server>>,
|
||||
member: Option<Cow<'a, Member>>,
|
||||
|
||||
// flag_known_relationship: Option<&'a RelationshipStatus>,
|
||||
cached_user_permission: Option<u32>,
|
||||
cached_user_permission: Option<PermissionValue>,
|
||||
cached_mutual_connection: Option<bool>,
|
||||
cached_permission: Option<u64>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PermissionQuery for PermissionCalculator<'_> {
|
||||
impl PermissionQuery for DatabasePermissionQuery<'_> {
|
||||
// * For calculating user permission
|
||||
|
||||
/// Is our perspective user privileged?
|
||||
@@ -48,6 +51,10 @@ impl PermissionQuery for PermissionCalculator<'_> {
|
||||
/// Get the relationship with have with the currently selected user
|
||||
async fn user_relationship(&mut self) -> RelationshipStatus {
|
||||
if let Some(other_user) = &self.user {
|
||||
if self.perspective.id == other_user.id {
|
||||
return RelationshipStatus::User;
|
||||
}
|
||||
|
||||
if let Some(relations) = &self.perspective.relations {
|
||||
for entry in relations {
|
||||
if entry.id == other_user.id {
|
||||
@@ -81,93 +88,308 @@ impl PermissionQuery for PermissionCalculator<'_> {
|
||||
|
||||
/// Do we have a mutual connection with the currently selected user?
|
||||
async fn have_mutual_connection(&mut self) -> bool {
|
||||
// TODO: User::has_mutual_connection
|
||||
false
|
||||
if let Some(value) = self.cached_mutual_connection {
|
||||
value
|
||||
} else if let Some(user) = &self.user {
|
||||
let value = self
|
||||
.perspective
|
||||
.has_mutual_connection(self.database, &user.id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
self.cached_mutual_connection = Some(value);
|
||||
matches!(value, true)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// * For calculating server permission
|
||||
|
||||
/// Is our perspective user the server's owner?
|
||||
async fn are_we_server_owner(&mut self) -> bool {
|
||||
todo!()
|
||||
if let Some(server) = &self.server {
|
||||
server.owner == self.perspective.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Is our perspective user a member of the server?
|
||||
async fn are_we_a_member(&mut self) -> bool {
|
||||
todo!()
|
||||
if let Some(server) = &self.server {
|
||||
if self.member.is_some() {
|
||||
true
|
||||
} else if let Ok(member) = self
|
||||
.database
|
||||
.fetch_member(&server.id, &self.perspective.id)
|
||||
.await
|
||||
{
|
||||
self.member = Some(Cow::Owned(member));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get default server permission
|
||||
async fn get_default_server_permissions(&mut self) -> u64 {
|
||||
todo!()
|
||||
if let Some(server) = &self.server {
|
||||
server.default_permissions as u64
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ordered role overrides (from lowest to highest) for this member in this server
|
||||
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
|
||||
todo!()
|
||||
if let Some(server) = &self.server {
|
||||
let member_roles = self
|
||||
.member
|
||||
.as_ref()
|
||||
.map(|member| member.roles.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut roles = server
|
||||
.roles
|
||||
.iter()
|
||||
.filter(|(id, _)| member_roles.contains(id))
|
||||
.map(|(_, role)| {
|
||||
let v: Override = role.permissions.into();
|
||||
(role.rank, v)
|
||||
})
|
||||
.collect::<Vec<(i64, Override)>>();
|
||||
|
||||
roles.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
roles.into_iter().map(|(_, v)| v).collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
/// Is our perspective user timed out on this server?
|
||||
async fn are_we_timed_out(&mut self) -> bool {
|
||||
todo!()
|
||||
if let Some(member) = &self.member {
|
||||
member.in_timeout()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// * For calculating channel permission
|
||||
|
||||
/// Get the type of the channel
|
||||
async fn get_channel_type(&mut self) -> ChannelType {
|
||||
todo!()
|
||||
if let Some(channel) = &self.channel {
|
||||
match channel {
|
||||
Cow::Borrowed(Channel::DirectMessage { .. })
|
||||
| Cow::Owned(Channel::DirectMessage { .. }) => ChannelType::DirectMessage,
|
||||
Cow::Borrowed(Channel::Group { .. }) | Cow::Owned(Channel::Group { .. }) => {
|
||||
ChannelType::Group
|
||||
}
|
||||
Cow::Borrowed(Channel::SavedMessages { .. })
|
||||
| Cow::Owned(Channel::SavedMessages { .. }) => ChannelType::SavedMessages,
|
||||
Cow::Borrowed(Channel::TextChannel { .. })
|
||||
| Cow::Owned(Channel::TextChannel { .. })
|
||||
| Cow::Borrowed(Channel::VoiceChannel { .. })
|
||||
| Cow::Owned(Channel::VoiceChannel { .. }) => ChannelType::ServerChannel,
|
||||
}
|
||||
} else {
|
||||
ChannelType::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default channel permissions
|
||||
/// Group channel defaults should be mapped to an allow-only override
|
||||
async fn get_default_channel_permissions(&mut self) -> Override {
|
||||
todo!()
|
||||
if let Some(channel) = &self.channel {
|
||||
match channel {
|
||||
Cow::Borrowed(Channel::Group { permissions, .. })
|
||||
| Cow::Owned(Channel::Group { permissions, .. }) => Override {
|
||||
allow: permissions.unwrap_or(*DEFAULT_PERMISSION_DIRECT_MESSAGE as i64) as u64,
|
||||
deny: 0,
|
||||
},
|
||||
Cow::Borrowed(Channel::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
})
|
||||
| Cow::Owned(Channel::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
})
|
||||
| Cow::Borrowed(Channel::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
})
|
||||
| Cow::Owned(Channel::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}) => default_permissions.unwrap_or_default().into(),
|
||||
_ => Default::default(),
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ordered role overrides (from lowest to highest) for this member in this channel
|
||||
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
|
||||
todo!()
|
||||
if let Some(channel) = &self.channel {
|
||||
match channel {
|
||||
Cow::Borrowed(Channel::TextChannel {
|
||||
role_permissions, ..
|
||||
})
|
||||
| Cow::Owned(Channel::TextChannel {
|
||||
role_permissions, ..
|
||||
})
|
||||
| Cow::Borrowed(Channel::VoiceChannel {
|
||||
role_permissions, ..
|
||||
})
|
||||
| Cow::Owned(Channel::VoiceChannel {
|
||||
role_permissions, ..
|
||||
}) => {
|
||||
if let Some(server) = &self.server {
|
||||
let member_roles = self
|
||||
.member
|
||||
.as_ref()
|
||||
.map(|member| member.roles.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut roles = role_permissions
|
||||
.iter()
|
||||
.filter(|(id, _)| member_roles.contains(id))
|
||||
.filter_map(|(id, permission)| {
|
||||
server.roles.get(id).map(|role| {
|
||||
let v: Override = (*permission).into();
|
||||
(role.rank, v)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<(i64, Override)>>();
|
||||
|
||||
roles.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
roles.into_iter().map(|(_, v)| v).collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
/// Do we own this group or saved messages channel if it is one of those?
|
||||
async fn do_we_own_the_channel(&mut self) -> bool {
|
||||
todo!()
|
||||
if let Some(channel) = &self.channel {
|
||||
match channel {
|
||||
Cow::Borrowed(Channel::Group { owner, .. })
|
||||
| Cow::Owned(Channel::Group { owner, .. }) => owner == &self.perspective.id,
|
||||
Cow::Borrowed(Channel::SavedMessages { user, .. })
|
||||
| Cow::Owned(Channel::SavedMessages { user, .. }) => user == &self.perspective.id,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Are we a recipient of this channel?
|
||||
async fn are_we_part_of_the_channel(&mut self) -> bool {
|
||||
todo!()
|
||||
if let Some(channel) = &self.channel {
|
||||
match channel {
|
||||
Cow::Borrowed(Channel::DirectMessage { recipients, .. })
|
||||
| Cow::Owned(Channel::DirectMessage { recipients, .. })
|
||||
| Cow::Borrowed(Channel::Group { recipients, .. })
|
||||
| Cow::Owned(Channel::Group { recipients, .. }) => {
|
||||
recipients.contains(&self.perspective.id)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current user as the recipient of this channel
|
||||
/// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths)
|
||||
async fn set_recipient_as_user(&mut self) {
|
||||
todo!()
|
||||
if let Some(channel) = &self.channel {
|
||||
match channel {
|
||||
Cow::Borrowed(Channel::DirectMessage { recipients, .. })
|
||||
| Cow::Owned(Channel::DirectMessage { recipients, .. }) => {
|
||||
let recipient_id = recipients
|
||||
.iter()
|
||||
.find(|recipient| recipient != &&self.perspective.id)
|
||||
.expect("Missing recipient for DM");
|
||||
|
||||
if let Ok(user) = self.database.fetch_user(recipient_id).await {
|
||||
self.user.replace(Cow::Owned(user));
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the current server as the server owning this channel
|
||||
/// (this will only ever be called for server channels, use unimplemented!() for other code paths)
|
||||
async fn set_server_from_channel(&mut self) {
|
||||
todo!()
|
||||
if let Some(channel) = &self.channel {
|
||||
match channel {
|
||||
Cow::Borrowed(Channel::TextChannel { server, .. })
|
||||
| Cow::Owned(Channel::TextChannel { server, .. })
|
||||
| Cow::Borrowed(Channel::VoiceChannel { server, .. })
|
||||
| Cow::Owned(Channel::VoiceChannel { server, .. }) => {
|
||||
if let Some(known_server) =
|
||||
// I'm not sure why I can't just pattern match both at once here?
|
||||
// It throws some weird error and the provided fix doesn't work :/
|
||||
if let Some(Cow::Borrowed(known_server)) = self.server {
|
||||
Some(known_server)
|
||||
} else if let Some(Cow::Owned(ref known_server)) = self.server {
|
||||
Some(known_server)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
{
|
||||
if server == &known_server.id {
|
||||
// Already cached, return early.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(server) = self.database.fetch_server(server).await {
|
||||
self.server.replace(Cow::Owned(server));
|
||||
}
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PermissionCalculator<'a> {
|
||||
impl<'a> DatabasePermissionQuery<'a> {
|
||||
/// Create a new permission calculator
|
||||
pub fn new(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> {
|
||||
PermissionCalculator {
|
||||
pub fn new(database: &'a Database, perspective: &'a User) -> DatabasePermissionQuery<'a> {
|
||||
DatabasePermissionQuery {
|
||||
database,
|
||||
perspective,
|
||||
user: None,
|
||||
channel: None,
|
||||
server: None,
|
||||
member: None,
|
||||
|
||||
cached_mutual_connection: None,
|
||||
cached_user_permission: None,
|
||||
cached_permission: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the user permission value
|
||||
pub async fn calc_user(mut self) -> PermissionCalculator<'a> {
|
||||
pub async fn calc_user(mut self) -> DatabasePermissionQuery<'a> {
|
||||
if self.cached_user_permission.is_some() {
|
||||
return self;
|
||||
}
|
||||
@@ -176,14 +398,14 @@ impl<'a> PermissionCalculator<'a> {
|
||||
panic!("Expected `PermissionCalculator.user to exist.");
|
||||
}
|
||||
|
||||
PermissionCalculator {
|
||||
DatabasePermissionQuery {
|
||||
cached_user_permission: Some(calculate_user_permissions(&mut self).await),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the permission value
|
||||
pub async fn calc(self) -> PermissionCalculator<'a> {
|
||||
pub async fn calc(self) -> DatabasePermissionQuery<'a> {
|
||||
if self.cached_permission.is_some() {
|
||||
return self;
|
||||
}
|
||||
@@ -192,15 +414,66 @@ impl<'a> PermissionCalculator<'a> {
|
||||
}
|
||||
|
||||
/// Use user
|
||||
pub fn user(self, user: Cow<'a, User>) -> PermissionCalculator {
|
||||
PermissionCalculator {
|
||||
user: Some(user),
|
||||
pub fn user(self, user: &'a User) -> DatabasePermissionQuery {
|
||||
DatabasePermissionQuery {
|
||||
user: Some(Cow::Borrowed(user)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Use channel
|
||||
pub fn channel(self, channel: &'a Channel) -> DatabasePermissionQuery {
|
||||
DatabasePermissionQuery {
|
||||
channel: Some(Cow::Borrowed(channel)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Use server
|
||||
pub fn server(self, server: &'a Server) -> DatabasePermissionQuery {
|
||||
DatabasePermissionQuery {
|
||||
server: Some(Cow::Borrowed(server)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Use member
|
||||
pub fn member(self, member: &'a Member) -> DatabasePermissionQuery {
|
||||
DatabasePermissionQuery {
|
||||
member: Some(Cow::Borrowed(member)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the underlying user
|
||||
pub fn user_ref(&self) -> &Option<Cow<User>> {
|
||||
&self.user
|
||||
}
|
||||
|
||||
/// Access the underlying server
|
||||
pub fn channel_ref(&self) -> &Option<Cow<Channel>> {
|
||||
&self.channel
|
||||
}
|
||||
|
||||
/// Access the underlying server
|
||||
pub fn server_ref(&self) -> &Option<Cow<Server>> {
|
||||
&self.server
|
||||
}
|
||||
|
||||
/// Access the underlying member
|
||||
pub fn member_ref(&self) -> &Option<Cow<Member>> {
|
||||
&self.member
|
||||
}
|
||||
|
||||
/// Get the known member's current ranking
|
||||
pub fn get_member_rank(&self) -> Option<i64> {
|
||||
self.member
|
||||
.as_ref()
|
||||
.map(|member| member.get_ranking(self.server.as_ref().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Short-hand for creating a permission calculator
|
||||
pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> {
|
||||
PermissionCalculator::new(database, perspective)
|
||||
pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> DatabasePermissionQuery<'a> {
|
||||
DatabasePermissionQuery::new(database, perspective)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use revolt_result::Result;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use rocket::request::FromParam;
|
||||
@@ -7,7 +9,9 @@ use schemars::{
|
||||
JsonSchema,
|
||||
};
|
||||
|
||||
use crate::{Bot, Database, Webhook};
|
||||
use crate::{
|
||||
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
|
||||
};
|
||||
|
||||
/// Reference to some object in the database
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -22,11 +26,79 @@ impl Reference {
|
||||
Reference { id }
|
||||
}
|
||||
|
||||
/// Fetch ban from Ref
|
||||
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
|
||||
db.fetch_ban(server, &self.id).await
|
||||
}
|
||||
|
||||
/// Fetch bot from Ref
|
||||
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
|
||||
db.fetch_bot(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch emoji from Ref
|
||||
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
|
||||
db.fetch_emoji(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch channel from Ref
|
||||
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
|
||||
db.fetch_channel(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch invite from Ref or create invite to server if discoverable
|
||||
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
||||
if ulid::Ulid::from_str(&self.id).is_ok() {
|
||||
let server = self.as_server(db).await?;
|
||||
if !server.discoverable {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
Ok(Invite::Server {
|
||||
code: self.id.to_string(),
|
||||
server: server.id,
|
||||
creator: server.owner,
|
||||
channel: server
|
||||
.channels
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(create_error!(NotFound))?,
|
||||
})
|
||||
} else {
|
||||
db.fetch_invite(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch message from Ref
|
||||
pub async fn as_message(&self, db: &Database) -> Result<Message> {
|
||||
db.fetch_message(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch message from Ref and validate channel
|
||||
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
|
||||
let msg = db.fetch_message(&self.id).await?;
|
||||
if msg.channel != channel {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Fetch member from Ref
|
||||
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
|
||||
db.fetch_member(server, &self.id).await
|
||||
}
|
||||
|
||||
/// Fetch server from Ref
|
||||
pub async fn as_server(&self, db: &Database) -> Result<Server> {
|
||||
db.fetch_server(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch user from Ref
|
||||
pub async fn as_user(&self, db: &Database) -> Result<User> {
|
||||
db.fetch_user(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch webhook from Ref
|
||||
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
|
||||
db.fetch_webhook(&self.id).await
|
||||
|
||||
103
crates/core/database/src/util/test_fixtures.rs
Normal file
103
crates/core/database/src/util/test_fixtures.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
use serde_json::from_str;
|
||||
|
||||
use crate::{Channel, Database, Member, Server, User};
|
||||
|
||||
static RE_ID: Lazy<Regex> = Lazy::new(|| Regex::new("__ID:(\\d+)__").unwrap());
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "_object_type")]
|
||||
enum LoadedFixture {
|
||||
User(User),
|
||||
Channel(Channel),
|
||||
Server(Server),
|
||||
ServerMember(Member),
|
||||
}
|
||||
|
||||
pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String> {
|
||||
let mut ids = HashMap::<String, String>::new();
|
||||
let input = RE_ID.replace_all(input, |cap: &Captures| {
|
||||
let d = cap.get(1).unwrap().as_str();
|
||||
|
||||
if !ids.contains_key(d) {
|
||||
ids.insert(d.to_string(), ulid::Ulid::new().to_string());
|
||||
}
|
||||
|
||||
ids.get(d).unwrap().clone()
|
||||
});
|
||||
|
||||
// Deserialise the fixtures
|
||||
let items: Vec<LoadedFixture> = from_str(&input).expect("Failed to deserialise fixture");
|
||||
|
||||
// Load all of the items within
|
||||
for item in items {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
match item {
|
||||
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
|
||||
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
|
||||
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
|
||||
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
// Return IDs for ease of use
|
||||
ids
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait FetchFixture {
|
||||
async fn user(&self, db: &Database, d: usize) -> User;
|
||||
async fn channel(&self, db: &Database, d: usize) -> Channel;
|
||||
async fn server(&self, db: &Database, d: usize) -> Server;
|
||||
async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FetchFixture for HashMap<String, String> {
|
||||
async fn user(&self, db: &Database, d: usize) -> User {
|
||||
db.fetch_user(self.get(&d.to_string()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn channel(&self, db: &Database, d: usize) -> Channel {
|
||||
db.fetch_channel(self.get(&d.to_string()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn server(&self, db: &Database, d: usize) -> Server {
|
||||
db.fetch_server(self.get(&d.to_string()).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member {
|
||||
db.fetch_member(
|
||||
self.get(&d_server.to_string()).unwrap(),
|
||||
self.get(&d_user.to_string()).unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fixture {
|
||||
( $database:expr, $name:expr, $( $variable:ident $type:ident $id: expr )+ ) => {
|
||||
use $crate::util::test_fixtures::FetchFixture;
|
||||
|
||||
let fixtures = $crate::util::test_fixtures::load_fixture(
|
||||
&$database,
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/", $name, ".json")),
|
||||
)
|
||||
.await;
|
||||
|
||||
$(
|
||||
let $variable = fixtures.$type(&$database, $id).await;
|
||||
)+
|
||||
};
|
||||
}
|
||||
@@ -1,33 +1,42 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.6.6"
|
||||
version = "0.7.11"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: API Models"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde", "revolt-permissions/serde"]
|
||||
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
|
||||
schemas = ["dep:schemars", "revolt-permissions/schemas"]
|
||||
validator = ["dep:validator"]
|
||||
rocket = ["dep:rocket"]
|
||||
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
|
||||
|
||||
default = ["serde", "partials"]
|
||||
default = ["serde", "partials", "rocket"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-permissions = { version = "0.6.6", path = "../permissions" }
|
||||
revolt-config = { version = "0.7.11", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.11", path = "../permissions" }
|
||||
|
||||
# Utility
|
||||
regex = "1"
|
||||
indexmap = "1.9.3"
|
||||
once_cell = "1.17.1"
|
||||
|
||||
# Rocket
|
||||
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
|
||||
|
||||
# Serialisation
|
||||
indexmap = "1.9.3"
|
||||
revolt_optional_struct = { version = "0.2.0", optional = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
|
||||
|
||||
# Spec Generation
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] }
|
||||
|
||||
# Validation
|
||||
validator = { version = "0.16.0", optional = true, features = ["derive"] }
|
||||
|
||||
9
crates/core/models/LICENSE
Normal file
9
crates/core/models/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -30,7 +30,16 @@ macro_rules! auto_derived_partial {
|
||||
#[derive(
|
||||
OptionalStruct, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
#[optional_derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[optional_derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
Default
|
||||
)]
|
||||
#[optional_name = $name]
|
||||
#[opt_skip_serializing_none]
|
||||
#[opt_some_priority]
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::User;
|
||||
|
||||
auto_derived!(
|
||||
/// Bot
|
||||
#[derive(Default)]
|
||||
pub struct Bot {
|
||||
/// Bot Id
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
@@ -55,6 +56,12 @@ auto_derived!(
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
/// Optional fields on bot object
|
||||
pub enum FieldsBot {
|
||||
Token,
|
||||
InteractionsURL,
|
||||
}
|
||||
|
||||
/// Flags that may be attributed to a bot
|
||||
#[repr(u32)]
|
||||
pub enum BotFlags {
|
||||
@@ -71,10 +78,16 @@ auto_derived!(
|
||||
/// Bot Username
|
||||
pub username: String,
|
||||
/// Profile Avatar
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "String::is_empty", default)
|
||||
)]
|
||||
pub avatar: String,
|
||||
/// Profile Description
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "String::is_empty", default)
|
||||
)]
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
@@ -85,4 +98,68 @@ auto_derived!(
|
||||
/// User object
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
/// Bot Details
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct DataCreateBot {
|
||||
/// Bot username
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")
|
||||
)]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// New Bot Details
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct DataEditBot {
|
||||
/// Bot username
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")
|
||||
)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
/// Whether the bot can be added by anyone
|
||||
pub public: Option<bool>,
|
||||
/// Whether analytics should be gathered for this bot
|
||||
///
|
||||
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
|
||||
pub analytics: Option<bool>,
|
||||
/// Interactions URL
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
|
||||
pub interactions_url: Option<String>,
|
||||
/// Fields to remove from bot object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsBot>>,
|
||||
}
|
||||
|
||||
/// Where we are inviting a bot to
|
||||
#[serde(untagged)]
|
||||
pub enum InviteBotDestination {
|
||||
/// Invite to a server
|
||||
Server {
|
||||
/// Server Id
|
||||
server: String,
|
||||
},
|
||||
/// Invite to a group
|
||||
Group {
|
||||
/// Group Id
|
||||
group: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Owned Bots Response
|
||||
///
|
||||
/// Both lists are sorted by their IDs.
|
||||
///
|
||||
/// TODO: user should be in bot object
|
||||
pub struct OwnedBotsResponse {
|
||||
/// Bot objects
|
||||
pub bots: Vec<Bot>,
|
||||
/// User objects
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::{Channel, File, Server, User};
|
||||
|
||||
auto_derived!(
|
||||
/// Invite
|
||||
#[serde(tag = "type")]
|
||||
pub enum Invite {
|
||||
/// Invite to a specific server channel
|
||||
Server {
|
||||
@@ -24,4 +27,77 @@ auto_derived!(
|
||||
channel: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Public invite response
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum InviteResponse {
|
||||
/// Server channel invite
|
||||
Server {
|
||||
/// Invite code
|
||||
code: String,
|
||||
/// Id of the server
|
||||
server_id: String,
|
||||
/// Name of the server
|
||||
server_name: String,
|
||||
/// Attachment for server icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_icon: Option<File>,
|
||||
/// Attachment for server banner
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_banner: Option<File>,
|
||||
/// Enum of server flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_flags: Option<i32>,
|
||||
/// Id of server channel
|
||||
channel_id: String,
|
||||
/// Name of server channel
|
||||
channel_name: String,
|
||||
/// Description of server channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_description: Option<String>,
|
||||
/// Name of user who created the invite
|
||||
user_name: String,
|
||||
/// Avatar of the user who created the invite
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_avatar: Option<File>,
|
||||
/// Number of members in this server
|
||||
member_count: i64,
|
||||
},
|
||||
/// Group channel invite
|
||||
Group {
|
||||
/// Invite code
|
||||
code: String,
|
||||
/// Id of group channel
|
||||
channel_id: String,
|
||||
/// Name of group channel
|
||||
channel_name: String,
|
||||
/// Description of group channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_description: Option<String>,
|
||||
/// Name of user who created the invite
|
||||
user_name: String,
|
||||
/// Avatar of the user who created the invite
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_avatar: Option<File>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Invite join response
|
||||
#[serde(tag = "type")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum InviteJoinResponse {
|
||||
Server {
|
||||
/// Channels in the server
|
||||
channels: Vec<Channel>,
|
||||
/// Server we are joining
|
||||
server: Server,
|
||||
},
|
||||
Group {
|
||||
/// Group channel we are joining
|
||||
channel: Channel,
|
||||
/// Members of this group
|
||||
users: Vec<User>,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
use super::File;
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -69,13 +72,23 @@ auto_derived!(
|
||||
pub channel_id: String,
|
||||
|
||||
/// The permissions for the webhook
|
||||
pub permissions: u64
|
||||
pub permissions: u64,
|
||||
}
|
||||
|
||||
/// Optional fields on webhook object
|
||||
pub enum FieldsWebhook {
|
||||
Avatar,
|
||||
}
|
||||
|
||||
/// Information for the webhook
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct CreateWebhookBody {
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
pub name: String,
|
||||
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
pub avatar: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
impl From<Webhook> for MessageWebhook {
|
||||
@@ -94,7 +107,7 @@ impl From<Webhook> for ResponseWebhook {
|
||||
name: value.name,
|
||||
avatar: value.avatar.map(|file| file.id),
|
||||
channel_id: value.channel_id,
|
||||
permissions: value.permissions
|
||||
permissions: value.permissions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use super::File;
|
||||
|
||||
use revolt_permissions::OverrideField;
|
||||
use std::collections::HashMap;
|
||||
use revolt_permissions::{Override, OverrideField};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::FromForm;
|
||||
|
||||
auto_derived!(
|
||||
/// Channel
|
||||
#[serde(tag = "channel_type")]
|
||||
pub enum Channel {
|
||||
/// Personal "Saved Notes" channel which allows users to save messages
|
||||
SavedMessages {
|
||||
@@ -205,4 +209,101 @@ auto_derived!(
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
pub remove: Option<Vec<FieldsChannel>>,
|
||||
}
|
||||
|
||||
/// Create new group
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct DataCreateGroup {
|
||||
/// Group name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: String,
|
||||
/// Group description
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
|
||||
pub description: Option<String>,
|
||||
/// Group icon
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub icon: Option<String>,
|
||||
/// Array of user IDs to add to the group
|
||||
///
|
||||
/// Must be friends with these users.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 49)))]
|
||||
#[serde(default)]
|
||||
pub users: HashSet<String>,
|
||||
/// Whether this group is age-restricted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
/// Server Channel Type
|
||||
#[derive(Default)]
|
||||
pub enum LegacyServerChannelType {
|
||||
/// Text Channel
|
||||
#[default]
|
||||
Text,
|
||||
/// Voice Channel
|
||||
Voice,
|
||||
}
|
||||
|
||||
/// Create new server channel
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct DataCreateServerChannel {
|
||||
/// Channel type
|
||||
#[serde(rename = "type", default = "LegacyServerChannelType::default")]
|
||||
pub channel_type: LegacyServerChannelType,
|
||||
/// Channel name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: String,
|
||||
/// Channel description
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
|
||||
pub description: Option<String>,
|
||||
/// Whether this channel is age restricted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
/// New default permissions
|
||||
#[serde(untagged)]
|
||||
pub enum DataDefaultChannelPermissions {
|
||||
Value {
|
||||
/// Permission values to set for members in a `Group`
|
||||
permissions: u64,
|
||||
},
|
||||
Field {
|
||||
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
|
||||
permissions: Override,
|
||||
},
|
||||
}
|
||||
|
||||
/// New role permissions
|
||||
pub struct DataSetRolePermissions {
|
||||
/// Allow / deny values to set for this role
|
||||
pub permissions: Override,
|
||||
}
|
||||
|
||||
/// Options when deleting a channel
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsChannelDelete {
|
||||
/// Whether to not send a leave message
|
||||
pub leave_silently: Option<bool>,
|
||||
}
|
||||
|
||||
/// Voice server token response
|
||||
pub struct LegacyCreateVoiceUserResponse {
|
||||
/// Token for authenticating with the voice server
|
||||
token: String,
|
||||
}
|
||||
);
|
||||
|
||||
impl Channel {
|
||||
/// Get a reference to 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
/// Regex for valid emoji names
|
||||
///
|
||||
/// Alphanumeric and underscores
|
||||
pub static RE_EMOJI: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap());
|
||||
|
||||
auto_derived!(
|
||||
/// Emoji
|
||||
pub struct Emoji {
|
||||
@@ -30,4 +41,17 @@ auto_derived!(
|
||||
Server { id: String },
|
||||
Detached,
|
||||
}
|
||||
|
||||
/// Create a new emoji
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataCreateEmoji {
|
||||
/// Server name
|
||||
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
|
||||
pub name: String,
|
||||
/// Parent information
|
||||
pub parent: EmojiParent,
|
||||
/// Whether the emoji is mature
|
||||
#[serde(default)]
|
||||
pub nsfw: bool,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use revolt_config::config;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::{FromForm, FromFormField};
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
use super::{Embed, File, MessageWebhook};
|
||||
use super::{Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR};
|
||||
|
||||
pub static RE_MENTION: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Message
|
||||
@@ -16,6 +31,12 @@ auto_derived_partial!(
|
||||
pub channel: String,
|
||||
/// Id of the user or webhook that sent this message
|
||||
pub author: String,
|
||||
/// The user that sent this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user: Option<User>,
|
||||
/// The member that sent this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub member: Option<Member>,
|
||||
/// The webhook that sent this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub webhook: Option<MessageWebhook>,
|
||||
@@ -54,6 +75,24 @@ auto_derived_partial!(
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Bulk Message Response
|
||||
#[serde(untagged)]
|
||||
pub enum BulkMessageResponse {
|
||||
JustMessages(
|
||||
/// List of messages
|
||||
Vec<Message>,
|
||||
),
|
||||
MessagesAndUsers {
|
||||
/// List of messages
|
||||
messages: Vec<Message>,
|
||||
/// List of users
|
||||
users: Vec<User>,
|
||||
/// List of members
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
members: Option<Vec<Member>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// System Event
|
||||
#[serde(tag = "type")]
|
||||
pub enum SystemMessage {
|
||||
@@ -82,17 +121,21 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// Name and / or avatar override information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct Masquerade {
|
||||
/// Replace the display name shown on this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
pub name: Option<String>,
|
||||
/// Replace the avatar shown on this message (URL to image file)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[validate(length(min = 1, max = 256))]
|
||||
pub avatar: Option<String>,
|
||||
/// Replace the display role colour shown on this message
|
||||
///
|
||||
/// Must have `ManageRole` permission to use
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
|
||||
pub colour: Option<String>,
|
||||
}
|
||||
|
||||
@@ -120,6 +163,7 @@ auto_derived!(
|
||||
///
|
||||
/// Sort used for retrieving messages
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "rocket", derive(FromFormField))]
|
||||
pub enum MessageSort {
|
||||
/// Sort by the most relevant messages
|
||||
#[default]
|
||||
@@ -129,11 +173,278 @@ auto_derived!(
|
||||
/// Sort by the oldest messages first
|
||||
Oldest,
|
||||
}
|
||||
|
||||
/// Push Notification
|
||||
pub struct PushNotification {
|
||||
/// Known author name
|
||||
pub author: String,
|
||||
/// URL to author avatar
|
||||
pub icon: String,
|
||||
/// URL to first matching attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image: Option<String>,
|
||||
/// Message content or system message information
|
||||
pub body: String,
|
||||
/// Unique tag, usually the channel ID
|
||||
pub tag: String,
|
||||
/// Timestamp at which this notification was created
|
||||
pub timestamp: u64,
|
||||
/// URL to open when clicking notification
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// Representation of a text embed before it is sent.
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct SendableEmbed {
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub icon_url: Option<String>,
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
|
||||
pub url: Option<String>,
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
|
||||
pub title: Option<String>,
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
|
||||
pub description: Option<String>,
|
||||
pub media: Option<String>,
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
|
||||
)]
|
||||
pub colour: Option<String>,
|
||||
}
|
||||
|
||||
/// What this message should reply to and how
|
||||
pub struct ReplyIntent {
|
||||
/// Message Id
|
||||
pub id: String,
|
||||
/// Whether this reply should mention the message's author
|
||||
pub mention: bool,
|
||||
}
|
||||
|
||||
/// Message to send
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataMessageSend {
|
||||
/// Unique token to prevent duplicate message sending
|
||||
///
|
||||
/// **This is deprecated and replaced by `Idempotency-Key`!**
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
|
||||
pub nonce: Option<String>,
|
||||
|
||||
/// Message content to send
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
|
||||
pub content: Option<String>,
|
||||
/// Attachments to include in message
|
||||
pub attachments: Option<Vec<String>>,
|
||||
/// Messages to reply to
|
||||
pub replies: Option<Vec<ReplyIntent>>,
|
||||
/// Embeds to include in message
|
||||
///
|
||||
/// Text embed content contributes to the content length cap
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub embeds: Option<Vec<SendableEmbed>>,
|
||||
/// Masquerade to apply to this message
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub masquerade: Option<Masquerade>,
|
||||
/// Information about how this message should be interacted with
|
||||
pub interactions: Option<Interactions>,
|
||||
}
|
||||
|
||||
/// Options for querying messages
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsQueryMessages {
|
||||
/// Maximum number of messages to fetch
|
||||
///
|
||||
/// For fetching nearby messages, this is \`(limit + 1)\`.
|
||||
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
||||
pub limit: Option<i64>,
|
||||
/// Message id before which messages should be fetched
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub before: Option<String>,
|
||||
/// Message id after which messages should be fetched
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub after: Option<String>,
|
||||
/// Message sort direction
|
||||
pub sort: Option<MessageSort>,
|
||||
/// Message id to search around
|
||||
///
|
||||
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
|
||||
/// It will also take half of limit rounded as the limits to each side.
|
||||
/// It also fetches the message ID specified.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub nearby: Option<String>,
|
||||
/// Whether to include user (and member, if server channel) objects
|
||||
pub include_users: Option<bool>,
|
||||
}
|
||||
|
||||
/// Options for searching for messages
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataMessageSearch {
|
||||
/// Full-text search query
|
||||
///
|
||||
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
|
||||
pub query: String,
|
||||
|
||||
/// Maximum number of messages to fetch
|
||||
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
||||
pub limit: Option<i64>,
|
||||
/// Message id before which messages should be fetched
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub before: Option<String>,
|
||||
/// Message id after which messages should be fetched
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub after: Option<String>,
|
||||
/// Message sort direction
|
||||
///
|
||||
/// By default, it will be sorted by latest.
|
||||
#[cfg_attr(feature = "serde", serde(default = "MessageSort::default"))]
|
||||
pub sort: MessageSort,
|
||||
/// Whether to include user (and member, if server channel) objects
|
||||
pub include_users: Option<bool>,
|
||||
}
|
||||
|
||||
/// Changes to make to message
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditMessage {
|
||||
/// New message content
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
|
||||
pub content: Option<String>,
|
||||
/// Embeds to include in the message
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 10)))]
|
||||
pub embeds: Option<Vec<SendableEmbed>>,
|
||||
}
|
||||
|
||||
/// Options for bulk deleting messages
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
cfg_attr(feature = "validator", derive(Validate))
|
||||
)]
|
||||
pub struct OptionsBulkDelete {
|
||||
/// Message IDs
|
||||
#[validate(length(min = 1, max = 100))]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Options for removing reaction
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsUnreact {
|
||||
/// Remove a specific user's reaction
|
||||
pub user_id: Option<String>,
|
||||
/// Remove all reactions
|
||||
pub remove_all: Option<bool>,
|
||||
}
|
||||
);
|
||||
|
||||
/// Message Author Abstraction
|
||||
pub enum MessageAuthor<'a> {
|
||||
User(&'a User),
|
||||
Webhook(&'a Webhook),
|
||||
System {
|
||||
username: &'a str,
|
||||
avatar: Option<&'a str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Interactions {
|
||||
/// Check if default initialisation of fields
|
||||
pub fn is_default(&self) -> bool {
|
||||
!self.restrict_reactions && self.reactions.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> MessageAuthor<'a> {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
MessageAuthor::User(user) => &user.id,
|
||||
MessageAuthor::Webhook(webhook) => &webhook.id,
|
||||
MessageAuthor::System { .. } => "00000000000000000000000000",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn avatar(&self) -> Option<&str> {
|
||||
match self {
|
||||
MessageAuthor::User(user) => user.avatar.as_ref().map(|file| file.id.as_str()),
|
||||
MessageAuthor::Webhook(webhook) => webhook.avatar.as_ref().map(|file| file.id.as_str()),
|
||||
MessageAuthor::System { avatar, .. } => *avatar,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn username(&self) -> &str {
|
||||
match self {
|
||||
MessageAuthor::User(user) => &user.username,
|
||||
MessageAuthor::Webhook(webhook) => &webhook.name,
|
||||
MessageAuthor::System { username, .. } => username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemMessage> for String {
|
||||
fn from(s: SystemMessage) -> String {
|
||||
match s {
|
||||
SystemMessage::Text { content } => content,
|
||||
SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(),
|
||||
SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(),
|
||||
SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(),
|
||||
SystemMessage::UserLeft { .. } => "User left the channel.".to_string(),
|
||||
SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(),
|
||||
SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(),
|
||||
SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(),
|
||||
SystemMessage::ChannelDescriptionChanged { .. } => {
|
||||
"Channel description changed.".to_string()
|
||||
}
|
||||
SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(),
|
||||
SystemMessage::ChannelOwnershipChanged { .. } => {
|
||||
"Channel ownership changed.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PushNotification {
|
||||
/// Create a new notification from a given message, author and channel ID
|
||||
pub async fn from(msg: Message, author: Option<MessageAuthor<'_>>, channel_id: &str) -> Self {
|
||||
let config = config().await;
|
||||
|
||||
let icon = if let Some(author) = &author {
|
||||
if let Some(avatar) = author.avatar() {
|
||||
format!("{}/avatars/{}", config.hosts.autumn, avatar)
|
||||
} else {
|
||||
format!("{}/users/{}/default_avatar", config.hosts.api, author.id())
|
||||
}
|
||||
} else {
|
||||
format!("{}/assets/logo.png", config.hosts.app)
|
||||
};
|
||||
|
||||
let image = msg.attachments.and_then(|attachments| {
|
||||
attachments
|
||||
.first()
|
||||
.map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
|
||||
});
|
||||
|
||||
let body = if let Some(sys) = msg.system {
|
||||
sys.into()
|
||||
} else if let Some(text) = msg.content {
|
||||
text
|
||||
} else {
|
||||
"Empty Message".to_string()
|
||||
};
|
||||
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("Time went backwards")
|
||||
.as_secs();
|
||||
|
||||
Self {
|
||||
author: author
|
||||
.map(|x| x.username().to_string())
|
||||
.unwrap_or_else(|| "Revolt".to_string()),
|
||||
icon,
|
||||
image,
|
||||
body,
|
||||
tag: channel_id.to_string(),
|
||||
timestamp,
|
||||
url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ mod embeds;
|
||||
mod emojis;
|
||||
mod files;
|
||||
mod messages;
|
||||
mod safety_reports;
|
||||
mod server_bans;
|
||||
mod server_members;
|
||||
mod servers;
|
||||
@@ -22,6 +23,7 @@ pub use embeds::*;
|
||||
pub use emojis::*;
|
||||
pub use files::*;
|
||||
pub use messages::*;
|
||||
pub use safety_reports::*;
|
||||
pub use server_bans::*;
|
||||
pub use server_members::*;
|
||||
pub use servers::*;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user