Compare commits
101
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -0,0 +1,2 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "3s", terminate-after = 2 }
|
||||
@@ -1,5 +1,7 @@
|
||||
# MongoDB URI
|
||||
MONGODB=mongodb://localhost
|
||||
AUTUMN_MONGO_URI=mongodb://database
|
||||
REDIS_URI=redis://localhost/
|
||||
|
||||
# URL to where the Revolt app is publicly accessible
|
||||
REVOLT_APP_URL=http://local.revolt.chat:5000
|
||||
@@ -80,3 +82,26 @@ REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nf
|
||||
##
|
||||
|
||||
# VOSO_MANAGE_TOKEN=CHANGEME
|
||||
|
||||
|
||||
##
|
||||
## Autumn configuration
|
||||
##
|
||||
|
||||
# S3 Region
|
||||
AUTUMN_S3_REGION=minio
|
||||
|
||||
# S3 Endpoint
|
||||
AUTUMN_S3_ENDPOINT=http://minio:9000
|
||||
|
||||
# MinIO Root User
|
||||
MINIO_ROOT_USER=minioautumn
|
||||
|
||||
# MinIO Root Password
|
||||
MINIO_ROOT_PASSWORD=minioautumn
|
||||
|
||||
# AWS Access Key ID
|
||||
AWS_ACCESS_KEY_ID=minioautumn
|
||||
|
||||
# AWS Secret Key
|
||||
AWS_SECRET_ACCESS_KEY=minioautumn
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Docker Test & Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "main"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
@@ -15,7 +15,7 @@ on:
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "main"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
+28
-21
@@ -3,7 +3,7 @@ name: Rust build, test, and generate specification
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -20,6 +20,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 +35,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 +74,11 @@ jobs:
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
with:
|
||||
cwd: "api"
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
projectV2(number: 5) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Rocket.toml
|
||||
Revolt.toml
|
||||
|
||||
target
|
||||
.data
|
||||
.env
|
||||
|
||||
Vendored
+2
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"nixEnvSelector.suggestion": false
|
||||
"nixEnvSelector.suggestion": false,
|
||||
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
|
||||
}
|
||||
|
||||
Generated
+292
-116
@@ -104,6 +104,12 @@ version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.6"
|
||||
@@ -165,7 +171,7 @@ dependencies = [
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
"tokio 0.2.25",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -246,7 +252,7 @@ dependencies = [
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
"wasm-bindgen-futures",
|
||||
@@ -315,7 +321,7 @@ dependencies = [
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"log",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
@@ -347,9 +353,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "authifier"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a84411393a4326726ead660991ce8047f1865fa89900fb6c18e1e6c513cf1f9b"
|
||||
checksum = "30269caf0aaf1e1b542b150030e9688bf41d50026e09a51efd9408f332636c9d"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -588,9 +594,19 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.1.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
|
||||
[[package]]
|
||||
name = "bytes-utils"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
|
||||
dependencies = [
|
||||
"bytes 1.5.0",
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cache-padded"
|
||||
@@ -612,7 +628,7 @@ dependencies = [
|
||||
"instant",
|
||||
"once_cell",
|
||||
"thiserror",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -712,11 +728,11 @@ version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"futures-core",
|
||||
"memchr",
|
||||
"pin-project-lite 0.2.9",
|
||||
"tokio 1.18.2",
|
||||
"pin-project-lite 0.2.13",
|
||||
"tokio 1.35.1",
|
||||
"tokio-util 0.7.2",
|
||||
]
|
||||
|
||||
@@ -784,6 +800,12 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie-factory"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
@@ -809,6 +831,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc16"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.5"
|
||||
@@ -1002,7 +1030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16a2561fd313df162315935989dceb8c99db4ee1933358270a57a3cfb8c957f3"
|
||||
dependencies = [
|
||||
"crossbeam-queue",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1348,6 +1376,15 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1371,14 +1408,40 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.0.1"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
|
||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||
dependencies = [
|
||||
"matches",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fred"
|
||||
version = "8.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3b2a2ac060e3266004c552235c241b481e438e2b1ea75715ea1176914ef2868"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
"bytes 1.5.0",
|
||||
"bytes-utils",
|
||||
"crossbeam-queue",
|
||||
"float-cmp",
|
||||
"futures",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"redis-protocol",
|
||||
"semver 1.0.9",
|
||||
"socket2 0.5.5",
|
||||
"tokio 1.35.1",
|
||||
"tokio-stream",
|
||||
"tokio-util 0.7.2",
|
||||
"url",
|
||||
"urlencoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
@@ -1460,7 +1523,7 @@ dependencies = [
|
||||
"futures-io",
|
||||
"memchr",
|
||||
"parking",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
@@ -1472,7 +1535,7 @@ checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-task",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1517,7 +1580,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
@@ -1661,7 +1724,7 @@ version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1669,7 +1732,7 @@ dependencies = [
|
||||
"http",
|
||||
"indexmap",
|
||||
"slab",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tokio-util 0.7.2",
|
||||
"tracing",
|
||||
]
|
||||
@@ -1802,7 +1865,7 @@ version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"fnv",
|
||||
"itoa 1.0.2",
|
||||
]
|
||||
@@ -1823,9 +1886,9 @@ version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"http",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1885,7 +1948,7 @@ version = "0.14.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -1895,9 +1958,9 @@ dependencies = [
|
||||
"httparse",
|
||||
"httpdate 1.0.2",
|
||||
"itoa 1.0.2",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"socket2 0.4.4",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"want",
|
||||
@@ -1922,10 +1985,10 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"hyper 0.14.19",
|
||||
"native-tls",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tokio-native-tls",
|
||||
]
|
||||
|
||||
@@ -1946,6 +2009,16 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "if_chain"
|
||||
version = "1.0.2"
|
||||
@@ -2183,7 +2256,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hostname",
|
||||
"httpdate 1.0.2",
|
||||
"idna",
|
||||
"idna 0.2.3",
|
||||
"mime",
|
||||
"native-tls",
|
||||
"nom",
|
||||
@@ -2194,9 +2267,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.126"
|
||||
version = "0.2.153"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
@@ -2429,14 +2502,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.3"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
|
||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2466,7 +2538,7 @@ dependencies = [
|
||||
"log",
|
||||
"metrics",
|
||||
"thiserror",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
@@ -2518,7 +2590,7 @@ dependencies = [
|
||||
"strsim",
|
||||
"take_mut",
|
||||
"thiserror",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tokio-rustls",
|
||||
"tokio-util 0.7.2",
|
||||
"trust-dns-proto",
|
||||
@@ -2534,7 +2606,7 @@ version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"encoding_rs",
|
||||
"futures-util",
|
||||
"http",
|
||||
@@ -2543,7 +2615,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"mime",
|
||||
"spin 0.9.3",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tokio-util 0.6.10",
|
||||
"version_check",
|
||||
]
|
||||
@@ -2846,7 +2918,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-sys",
|
||||
"windows-sys 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2924,9 +2996,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.1.0"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
@@ -2999,9 +3071,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.9"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
@@ -3114,9 +3186,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.56"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -3334,15 +3406,15 @@ source = "git+https://github.com/revoltchat/redis-rs?rev=1a41faf356fd21aebba71ce
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"combine",
|
||||
"futures-util",
|
||||
"itoa 1.0.2",
|
||||
"percent-encoding",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"ryu",
|
||||
"sha1_smol",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tokio-util 0.7.2",
|
||||
"url",
|
||||
]
|
||||
@@ -3354,14 +3426,14 @@ source = "git+https://github.com/revoltchat/redis-rs?rev=f8ca28ab85da59d2ccde526
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"combine",
|
||||
"futures-util",
|
||||
"itoa 1.0.2",
|
||||
"percent-encoding",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"ryu",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tokio-util 0.7.2",
|
||||
"url",
|
||||
]
|
||||
@@ -3382,6 +3454,20 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redis-protocol"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c31deddf734dc0a39d3112e73490e88b61a05e83e074d211f348404cee4d2c6"
|
||||
dependencies = [
|
||||
"bytes 1.5.0",
|
||||
"bytes-utils",
|
||||
"cookie-factory",
|
||||
"crc16",
|
||||
"log",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.13"
|
||||
@@ -3453,7 +3539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -3469,11 +3555,11 @@ dependencies = [
|
||||
"mime",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tokio-native-tls",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@@ -3494,10 +3580,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-tungstenite",
|
||||
"bincode",
|
||||
"fred",
|
||||
"futures",
|
||||
"log",
|
||||
"once_cell",
|
||||
@@ -3512,19 +3600,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-config"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"cached",
|
||||
"config",
|
||||
"dotenv",
|
||||
"futures-locks",
|
||||
"log",
|
||||
"once_cell",
|
||||
"pretty_env_logger",
|
||||
"sentry",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-database"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"async-recursion",
|
||||
@@ -3564,15 +3656,17 @@ dependencies = [
|
||||
"ulid 1.0.0",
|
||||
"unicode-segmentation",
|
||||
"url-escape",
|
||||
"validator 0.16.0",
|
||||
"web-push 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "revolt-delta"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-std",
|
||||
"authifier",
|
||||
"bitfield",
|
||||
"chrono",
|
||||
"dashmap",
|
||||
@@ -3580,6 +3674,7 @@ dependencies = [
|
||||
"env_logger",
|
||||
"futures",
|
||||
"impl_ops",
|
||||
"iso8601-timestamp 0.2.11",
|
||||
"lettre",
|
||||
"linkify 0.6.0",
|
||||
"log",
|
||||
@@ -3591,10 +3686,10 @@ dependencies = [
|
||||
"redis-kiss",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"revolt-config",
|
||||
"revolt-database",
|
||||
"revolt-models",
|
||||
"revolt-permissions",
|
||||
"revolt-quark",
|
||||
"revolt-result",
|
||||
"revolt_rocket_okapi",
|
||||
"rocket",
|
||||
@@ -3613,7 +3708,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-models"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"iso8601-timestamp 0.2.11",
|
||||
@@ -3622,6 +3717,7 @@ dependencies = [
|
||||
"revolt-config",
|
||||
"revolt-permissions",
|
||||
"revolt_optional_struct",
|
||||
"rocket",
|
||||
"schemars",
|
||||
"serde",
|
||||
"validator 0.16.0",
|
||||
@@ -3629,7 +3725,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-permissions"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -3644,7 +3740,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-presence"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"log",
|
||||
@@ -3655,7 +3751,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-quark"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"async-recursion",
|
||||
@@ -3708,7 +3804,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "revolt-result"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi",
|
||||
@@ -3828,7 +3924,7 @@ dependencies = [
|
||||
"atomic",
|
||||
"atty",
|
||||
"binascii",
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"either",
|
||||
"figment",
|
||||
"futures",
|
||||
@@ -3838,7 +3934,7 @@ dependencies = [
|
||||
"multer",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"rand 0.8.5",
|
||||
"ref-cast",
|
||||
"rocket_codegen",
|
||||
@@ -3848,7 +3944,7 @@ dependencies = [
|
||||
"state",
|
||||
"tempfile",
|
||||
"time 0.3.17",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"tokio-stream",
|
||||
"tokio-util 0.7.2",
|
||||
"ubyte",
|
||||
@@ -3858,9 +3954,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rocket_authifier"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4554a81937341d8c3cd3e43431ac2d23624444dc3a88e7ffd3ff66593779293"
|
||||
checksum = "5f89a12311f60e9288833fc3ce6029bce5d5c61870ceef74d4a50668a8b520ad"
|
||||
dependencies = [
|
||||
"authifier",
|
||||
"iso8601-timestamp 0.1.10",
|
||||
@@ -3931,14 +4027,14 @@ dependencies = [
|
||||
"memchr",
|
||||
"pear",
|
||||
"percent-encoding",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"stable-pattern",
|
||||
"state",
|
||||
"time 0.3.17",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"uncased",
|
||||
]
|
||||
|
||||
@@ -4086,7 +4182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys",
|
||||
"windows-sys 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4220,7 +4316,7 @@ dependencies = [
|
||||
"sentry-debug-images",
|
||||
"sentry-panic",
|
||||
"sentry-tracing",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
@@ -4521,6 +4617,16 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
@@ -4781,33 +4887,32 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.18.2"
|
||||
version = "1.35.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395"
|
||||
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"backtrace",
|
||||
"bytes 1.5.0",
|
||||
"libc",
|
||||
"memchr",
|
||||
"mio 0.8.3",
|
||||
"mio 0.8.10",
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.4.4",
|
||||
"socket2 0.5.5",
|
||||
"tokio-macros",
|
||||
"winapi 0.3.9",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "1.7.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote 1.0.26",
|
||||
"syn 1.0.107",
|
||||
"syn 2.0.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4817,7 +4922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4827,7 +4932,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"webpki",
|
||||
]
|
||||
|
||||
@@ -4838,8 +4943,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite 0.2.9",
|
||||
"tokio 1.18.2",
|
||||
"pin-project-lite 0.2.13",
|
||||
"tokio 1.35.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4872,12 +4977,12 @@ version = "0.6.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"log",
|
||||
"pin-project-lite 0.2.9",
|
||||
"tokio 1.18.2",
|
||||
"pin-project-lite 0.2.13",
|
||||
"tokio 1.35.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4886,12 +4991,12 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c"
|
||||
dependencies = [
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"pin-project-lite 0.2.9",
|
||||
"tokio 1.18.2",
|
||||
"pin-project-lite 0.2.13",
|
||||
"tokio 1.35.1",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -4930,7 +5035,7 @@ checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"log",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-project-lite 0.2.13",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
@@ -5008,7 +5113,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"idna",
|
||||
"idna 0.2.3",
|
||||
"ipnet",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@@ -5016,7 +5121,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -5036,7 +5141,7 @@ dependencies = [
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"tokio 1.18.2",
|
||||
"tokio 1.35.1",
|
||||
"trust-dns-proto",
|
||||
]
|
||||
|
||||
@@ -5054,7 +5159,7 @@ checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"byteorder",
|
||||
"bytes 1.1.0",
|
||||
"bytes 1.5.0",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
@@ -5167,9 +5272,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.8"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
|
||||
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
@@ -5179,9 +5284,9 @@ checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.19"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
@@ -5241,13 +5346,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.2.2"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
|
||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"matches",
|
||||
"idna 0.5.0",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
@@ -5261,6 +5365,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
@@ -5293,7 +5403,7 @@ version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f07b0a1390e01c0fc35ebb26b28ced33c9a3808f7f9fbe94d3cc01e233bfeed5"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"idna 0.2.3",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -5308,7 +5418,7 @@ version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"idna 0.2.3",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -5624,43 +5734,109 @@ version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
|
||||
dependencies = [
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_msvc 0.36.1",
|
||||
"windows_i686_gnu 0.36.1",
|
||||
"windows_i686_msvc 0.36.1",
|
||||
"windows_x86_64_gnu 0.36.1",
|
||||
"windows_x86_64_msvc 0.36.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.7.0"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
|
||||
|
||||
[patch.crates-io]
|
||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||
redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
|
||||
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
|
||||
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
|
||||
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
|
||||
|
||||
@@ -2,34 +2,112 @@
|
||||
|
||||
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 |
|
||||
| `quark` | [crates/quark](crates/quark) | Models and logic (**DEPRECATED**) |
|
||||
|
||||
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...
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
## 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!**
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
disallowed-methods = [
|
||||
# Shouldn't need to access these directly
|
||||
"revolt_database::models::bots::model::Bot::remove_field",
|
||||
"revolt_database::models::messages::model::Message::attach_sendable_embed",
|
||||
"revolt_database::models::users::model::User::set_relationship",
|
||||
"revolt_database::models::users::model::User::apply_relationship",
|
||||
|
||||
# Prefer to use Object::create()
|
||||
"revolt_database::models::bots::ops::AbstractBots::insert_bot",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -18,6 +18,7 @@ querystring = "1.1.0"
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# serde
|
||||
bincode = "1.3.3"
|
||||
serde_json = "1.0.79"
|
||||
rmp-serde = "1.0.0"
|
||||
serde = "1.0.136"
|
||||
@@ -34,3 +35,6 @@ async-std = { version = "1.8.0", features = [
|
||||
# core
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
sentry = "0.31.5"
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
|
||||
@@ -29,6 +29,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:?}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+253
-228
@@ -1,6 +1,16 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use fred::{
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
pin_mut, select,
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
use revolt_quark::{
|
||||
events::{
|
||||
@@ -9,249 +19,264 @@ use revolt_quark::{
|
||||
state::{State, SubscriptionStateChange},
|
||||
},
|
||||
models::{user::UserHint, User},
|
||||
redis_kiss, Database,
|
||||
redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI},
|
||||
Database,
|
||||
};
|
||||
|
||||
use async_std::{net::TcpStream, sync::Mutex, task};
|
||||
use async_std::{net::TcpStream, sync::Mutex};
|
||||
|
||||
use crate::config::WebsocketHandshakeCallback;
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
|
||||
/// 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;
|
||||
};
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
config.get_protocol_format()
|
||||
);
|
||||
|
||||
// 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,
|
||||
)
|
||||
// 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(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
return;
|
||||
};
|
||||
let user = 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);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
if write
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.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()
|
||||
);
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Split the socket for simultaneously read and write.
|
||||
let (write, mut read) = ws.split();
|
||||
let write = Mutex::new(write);
|
||||
// Download required data to local cache and send Ready payload.
|
||||
let Ok(ready_payload) = state.generate_ready_payload(db).await else {
|
||||
return;
|
||||
};
|
||||
if write.send(config.encode(&ready_payload)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
'outer: while let Ok(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
config.set_session_token(token);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
|
||||
// If this was the first session, notify other users that we just went online.
|
||||
if first_session {
|
||||
state.broadcast_presence_change(true).await;
|
||||
}
|
||||
|
||||
{
|
||||
let write = Mutex::new(write);
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener(db, &mut state, addr, &config, &write).fuse();
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker(addr, user_id.clone(), &config, read, &write).fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
|
||||
// Wait for either disconnect or for listener to die.
|
||||
select!(
|
||||
() = 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;
|
||||
}
|
||||
}
|
||||
|
||||
async fn listener(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
||||
let Ok(subscriber) = fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if subscriber.init().await.is_err() {
|
||||
return;
|
||||
};
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state() {
|
||||
SubscriptionStateChange::Reset => {
|
||||
subscriber.unsubscribe_all().await.unwrap();
|
||||
for id in state.iter_subscriptions() {
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
// 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();
|
||||
subscriber.unsubscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
// 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(|res| {
|
||||
res.map(|item|(
|
||||
item.get_channel_name().to_string(),
|
||||
redis_kiss::decode_payload::<EventV1>(&item),
|
||||
))
|
||||
}) {
|
||||
Some(Ok((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}!");
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
info!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
break
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// * 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:?}");
|
||||
});
|
||||
// Handle incoming events.
|
||||
let Ok(message) = message_rx.recv().await.map_err(|e| {
|
||||
warn!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
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 {
|
||||
warn!("Failed to deserialise an event for {}!", message.channel);
|
||||
return;
|
||||
};
|
||||
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) {
|
||||
warn!("Error while sending an event to {addr:?}: {e:?}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn worker(
|
||||
addr: SocketAddr,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
loop {
|
||||
let result = read.try_next().await;
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => return,
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
warn!("Error while reading an event from {addr:?}: {e:?}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
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();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -14,6 +14,7 @@ default = ["test"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
dotenv = "0.15.0"
|
||||
config = "0.13.3"
|
||||
cached = "0.44.0"
|
||||
once_cell = "1.18.0"
|
||||
@@ -24,3 +25,10 @@ serde = { version = "1", features = ["derive"] }
|
||||
# Async
|
||||
futures-locks = "0.7.1"
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
|
||||
# Logging
|
||||
log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
# Sentry
|
||||
sentry = "0.31.5"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost/"
|
||||
@@ -1,3 +1,5 @@
|
||||
sentry_dsn = ""
|
||||
|
||||
[database]
|
||||
mongodb = "mongodb://database"
|
||||
redis = "redis://redis/"
|
||||
@@ -33,6 +35,7 @@ api_key = ""
|
||||
[api.security]
|
||||
authifier_shield_key = ""
|
||||
voso_legacy_token = ""
|
||||
trust_cloudflare = false
|
||||
|
||||
[api.security.captcha]
|
||||
hcaptcha_key = ""
|
||||
@@ -42,15 +45,18 @@ hcaptcha_sitekey = ""
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[features]
|
||||
webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.default]
|
||||
group_size = 100
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_embeds = 5
|
||||
message_replies = 5
|
||||
message_attachments = 5
|
||||
message_embeds = 5
|
||||
message_reactions = 20
|
||||
servers = 100
|
||||
server_emoji = 100
|
||||
server_roles = 200
|
||||
|
||||
@@ -13,8 +13,13 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
FileFormat::Toml,
|
||||
));
|
||||
|
||||
if std::path::Path::new("revolt.toml").exists() {
|
||||
builder = builder.add_source(File::new("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()
|
||||
@@ -51,6 +56,9 @@ pub struct ApiSmtp {
|
||||
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)]
|
||||
@@ -75,6 +83,7 @@ pub struct ApiSecurity {
|
||||
pub authifier_shield_key: String,
|
||||
pub voso_legacy_token: String,
|
||||
pub captcha: ApiSecurityCaptcha,
|
||||
pub trust_cloudflare: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -97,9 +106,11 @@ pub struct Api {
|
||||
pub struct FeaturesLimits {
|
||||
pub group_size: usize,
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_replies: usize,
|
||||
pub message_attachments: usize,
|
||||
pub message_embeds: usize,
|
||||
pub message_reactions: usize,
|
||||
pub servers: usize,
|
||||
pub server_emoji: usize,
|
||||
pub server_roles: usize,
|
||||
@@ -124,6 +135,7 @@ pub struct FeaturesLimitsCollection {
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Features {
|
||||
pub limits: FeaturesLimitsCollection,
|
||||
pub webhooks_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -132,6 +144,31 @@ pub struct Settings {
|
||||
pub hosts: Hosts,
|
||||
pub api: Api,
|
||||
pub features: Features,
|
||||
pub sentry_dsn: String,
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -150,6 +187,47 @@ 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) -> 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}");
|
||||
|
||||
let config = config().await;
|
||||
if config.sentry_dsn.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(sentry::init((
|
||||
config.sentry_dsn,
|
||||
sentry::ClientOptions {
|
||||
release: Some(release.into()),
|
||||
..Default::default()
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! configure {
|
||||
() => {
|
||||
let _sentry = $crate::setup_logging(concat!(
|
||||
env!("CARGO_PKG_NAME"),
|
||||
"@",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
))
|
||||
.await;
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -23,11 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.6.8", path = "../config" }
|
||||
revolt-result = { version = "0.6.8", path = "../result" }
|
||||
revolt-models = { version = "0.6.8", path = "../models" }
|
||||
revolt-presence = { version = "0.6.8", path = "../presence" }
|
||||
revolt-permissions = { version = "0.6.8", path = "../permissions", features = [
|
||||
revolt-config = { version = "0.7.0", path = "../config" }
|
||||
revolt-result = { version = "0.7.0", path = "../result" }
|
||||
revolt-models = { version = "0.7.0", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.0", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.0", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
@@ -45,6 +47,7 @@ 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
|
||||
@@ -86,4 +89,4 @@ fcm = "0.9.2"
|
||||
web-push = "0.10.0"
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0" }
|
||||
authifier = { version = "1.0.8" }
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Member",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "Member",
|
||||
"discriminator": "0002"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "Group",
|
||||
"name": "My Group",
|
||||
"owner": "__ID:0__",
|
||||
"recipients": ["__ID:0__", "__ID:1__"]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,89 @@
|
||||
[
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Moderator",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "User",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "Channel",
|
||||
"_id": "__ID:3__",
|
||||
"channel_type": "TextChannel",
|
||||
"name": "General",
|
||||
"server": "__ID:4__",
|
||||
"default_permissions": {
|
||||
"a": 0,
|
||||
"d": 1048576
|
||||
},
|
||||
"role_permissions": {
|
||||
"__ID:5__": {
|
||||
"a": 1048576,
|
||||
"d": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"_object_type": "Server",
|
||||
"_id": "__ID:4__",
|
||||
"owner": "__ID:0__",
|
||||
"name": "Server",
|
||||
"channels": ["__ID:3__"],
|
||||
"roles": {
|
||||
"__ID:5__": {
|
||||
"name": "Moderator",
|
||||
"permissions": {
|
||||
"a": 545270208,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 3
|
||||
},
|
||||
"__ID:6__": {
|
||||
"name": "Owner",
|
||||
"permissions": {
|
||||
"a": 0,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 0
|
||||
}
|
||||
},
|
||||
"default_permissions": 4000322560
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:0__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": ["__ID:6__"],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:1__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": ["__ID:5__"],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
"_object_type": "ServerMember",
|
||||
"_id": {
|
||||
"user": "__ID:2__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"joined_at": 1698318340195
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,9 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
|
||||
pub use self::mongodb::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
@@ -31,25 +34,44 @@ impl DatabaseInfo {
|
||||
/// Create a database client from the given database information
|
||||
#[async_recursion]
|
||||
pub async fn connect(self) -> Result<Database, String> {
|
||||
let config = config().await;
|
||||
|
||||
Ok(match self {
|
||||
DatabaseInfo::Auto => {
|
||||
if let Ok(uri) = std::env::var("MONGODB") {
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri,
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
DatabaseInfo::Test(format!(
|
||||
"revolt_test_{}",
|
||||
rand::thread_rng().gen_range(1_000_000..10_000_000)
|
||||
))
|
||||
.connect()
|
||||
.await?
|
||||
} else if !config.database.mongodb.is_empty() {
|
||||
DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name: "revolt".to_string(),
|
||||
}
|
||||
.connect()
|
||||
.await;
|
||||
.await?
|
||||
} else {
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
DatabaseInfo::Test(database_name) => {
|
||||
if let Ok(uri) = std::env::var("MONGODB") {
|
||||
return DatabaseInfo::MongoDb { uri, database_name }.connect().await;
|
||||
match std::env::var("TEST_DB")
|
||||
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
|
||||
.as_str()
|
||||
{
|
||||
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
|
||||
"MONGODB" => {
|
||||
DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name,
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
}
|
||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
||||
}
|
||||
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
DatabaseInfo::Reference => Database::Reference(Default::default()),
|
||||
DatabaseInfo::MongoDb { uri, database_name } => {
|
||||
@@ -65,14 +87,3 @@ impl DatabaseInfo {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Database> for authifier::Database {
|
||||
fn from(value: Database) -> Self {
|
||||
match value {
|
||||
Database::Reference(_) => Default::default(),
|
||||
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
|
||||
MemberCompositeKey, Message, Server, ServerBan, User, UserSettings, Webhook,
|
||||
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
@@ -19,12 +20,13 @@ database_derived!(
|
||||
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
|
||||
pub files: Arc<Mutex<HashMap<String, File>>>,
|
||||
pub messages: Arc<Mutex<HashMap<String, Message>>>,
|
||||
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
|
||||
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
||||
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
|
||||
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
|
||||
pub servers: Arc<Mutex<HashMap<String, Server>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
|
||||
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, UserSettings,
|
||||
Webhook,
|
||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
use revolt_result::Error;
|
||||
|
||||
@@ -110,6 +110,7 @@ pub enum EventV1 {
|
||||
id: String,
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
emojis: Vec<Emoji>,
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
@@ -154,17 +155,12 @@ pub enum EventV1 {
|
||||
event_id: Option<String>,
|
||||
},
|
||||
|
||||
/*/// Relationship with another user changed
|
||||
UserRelationship {
|
||||
id: String,
|
||||
user: User,
|
||||
// ! this field can be deprecated
|
||||
status: RelationshipStatus,
|
||||
},*/
|
||||
/// Relationship with another user changed
|
||||
UserRelationship { id: String, user: User },
|
||||
/// Settings updated remotely
|
||||
UserSettingsUpdate { id: String, update: UserSettings },
|
||||
|
||||
/*/// User has been platform banned or deleted their account
|
||||
/// User has been platform banned or deleted their account
|
||||
///
|
||||
/// Clients should remove the following associated data:
|
||||
/// - Messages
|
||||
@@ -173,15 +169,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),
|
||||
|
||||
@@ -248,8 +244,7 @@ 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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
@@ -79,9 +80,8 @@ impl Bot {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
// TODO: config
|
||||
let max_bot_count = 5;
|
||||
if db.get_number_of_bots_by_user(&owner.id).await? >= max_bot_count {
|
||||
let config = config().await;
|
||||
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
|
||||
return Err(create_error!(ReachedMaximumBots));
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ impl Bot {
|
||||
Some(id.to_string()),
|
||||
Some(PartialUser {
|
||||
bot: Some(BotInformation {
|
||||
owner: id.to_string(),
|
||||
owner: owner.id.to_string(),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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
|
||||
@@ -56,9 +56,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 +76,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> {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use revolt_models::v0::MessageAuthor;
|
||||
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, SystemMessage, User};
|
||||
use crate::{
|
||||
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
|
||||
Server, SystemMessage, User,
|
||||
};
|
||||
|
||||
auto_derived!(
|
||||
#[serde(tag = "channel_type")]
|
||||
@@ -168,7 +173,7 @@ 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?;
|
||||
|
||||
@@ -186,6 +191,146 @@ 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.default.server_channels {
|
||||
return Err(create_error!(TooManyChannels {
|
||||
max: config.features.limits.default.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.default.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.default.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
|
||||
@@ -200,6 +345,13 @@ impl Channel {
|
||||
return Err(create_error!(AlreadyInGroup));
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
if recipients.len() >= config.features.limits.default.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.default.group_size
|
||||
}));
|
||||
}
|
||||
|
||||
recipients.push(String::from(&user.id));
|
||||
}
|
||||
|
||||
@@ -214,10 +366,6 @@ impl Channel {
|
||||
.p(id.to_string())
|
||||
.await;
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user.id.to_string())
|
||||
.await;
|
||||
|
||||
SystemMessage::UserAdded {
|
||||
id: user.id.to_string(),
|
||||
by: by_id.to_string(),
|
||||
@@ -235,6 +383,10 @@ impl Channel {
|
||||
.await
|
||||
.ok();
|
||||
|
||||
EventV1::ChannelCreate(self.clone().into())
|
||||
.private(user.id.to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
@@ -262,7 +414,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, .. }
|
||||
@@ -475,6 +627,28 @@ 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,
|
||||
@@ -520,8 +694,7 @@ impl Channel {
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
db.delete_channel(self).await?;
|
||||
return Ok(());
|
||||
return self.delete(db).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,6 +741,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
|
||||
}
|
||||
}
|
||||
@@ -581,3 +757,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,13 +1,22 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_models::v0::{Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification};
|
||||
use revolt_config::config;
|
||||
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::{
|
||||
events::client::EventV1,
|
||||
tasks::{self, ack::AckEvent},
|
||||
Channel, Database, File,
|
||||
util::idempotency::IdempotencyKey,
|
||||
Channel, Database, Emoji, File, User,
|
||||
};
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -147,6 +156,7 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// Message Filter
|
||||
#[derive(Default)]
|
||||
pub struct MessageFilter {
|
||||
/// Parent channel ID
|
||||
pub channel: Option<String>,
|
||||
@@ -196,6 +206,165 @@ impl Default for Message {
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Message {
|
||||
/// Create message from API data
|
||||
pub async fn create_from_api(
|
||||
db: &Database,
|
||||
channel: Channel,
|
||||
data: DataMessageSend,
|
||||
author: MessageAuthor<'_>,
|
||||
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(),
|
||||
config.features.limits.default.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.default.message_replies {
|
||||
return Err(create_error!(TooManyReplies {
|
||||
max: config.features.limits.default.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() > config.features.limits.default.message_attachments)
|
||||
{
|
||||
return Err(create_error!(TooManyAttachments {
|
||||
max: config.features.limits.default.message_attachments,
|
||||
}));
|
||||
}
|
||||
|
||||
if data
|
||||
.embeds
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
|
||||
{
|
||||
return Err(create_error!(TooManyEmbeds {
|
||||
max: config.features.limits.default.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, &channel, generate_embeds).await?;
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Send a message without any notifications
|
||||
pub async fn send_without_notifications(
|
||||
&mut self,
|
||||
@@ -274,6 +443,93 @@ impl Message {
|
||||
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(Into::into)
|
||||
.collect();
|
||||
|
||||
if let Some(true) = include_users {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.map(|m| m.author.clone())
|
||||
.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,
|
||||
@@ -293,6 +549,187 @@ impl Message {
|
||||
|
||||
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.default.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 {
|
||||
@@ -309,6 +746,27 @@ impl SystemMessage {
|
||||
}
|
||||
|
||||
impl Interactions {
|
||||
/// Validate interactions info is correct
|
||||
pub async fn validate(&self, db: &Database, permissions: &PermissionValue) -> Result<()> {
|
||||
let config = config().await;
|
||||
|
||||
if let Some(reactions) = &self.reactions {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
||||
|
||||
if reactions.len() > config.features.limits.default.message_reactions {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
for reaction in reactions {
|
||||
if !Emoji::can_use(db, reaction).await? {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if we can use a given emoji to react
|
||||
pub fn can_use(&self, emoji: &str) -> bool {
|
||||
if self.restrict_reactions {
|
||||
|
||||
@@ -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<()>;
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ use mongodb::options::FindOptions;
|
||||
use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage};
|
||||
use crate::{
|
||||
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
|
||||
};
|
||||
|
||||
use super::AbstractMessages;
|
||||
|
||||
@@ -159,6 +161,21 @@ impl AbstractMessages for MongoDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(|_| ())
|
||||
@@ -278,3 +295,57 @@ impl AbstractMessages for MongoDb {
|
||||
.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,3 +1,4 @@
|
||||
use futures::future::try_join_all;
|
||||
use indexmap::IndexSet;
|
||||
use revolt_result::Result;
|
||||
|
||||
@@ -60,7 +61,7 @@ impl AbstractMessages for ReferenceDb {
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// TODO: sorting, etc
|
||||
// FIXME: sorting, etc (will be required for tests)
|
||||
|
||||
Ok(matched_messages)
|
||||
|
||||
@@ -176,6 +177,11 @@ impl AbstractMessages for ReferenceDb {
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Fetch multiple messages by given IDs
|
||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
|
||||
try_join_all(ids.iter().map(|id| self.fetch_message(id))).await
|
||||
}
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
|
||||
@@ -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::*;
|
||||
@@ -45,6 +49,8 @@ pub trait AbstractDatabase:
|
||||
+ files::AbstractAttachments
|
||||
+ messages::AbstractMessages
|
||||
+ ratelimit_events::AbstractRatelimitEvents
|
||||
+ safety_reports::AbstractReport
|
||||
+ safety_snapshots::AbstractSnapshot
|
||||
+ server_bans::AbstractServerBans
|
||||
+ server_members::AbstractServerMembers
|
||||
+ servers::AbstractServers
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::time::Duration;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::AbstractRatelimitEvents;
|
||||
use crate::RatelimitEvent;
|
||||
use crate::RatelimitEventType;
|
||||
use crate::ReferenceDb;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractRatelimitEvents for ReferenceDb {
|
||||
/// Insert a new ratelimit event
|
||||
async fn insert_ratelimit_event(&self, _event: &RatelimitEvent) -> Result<()> {
|
||||
// TODO: implement
|
||||
unimplemented!()
|
||||
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
|
||||
let mut ratelimit_events = self.ratelimit_events.lock().await;
|
||||
if ratelimit_events.contains_key(&event.id) {
|
||||
Err(create_database_error!("insert", "message"))
|
||||
} else {
|
||||
ratelimit_events.insert(event.id.to_string(), event.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Count number of events in given duration and check if we've hit the limit
|
||||
async fn has_ratelimited(
|
||||
&self,
|
||||
_target_id: &str,
|
||||
_event_type: RatelimitEventType,
|
||||
_period: Duration,
|
||||
_count: usize,
|
||||
target_id: &str,
|
||||
event_type: RatelimitEventType,
|
||||
period: Duration,
|
||||
count: usize,
|
||||
) -> Result<bool> {
|
||||
// TODO: implement
|
||||
unimplemented!()
|
||||
let ratelimit_events = self.ratelimit_events.lock().await;
|
||||
let gte_cmp_id = Ulid::from_datetime(SystemTime::now() - period).to_string();
|
||||
|
||||
Ok(ratelimit_events
|
||||
.iter()
|
||||
.filter(|(id, event)| {
|
||||
id.cmp(&>e_cmp_id) == Ordering::Greater
|
||||
&& event.target_id == target_id
|
||||
&& event.event_type == event_type
|
||||
})
|
||||
.count()
|
||||
>= count)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -0,0 +1,22 @@
|
||||
use revolt_models::v0::{ReportStatus, ReportedContent};
|
||||
|
||||
auto_derived!(
|
||||
/// User-generated platform moderation report
|
||||
pub struct Report {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Id of the user creating this report
|
||||
pub author_id: String,
|
||||
/// Reported content
|
||||
pub content: ReportedContent,
|
||||
/// Additional report context
|
||||
pub additional_context: String,
|
||||
/// Status of the report
|
||||
#[serde(flatten)]
|
||||
pub status: ReportStatus,
|
||||
/// Additional notes included on the report
|
||||
#[serde(default)]
|
||||
pub notes: String,
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Report;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractReport: Sync + Send {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::Report;
|
||||
|
||||
use super::AbstractReport;
|
||||
|
||||
static COL: &str = "safety_reports";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractReport for MongoDb {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()> {
|
||||
query!(self, insert_one, COL, &report).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::Report;
|
||||
|
||||
use super::AbstractReport;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractReport for ReferenceDb {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()> {
|
||||
let mut reports = self.safety_reports.lock().await;
|
||||
if reports.contains_key(&report.id) {
|
||||
Err(create_database_error!("insert", "report"))
|
||||
} else {
|
||||
reports.insert(report.id.to_string(), report.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -0,0 +1,121 @@
|
||||
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,
|
||||
) -> Result<(SnapshotContent, Vec<String>)> {
|
||||
// Collect message attachments
|
||||
let files = message
|
||||
.attachments
|
||||
.as_ref()
|
||||
.map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Collect prior context
|
||||
let prior_context = db
|
||||
.fetch_messages(MessageQuery {
|
||||
filter: MessageFilter {
|
||||
channel: Some(message.channel.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
limit: Some(15),
|
||||
time_period: MessageTimePeriod::Absolute {
|
||||
before: Some(message.id.to_string()),
|
||||
after: None,
|
||||
sort: Some(MessageSort::Latest),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Collect leading context
|
||||
let leading_context = db
|
||||
.fetch_messages(MessageQuery {
|
||||
filter: MessageFilter {
|
||||
channel: Some(message.channel.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
limit: Some(15),
|
||||
time_period: MessageTimePeriod::Absolute {
|
||||
before: None,
|
||||
after: Some(message.id.to_string()),
|
||||
sort: Some(MessageSort::Oldest),
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
SnapshotContent::Message {
|
||||
message,
|
||||
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]
|
||||
.iter()
|
||||
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
|
||||
.collect();
|
||||
|
||||
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 = [
|
||||
user.avatar.as_ref(),
|
||||
user.profile
|
||||
.as_ref()
|
||||
.and_then(|profile| profile.background.as_ref()),
|
||||
]
|
||||
.iter()
|
||||
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
|
||||
.collect();
|
||||
|
||||
Ok((SnapshotContent::User(user), files))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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<()>;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Database, File, Server,
|
||||
SystemMessage, User,
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
|
||||
Server, SystemMessage, User,
|
||||
};
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -80,9 +80,8 @@ impl Member {
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
user: &User,
|
||||
// channels: Option<Vec<Channel>>,
|
||||
//) -> Result<Vec<Channel>> {
|
||||
) -> Result<()> {
|
||||
channels: Option<Vec<Channel>>,
|
||||
) -> Result<Vec<Channel>> {
|
||||
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
|
||||
return Err(create_error!(Banned));
|
||||
}
|
||||
@@ -101,9 +100,10 @@ impl Member {
|
||||
|
||||
db.insert_member(&member).await?;
|
||||
|
||||
let mut channels = vec![];
|
||||
let should_fetch = channels.is_none();
|
||||
let mut channels = channels.unwrap_or_default();
|
||||
|
||||
if true {
|
||||
if should_fetch {
|
||||
let query = DatabasePermissionQuery::new(db, user).server(server);
|
||||
let existing_channels = db.fetch_channels(&server.channels).await?;
|
||||
|
||||
@@ -119,6 +119,8 @@ impl Member {
|
||||
}
|
||||
}
|
||||
|
||||
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
|
||||
|
||||
EventV1::ServerMemberJoin {
|
||||
id: server.id.clone(),
|
||||
user: user.id.clone(),
|
||||
@@ -134,6 +136,7 @@ impl Member {
|
||||
.into_iter()
|
||||
.map(|channel| channel.into())
|
||||
.collect(),
|
||||
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
|
||||
}
|
||||
.private(user.id.clone())
|
||||
.await;
|
||||
@@ -152,8 +155,7 @@ impl Member {
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Ok(channels)
|
||||
Ok(())
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
/// Update member data
|
||||
@@ -213,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, 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")]
|
||||
@@ -131,8 +132,52 @@ 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
|
||||
@@ -208,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 {
|
||||
@@ -450,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(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ use crate::{events::client::EventV1, Database, File, RatelimitEvent};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_result::{create_error, Error, ErrorType, Result};
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0;
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::{create_error, Result};
|
||||
use ulid::Ulid;
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -93,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>,
|
||||
@@ -182,6 +187,28 @@ impl User {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -196,6 +223,18 @@ impl User {
|
||||
.is_empty())
|
||||
}
|
||||
|
||||
/// Check if this user can acquire another server
|
||||
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
|
||||
let config = config().await;
|
||||
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(TooManyServers {
|
||||
max: config.features.limits.default.servers
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitise and validate a username can be used
|
||||
pub fn validate_username(username: String) -> Result<String> {
|
||||
// Copy the username for validation
|
||||
@@ -227,7 +266,27 @@ impl User {
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
// Find a free discriminator for a given username
|
||||
/// 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,
|
||||
@@ -313,16 +372,164 @@ 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),
|
||||
/// 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 => {
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
RelationshipStatus::Friend,
|
||||
RelationshipStatus::Friend,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RelationshipStatus::None => {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,7 +565,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 => {
|
||||
@@ -368,7 +575,7 @@ impl User {
|
||||
}
|
||||
FieldsUser::ProfileContent => {
|
||||
if let Some(x) = self.profile.as_mut() {
|
||||
x.content = String::new();
|
||||
x.content = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::ProfileBackground => {
|
||||
|
||||
@@ -14,7 +14,7 @@ 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>;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
use crate::{ReferenceDb, Relationship};
|
||||
|
||||
use super::AbstractUsers;
|
||||
|
||||
@@ -28,12 +28,14 @@ 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))
|
||||
}
|
||||
@@ -106,19 +108,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);
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ pub async fn generate(
|
||||
return Err(create_error!(LabelMe));
|
||||
}
|
||||
|
||||
// ! FIXME: batch request to january
|
||||
// TODO: batch request to january
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for link in links {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
@@ -275,6 +279,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 {
|
||||
@@ -385,7 +398,20 @@ 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,
|
||||
}
|
||||
}
|
||||
@@ -409,6 +435,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 {
|
||||
@@ -464,6 +513,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 {
|
||||
@@ -540,6 +600,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 {
|
||||
@@ -550,6 +622,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 {
|
||||
@@ -561,6 +643,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 {
|
||||
@@ -593,36 +686,78 @@ impl From<crate::FieldsRole> for FieldsRole {
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::User {
|
||||
pub async fn into<P>(self, perspective: P) -> User
|
||||
where
|
||||
P: Into<Option<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()
|
||||
} else {
|
||||
RelationshipStatus::None
|
||||
};
|
||||
impl From<FieldsRole> for crate::FieldsRole {
|
||||
fn from(value: FieldsRole) -> Self {
|
||||
match value {
|
||||
FieldsRole::Colour => crate::FieldsRole::Colour,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do permission stuff here
|
||||
// TODO: implement permissions =)
|
||||
let can_see_profile = false;
|
||||
impl crate::User {
|
||||
pub async fn into<'a, P>(self, db: &Database, perspective: P) -> 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 {
|
||||
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, false)
|
||||
};
|
||||
|
||||
User {
|
||||
username: self.username,
|
||||
discriminator: self.discriminator,
|
||||
display_name: self.display_name,
|
||||
avatar: self.avatar.map(|file| file.into()),
|
||||
relations: vec![],
|
||||
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,
|
||||
status: None,
|
||||
profile: None,
|
||||
status: if can_see_profile {
|
||||
self.status.map(|status| status.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
profile: if can_see_profile {
|
||||
self.profile.map(|profile| profile.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
@@ -632,6 +767,75 @@ impl crate::User {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
status: if can_see_profile {
|
||||
self.status.map(|status| status.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
profile: if can_see_profile {
|
||||
self.profile.map(|profile| profile.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && is_online,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn into_self(self) -> User {
|
||||
User {
|
||||
username: self.username,
|
||||
@@ -658,6 +862,13 @@ impl crate::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<crate::PartialUser> for PartialUser {
|
||||
@@ -745,6 +956,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 {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use revolt_result::{create_error, Error, Result};
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use revolt_result::Error;
|
||||
|
||||
use async_std::sync::Mutex;
|
||||
use once_cell::sync::Lazy;
|
||||
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};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -42,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,
|
||||
@@ -71,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;
|
||||
|
||||
@@ -2,3 +2,4 @@ pub mod bridge;
|
||||
pub mod idempotency;
|
||||
pub mod permissions;
|
||||
pub mod reference;
|
||||
pub mod test_fixtures;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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::{Channel, Database, Member, Server, User};
|
||||
@@ -19,7 +20,8 @@ pub struct DatabasePermissionQuery<'a> {
|
||||
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>,
|
||||
}
|
||||
|
||||
@@ -49,6 +51,10 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
|
||||
/// 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 {
|
||||
@@ -82,14 +88,17 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
|
||||
|
||||
/// Do we have a mutual connection with the currently selected user?
|
||||
async fn have_mutual_connection(&mut self) -> bool {
|
||||
if let Some(user) = &self.user {
|
||||
// TODO: cache result?
|
||||
matches!(
|
||||
self.perspective
|
||||
.has_mutual_connection(self.database, &user.id)
|
||||
.await,
|
||||
Ok(true)
|
||||
)
|
||||
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
|
||||
}
|
||||
@@ -111,11 +120,15 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
|
||||
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 {
|
||||
self.database
|
||||
.fetch_member(&server.id, &self.perspective.id)
|
||||
.await
|
||||
.is_ok()
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
@@ -196,7 +209,7 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
|
||||
match channel {
|
||||
Cow::Borrowed(Channel::Group { permissions, .. })
|
||||
| Cow::Owned(Channel::Group { permissions, .. }) => Override {
|
||||
allow: permissions.unwrap_or_default() as u64,
|
||||
allow: permissions.unwrap_or(*DEFAULT_PERMISSION_DIRECT_MESSAGE as i64) as u64,
|
||||
deny: 0,
|
||||
},
|
||||
Cow::Borrowed(Channel::TextChannel {
|
||||
@@ -331,6 +344,23 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
|
||||
| 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));
|
||||
}
|
||||
@@ -352,6 +382,7 @@ impl<'a> DatabasePermissionQuery<'a> {
|
||||
server: None,
|
||||
member: None,
|
||||
|
||||
cached_mutual_connection: None,
|
||||
cached_user_permission: None,
|
||||
cached_permission: None,
|
||||
}
|
||||
@@ -413,6 +444,33 @@ impl<'a> DatabasePermissionQuery<'a> {
|
||||
..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
|
||||
|
||||
@@ -7,7 +7,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 +24,61 @@ 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
|
||||
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
||||
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
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -9,30 +9,34 @@ 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-config = { version = "0.6.8", path = "../config" }
|
||||
revolt-permissions = { version = "0.6.8", path = "../permissions" }
|
||||
revolt-config = { version = "0.7.0", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.0", 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
|
||||
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"] }
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use super::User;
|
||||
|
||||
use validator::Validate;
|
||||
|
||||
auto_derived!(
|
||||
/// Bot
|
||||
#[derive(Default)]
|
||||
@@ -103,7 +101,7 @@ auto_derived!(
|
||||
|
||||
/// Bot Details
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct DataCreateBot {
|
||||
/// Bot username
|
||||
#[cfg_attr(
|
||||
@@ -115,7 +113,7 @@ auto_derived!(
|
||||
|
||||
/// New Bot Details
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
||||
pub struct DataEditBot {
|
||||
/// Bot username
|
||||
#[cfg_attr(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use super::{Channel, File, Server, User};
|
||||
|
||||
auto_derived!(
|
||||
/// Invite
|
||||
pub enum Invite {
|
||||
@@ -24,4 +26,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,11 +1,22 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use revolt_config::config;
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::{FromForm, FromFormField};
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
use super::{Embed, File, MessageWebhook, User, Webhook};
|
||||
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
|
||||
@@ -58,6 +69,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 {
|
||||
@@ -86,17 +115,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>,
|
||||
}
|
||||
|
||||
@@ -124,6 +157,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]
|
||||
@@ -152,6 +186,148 @@ auto_derived!(
|
||||
/// 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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
/// Reason for reporting content (message or server)
|
||||
pub enum ContentReportReason {
|
||||
/// No reason has been specified
|
||||
NoneSpecified,
|
||||
|
||||
/// Illegal content catch-all reason
|
||||
Illegal,
|
||||
|
||||
/// Selling or facilitating use of drugs or other illegal goods
|
||||
IllegalGoods,
|
||||
|
||||
/// Extortion or blackmail
|
||||
IllegalExtortion,
|
||||
|
||||
/// Revenge or child pornography
|
||||
IllegalPornography,
|
||||
|
||||
/// Illegal hacking activity
|
||||
IllegalHacking,
|
||||
|
||||
/// Extreme violence, gore, or animal cruelty
|
||||
/// With exception to violence potrayed in media / creative arts
|
||||
ExtremeViolence,
|
||||
|
||||
/// Content that promotes harm to others / self
|
||||
PromotesHarm,
|
||||
|
||||
/// Unsolicited advertisements
|
||||
UnsolicitedSpam,
|
||||
|
||||
/// This is a raid
|
||||
Raid,
|
||||
|
||||
/// Spam or platform abuse
|
||||
SpamAbuse,
|
||||
|
||||
/// Scams or fraud
|
||||
ScamsFraud,
|
||||
|
||||
/// Distribution of malware or malicious links
|
||||
Malware,
|
||||
|
||||
/// Harassment or abuse targeted at another user
|
||||
Harassment,
|
||||
}
|
||||
|
||||
/// Reason for reporting a user
|
||||
pub enum UserReportReason {
|
||||
/// No reason has been specified
|
||||
NoneSpecified,
|
||||
|
||||
/// Unsolicited advertisements
|
||||
UnsolicitedSpam,
|
||||
|
||||
/// User is sending spam or otherwise abusing the platform
|
||||
SpamAbuse,
|
||||
|
||||
/// User's profile contains inappropriate content for a general audience
|
||||
InappropriateProfile,
|
||||
|
||||
/// User is impersonating another user
|
||||
Impersonation,
|
||||
|
||||
/// User is evading a ban
|
||||
BanEvasion,
|
||||
|
||||
/// User is not of minimum age to use the platform
|
||||
Underage,
|
||||
}
|
||||
|
||||
/// The content being reported
|
||||
#[serde(tag = "type")]
|
||||
pub enum ReportedContent {
|
||||
/// Report a message
|
||||
Message {
|
||||
/// ID of the message
|
||||
id: String,
|
||||
/// Reason for reporting message
|
||||
report_reason: ContentReportReason,
|
||||
},
|
||||
/// Report a server
|
||||
Server {
|
||||
/// ID of the server
|
||||
id: String,
|
||||
/// Reason for reporting server
|
||||
report_reason: ContentReportReason,
|
||||
},
|
||||
/// Report a user
|
||||
User {
|
||||
/// ID of the user
|
||||
id: String,
|
||||
/// Reason for reporting a user
|
||||
report_reason: UserReportReason,
|
||||
/// Message context
|
||||
message_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Status of the report
|
||||
#[serde(tag = "status")]
|
||||
pub enum ReportStatus {
|
||||
/// Report is waiting for triage / action
|
||||
Created {},
|
||||
|
||||
/// Report was rejected
|
||||
Rejected {
|
||||
rejection_reason: String,
|
||||
closed_at: Option<Timestamp>,
|
||||
},
|
||||
|
||||
/// Report was actioned and resolved
|
||||
Resolved { closed_at: Option<Timestamp> },
|
||||
}
|
||||
|
||||
/// Just the status of the report
|
||||
pub enum ReportStatusString {
|
||||
/// Report is waiting for triage / action
|
||||
Created,
|
||||
|
||||
/// Report was rejected
|
||||
Rejected,
|
||||
|
||||
/// Report was actioned and resolved
|
||||
Resolved,
|
||||
}
|
||||
);
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::MemberCompositeKey;
|
||||
use super::{File, MemberCompositeKey, User};
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
auto_derived!(
|
||||
/// Server Ban
|
||||
@@ -9,4 +12,44 @@ auto_derived!(
|
||||
/// Reason for ban creation
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Information for new server ban
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataBanCreate {
|
||||
/// Ban reason
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Just enough information to list a ban
|
||||
pub struct BannedUser {
|
||||
/// Id of the banned user
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
pub id: String,
|
||||
/// Username of the banned user
|
||||
pub username: String,
|
||||
/// Discriminator of the banned user
|
||||
pub discriminator: String,
|
||||
/// Avatar of the banned user
|
||||
pub avatar: Option<File>,
|
||||
}
|
||||
|
||||
/// Ban list result
|
||||
pub struct BanListResult {
|
||||
/// Users objects
|
||||
pub users: Vec<BannedUser>,
|
||||
/// Ban objects
|
||||
pub bans: Vec<ServerBan>,
|
||||
}
|
||||
);
|
||||
|
||||
impl From<User> for BannedUser {
|
||||
fn from(user: User) -> Self {
|
||||
BannedUser {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
discriminator: user.discriminator,
|
||||
avatar: user.avatar,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
use super::File;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{File, Role, User};
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::FromForm;
|
||||
|
||||
/// Regex for valid role colours
|
||||
///
|
||||
/// Allows the use of named colours, rgb(a), variables and all gradients.
|
||||
///
|
||||
/// Flags:
|
||||
/// - Case-insensitive (`i`)
|
||||
///
|
||||
/// Source:
|
||||
/// ```regex
|
||||
/// VALUE = [a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+
|
||||
/// ADDITIONAL_VALUE = \d+deg
|
||||
/// STOP = ([ ]+(\d{1,3}%|0))?
|
||||
///
|
||||
/// ^(?:VALUE|(repeating-)?(linear|conic|radial)-gradient\((VALUE|ADDITIONAL_VALUE)STOP(,[ ]*(VALUE)STOP)+\))$
|
||||
/// ```
|
||||
pub static RE_COLOUR: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)^(?:[a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|(repeating-)?(linear|conic|radial)-gradient\(([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|\d+deg)([ ]+(\d{1,3}%|0))?(,[ ]*([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+)([ ]+(\d{1,3}%|0))?)+\))$").unwrap()
|
||||
});
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server Member
|
||||
@@ -56,4 +85,46 @@ auto_derived!(
|
||||
Kick,
|
||||
Ban,
|
||||
}
|
||||
|
||||
/// Member response
|
||||
#[serde(untagged)]
|
||||
pub enum MemberResponse {
|
||||
Member(Member),
|
||||
MemberWithRoles {
|
||||
member: Member,
|
||||
roles: HashMap<String, Role>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Options for fetching all members
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsFetchAllMembers {
|
||||
/// Whether to exclude offline users
|
||||
pub exclude_offline: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response with all members
|
||||
pub struct AllMemberResponse {
|
||||
/// List of members
|
||||
pub members: Vec<Member>,
|
||||
/// List of users
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
/// New member information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataMemberEdit {
|
||||
/// Member nickname
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub nickname: Option<String>,
|
||||
/// Attachment Id to set for avatar
|
||||
pub avatar: Option<String>,
|
||||
/// Array of role ids
|
||||
pub roles: Option<Vec<String>>,
|
||||
/// Timestamp this member is timed out until
|
||||
pub timeout: Option<Timestamp>,
|
||||
/// Fields to remove from channel object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsMember>>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use super::File;
|
||||
use super::{Channel, File, RE_COLOUR};
|
||||
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_permissions::{Override, OverrideField};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::FromForm;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server
|
||||
pub struct Server {
|
||||
@@ -19,7 +25,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
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
@@ -117,10 +123,13 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// Channel category
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct Category {
|
||||
/// Unique ID for this category
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub id: String,
|
||||
/// Title for this category
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub title: String,
|
||||
/// Channels in this category
|
||||
pub channels: Vec<String>,
|
||||
@@ -141,4 +150,140 @@ auto_derived!(
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub user_banned: Option<String>,
|
||||
}
|
||||
|
||||
/// Information about new server to create
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataCreateServer {
|
||||
/// Server name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: String,
|
||||
/// Server description
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
|
||||
pub description: Option<String>,
|
||||
/// Whether this server is age-restricted
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
/// Information about new role to create
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataCreateRole {
|
||||
/// Role name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: String,
|
||||
/// Ranking position
|
||||
///
|
||||
/// Smaller values take priority.
|
||||
pub rank: Option<i64>,
|
||||
}
|
||||
|
||||
/// Response after creating new role
|
||||
pub struct NewRoleResponse {
|
||||
/// Id of the role
|
||||
pub id: String,
|
||||
/// New role
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
/// Information returned when creating server
|
||||
pub struct CreateServerLegacyResponse {
|
||||
/// Server object
|
||||
pub server: Server,
|
||||
/// Default channels
|
||||
pub channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
/// Options when fetching server
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsFetchServer {
|
||||
/// Whether to include channels
|
||||
pub include_channels: Option<bool>,
|
||||
}
|
||||
|
||||
/// Fetch server information
|
||||
#[serde(untagged)]
|
||||
pub enum FetchServerResponse {
|
||||
JustServer(Server),
|
||||
ServerWithChannels {
|
||||
#[serde(flatten)]
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
},
|
||||
}
|
||||
|
||||
/// New server information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditServer {
|
||||
/// Server name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: Option<String>,
|
||||
/// Server description
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Attachment Id for icon
|
||||
pub icon: Option<String>,
|
||||
/// Attachment Id for banner
|
||||
pub banner: Option<String>,
|
||||
|
||||
/// Category structure for server
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub categories: Option<Vec<Category>>,
|
||||
/// System message configuration
|
||||
pub system_messages: Option<SystemMessageChannels>,
|
||||
|
||||
/// Bitfield of server flags
|
||||
#[cfg_attr(feature = "validator", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub flags: Option<i32>,
|
||||
|
||||
// Whether this server is age-restricted
|
||||
// nsfw: Option<bool>,
|
||||
/// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg)
|
||||
pub discoverable: Option<bool>,
|
||||
/// Whether analytics should be collected for this server
|
||||
///
|
||||
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
|
||||
pub analytics: Option<bool>,
|
||||
|
||||
/// Fields to remove from server object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsServer>>,
|
||||
}
|
||||
|
||||
/// New role information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditRole {
|
||||
/// Role name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: Option<String>,
|
||||
/// Role colour
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
|
||||
)]
|
||||
pub colour: Option<String>,
|
||||
/// Whether this role should be displayed separately
|
||||
pub hoist: Option<bool>,
|
||||
/// Ranking position
|
||||
///
|
||||
/// Smaller values take priority.
|
||||
pub rank: Option<i64>,
|
||||
/// Fields to remove from role object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsRole>>,
|
||||
}
|
||||
|
||||
/// New role permissions
|
||||
pub struct DataSetServerRolePermission {
|
||||
/// Allow / deny values for the role in this server.
|
||||
pub permissions: Override,
|
||||
}
|
||||
|
||||
/// Options when leaving a server
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsServerDelete {
|
||||
/// Whether to not send a leave message
|
||||
pub leave_silently: Option<bool>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::FromForm;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// HashMap of user settings
|
||||
/// Each key is mapped to a tuple consisting of the
|
||||
/// revision timestamp and serialised data (in JSON format)
|
||||
pub type UserSettings = HashMap<String, (i64, String)>;
|
||||
|
||||
auto_derived!(
|
||||
/// Options for fetching settings
|
||||
pub struct OptionsFetchSettings {
|
||||
/// Keys to fetch
|
||||
pub keys: Vec<String>,
|
||||
}
|
||||
|
||||
/// Additional options for inserting settings
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsSetSettings {
|
||||
/// Timestamp of settings change.
|
||||
///
|
||||
/// Used to avoid feedback loops.
|
||||
pub timestamp: Option<i64>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,12 +3,21 @@ use regex::Regex;
|
||||
|
||||
use super::File;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
/// Regex for valid usernames
|
||||
///
|
||||
/// Block zero width space
|
||||
/// Block lookalike characters
|
||||
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
|
||||
|
||||
/// Regex for valid display names
|
||||
///
|
||||
/// Block zero width space
|
||||
/// Block newline and carriage return
|
||||
pub static RE_DISPLAY_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap());
|
||||
|
||||
auto_derived_partial!(
|
||||
/// User
|
||||
pub struct User {
|
||||
@@ -123,20 +132,26 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// User's active status
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct UserStatus {
|
||||
/// Custom status text
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
|
||||
pub text: String,
|
||||
#[validate(length(min = 0, max = 128))]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub text: Option<String>,
|
||||
/// Current presence option
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub presence: Option<Presence>,
|
||||
}
|
||||
|
||||
/// User's profile
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct UserProfile {
|
||||
/// Text content on user's profile
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
|
||||
pub content: String,
|
||||
#[validate(length(min = 0, max = 2000))]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub content: Option<String>,
|
||||
/// Background visible on user's profile
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub background: Option<File>,
|
||||
@@ -182,12 +197,79 @@ auto_derived!(
|
||||
Spam = 8,
|
||||
}
|
||||
|
||||
/// New user profile data
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataUserProfile {
|
||||
/// Text to set as user profile description
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub content: Option<String>,
|
||||
/// Attachment Id for background
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub background: Option<String>,
|
||||
}
|
||||
|
||||
/// New user information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditUser {
|
||||
/// New display name
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 2, max = 32), regex = "RE_DISPLAY_NAME")
|
||||
)]
|
||||
pub display_name: Option<String>,
|
||||
/// Attachment Id for avatar
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub avatar: Option<String>,
|
||||
|
||||
/// New user status
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub status: Option<UserStatus>,
|
||||
/// New user profile data
|
||||
///
|
||||
/// This is applied as a partial.
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub profile: Option<DataUserProfile>,
|
||||
|
||||
/// Bitfield of user badges
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub badges: Option<i32>,
|
||||
/// Enum of user flags
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub flags: Option<i32>,
|
||||
|
||||
/// Fields to remove from user object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsUser>>,
|
||||
}
|
||||
|
||||
/// User flag reponse
|
||||
pub struct FlagResponse {
|
||||
/// Flags
|
||||
pub flags: i32,
|
||||
}
|
||||
|
||||
/// Mutual friends and servers response
|
||||
pub struct MutualResponse {
|
||||
/// Array of mutual user IDs that both users are friends with
|
||||
pub users: Vec<String>,
|
||||
/// Array of mutual server IDs that both users are in
|
||||
pub servers: Vec<String>,
|
||||
}
|
||||
|
||||
/// Bot information for if the user is a bot
|
||||
pub struct BotInformation {
|
||||
/// Id of the owner of this bot
|
||||
#[cfg_attr(feature = "serde", serde(rename = "owner"))]
|
||||
pub owner_id: String,
|
||||
}
|
||||
|
||||
/// User lookup information
|
||||
pub struct DataSendFriendRequest {
|
||||
/// Username and discriminator combo separated by #
|
||||
pub username: String,
|
||||
}
|
||||
);
|
||||
|
||||
pub trait CheckRelationship {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-permissions"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.6.5", path = "../result" }
|
||||
revolt-result = { version = "0.7.0", path = "../result" }
|
||||
|
||||
# Utility
|
||||
auto_ops = "0.3.0"
|
||||
|
||||
@@ -5,38 +5,44 @@ use crate::{
|
||||
};
|
||||
|
||||
/// Calculate permissions against a user
|
||||
pub async fn calculate_user_permissions<P: PermissionQuery>(query: &mut P) -> u32 {
|
||||
pub async fn calculate_user_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
|
||||
if query.are_we_privileged().await {
|
||||
return u32::MAX;
|
||||
return u64::MAX.into();
|
||||
}
|
||||
|
||||
if query.are_the_users_same().await {
|
||||
return u32::MAX;
|
||||
return u64::MAX.into();
|
||||
}
|
||||
|
||||
let mut permissions = 0_u32;
|
||||
let mut permissions = 0_u64;
|
||||
match query.user_relationship().await {
|
||||
RelationshipStatus::Friend => return u32::MAX,
|
||||
RelationshipStatus::Friend => return u64::MAX.into(),
|
||||
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
|
||||
return UserPermission::Access as u32
|
||||
return (UserPermission::Access as u64).into()
|
||||
}
|
||||
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
|
||||
permissions = UserPermission::Access as u32;
|
||||
permissions = UserPermission::Access as u64;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if query.have_mutual_connection().await {
|
||||
permissions = UserPermission::Access + UserPermission::ViewProfile;
|
||||
permissions = UserPermission::Access as u64 + UserPermission::ViewProfile as u64;
|
||||
|
||||
if query.user_is_bot().await || query.are_we_a_bot().await {
|
||||
permissions += UserPermission::SendMessage as u32;
|
||||
permissions += UserPermission::SendMessage as u64;
|
||||
}
|
||||
|
||||
permissions
|
||||
permissions.into()
|
||||
} else {
|
||||
permissions
|
||||
permissions.into()
|
||||
}
|
||||
|
||||
// TODO: add boolean switch for permission for users to globally message a user
|
||||
// maybe an enum?
|
||||
// PrivacyLevel { Private, Friends, Mutual, Public, Global }
|
||||
|
||||
// TODO: add boolean switch for permission for users to mutually DM a user
|
||||
}
|
||||
|
||||
/// Calculate permissions against a server
|
||||
@@ -81,9 +87,7 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
|
||||
query.set_recipient_as_user().await;
|
||||
|
||||
let permissions = calculate_user_permissions(query).await;
|
||||
if (permissions & UserPermission::SendMessage as u32)
|
||||
== UserPermission::SendMessage as u32
|
||||
{
|
||||
if permissions.has_user_permission(UserPermission::SendMessage) {
|
||||
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
|
||||
} else {
|
||||
(*DEFAULT_PERMISSION_VIEW_ONLY).into()
|
||||
@@ -106,7 +110,9 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
|
||||
ChannelType::ServerChannel => {
|
||||
query.set_server_from_channel().await;
|
||||
|
||||
if query.are_we_a_member().await {
|
||||
if query.are_we_server_owner().await {
|
||||
ChannelPermission::GrantAllSafe.into()
|
||||
} else if query.are_we_a_member().await {
|
||||
let mut permissions = calculate_server_permissions(query).await;
|
||||
permissions.apply(query.get_default_channel_permissions().await);
|
||||
|
||||
@@ -121,7 +127,7 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
|
||||
if !permissions.has_channel_permission(ChannelPermission::ViewChannel) {
|
||||
permissions.revoke_all();
|
||||
}
|
||||
|
||||
|
||||
permissions
|
||||
} else {
|
||||
0_u64.into()
|
||||
|
||||
@@ -8,7 +8,7 @@ pub use server::*;
|
||||
pub use user::*;
|
||||
|
||||
/// Holds a permission value to manipulate.
|
||||
#[derive(Debug)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PermissionValue(u64);
|
||||
|
||||
impl PermissionValue {
|
||||
@@ -39,20 +39,33 @@ impl PermissionValue {
|
||||
}
|
||||
|
||||
/// Check whether certain a permission has been granted
|
||||
pub fn has(&mut self, v: u64) -> bool {
|
||||
pub fn has(&self, v: u64) -> bool {
|
||||
(self.0 & v) == v
|
||||
}
|
||||
|
||||
/// Check whether certain a channel permission has been granted
|
||||
pub fn has_channel_permission(&mut self, permission: ChannelPermission) -> bool {
|
||||
/// Check whether certain a user permission has been granted
|
||||
pub fn has_user_permission(&self, permission: UserPermission) -> bool {
|
||||
self.has(permission as u64)
|
||||
}
|
||||
|
||||
/// Check whether certain a channel permission has been granted
|
||||
pub fn has_channel_permission(&self, permission: ChannelPermission) -> bool {
|
||||
self.has(permission as u64)
|
||||
}
|
||||
|
||||
/// Throw if missing user permission
|
||||
pub fn throw_if_lacking_user_permission(&self, permission: UserPermission) -> Result<()> {
|
||||
if self.has_user_permission(permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(MissingPermission {
|
||||
permission: permission.to_string()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Throw if missing channel permission
|
||||
pub fn throw_if_lacking_channel_permission(
|
||||
&mut self,
|
||||
permission: ChannelPermission,
|
||||
) -> Result<()> {
|
||||
pub fn throw_if_lacking_channel_permission(&self, permission: ChannelPermission) -> Result<()> {
|
||||
if self.has_channel_permission(permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -61,6 +74,35 @@ impl PermissionValue {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Throw an error if we cannot grant permissions on either allows or denies
|
||||
/// going from the previous given value to the next given value.
|
||||
///
|
||||
/// We need to check any:
|
||||
/// - allows added (permissions now granted)
|
||||
/// - denies removed (permissions now neutral or granted)
|
||||
pub async fn throw_permission_override<C>(
|
||||
&self,
|
||||
current_value: C,
|
||||
next_value: &Override,
|
||||
) -> Result<()>
|
||||
where
|
||||
C: Into<Option<Override>>,
|
||||
{
|
||||
let current_value = current_value.into();
|
||||
|
||||
if let Some(current_value) = current_value {
|
||||
if !self.has(!current_value.allows() & next_value.allows())
|
||||
|| !self.has(current_value.denies() & !next_value.denies())
|
||||
{
|
||||
return Err(create_error!(CannotGiveMissingPermissions));
|
||||
}
|
||||
} else if !self.has(next_value.allows()) {
|
||||
return Err(create_error!(CannotGiveMissingPermissions));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for PermissionValue {
|
||||
|
||||
@@ -51,9 +51,9 @@ pub enum DataPermissionPoly {
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
pub struct OverrideField {
|
||||
/// Allow bit flags
|
||||
a: i64,
|
||||
pub a: i64,
|
||||
/// Disallow bit flags
|
||||
d: i64,
|
||||
pub d: i64,
|
||||
}
|
||||
|
||||
impl Override {
|
||||
|
||||
@@ -12,7 +12,7 @@ async fn validate_user_permissions() {
|
||||
let mut query = Scenario {};
|
||||
|
||||
let perms = calculate_user_permissions(&mut query).await;
|
||||
assert_eq!(perms, u32::MAX);
|
||||
assert!(perms.has(u64::MAX));
|
||||
|
||||
let perms = calculate_channel_permissions(&mut query).await;
|
||||
let value: u64 = perms.into();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-presence"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -221,7 +221,6 @@ mod tests {
|
||||
// Create a few more sessions
|
||||
let (first_session, second_session_id) = create_session(&user_id, 0).await;
|
||||
assert!(!first_session);
|
||||
dbg!(second_session_id);
|
||||
assert_eq!(second_session_id as u8 & 1, 0);
|
||||
|
||||
let (first_session, other_session_id) = create_session(&other_id, 0).await;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-result"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -60,6 +60,9 @@ pub enum ErrorType {
|
||||
TooManyAttachments {
|
||||
max: usize,
|
||||
},
|
||||
TooManyEmbeds {
|
||||
max: usize,
|
||||
},
|
||||
TooManyReplies {
|
||||
max: usize,
|
||||
},
|
||||
@@ -144,7 +147,7 @@ macro_rules! create_error {
|
||||
#[macro_export]
|
||||
macro_rules! create_database_error {
|
||||
( $operation: expr, $collection: expr ) => {
|
||||
create_error!(DatabaseError {
|
||||
$crate::create_error!(DatabaseError {
|
||||
operation: $operation.to_string(),
|
||||
collection: $collection.to_string()
|
||||
})
|
||||
|
||||
@@ -46,6 +46,7 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
ErrorType::AlreadyInServer => Status::Conflict,
|
||||
|
||||
ErrorType::TooManyServers { .. } => Status::BadRequest,
|
||||
ErrorType::TooManyEmbeds { .. } => Status::BadRequest,
|
||||
ErrorType::TooManyEmoji { .. } => Status::BadRequest,
|
||||
ErrorType::TooManyChannels { .. } => Status::BadRequest,
|
||||
ErrorType::TooManyRoles { .. } => Status::BadRequest,
|
||||
|
||||
+10
-6
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.6.8"
|
||||
version = "0.7.0"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
@@ -36,6 +36,7 @@ nanoid = "0.4.0"
|
||||
serde_json = "1.0.57"
|
||||
serde = { version = "1.0.115", features = ["derive"] }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
iso8601-timestamp = { version = "0.2.11", features = [] }
|
||||
|
||||
# async
|
||||
futures = "0.3.8"
|
||||
@@ -57,22 +58,25 @@ rocket = { version = "0.5.0-rc.2", default-features = false, features = [
|
||||
] }
|
||||
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" }
|
||||
rocket_empty = { version = "0.1.1", features = ["schema"] }
|
||||
rocket_authifier = { version = "1.0.7" }
|
||||
rocket_authifier = { version = "1.0.8" }
|
||||
rocket_prometheus = "0.10.0-rc.3"
|
||||
|
||||
# spec generation
|
||||
schemars = "0.8.8"
|
||||
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# core
|
||||
authifier = "1.0.8"
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database", features = [
|
||||
"rocket-impl",
|
||||
"redis-is-patched",
|
||||
] }
|
||||
revolt-models = { path = "../core/models", features = ["schemas", "validator"] }
|
||||
revolt-models = { path = "../core/models", features = [
|
||||
"schemas",
|
||||
"validator",
|
||||
"rocket",
|
||||
] }
|
||||
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
|
||||
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
|
||||
|
||||
|
||||
+100
-16
@@ -8,6 +8,9 @@ extern crate serde_json;
|
||||
pub mod routes;
|
||||
pub mod util;
|
||||
|
||||
use revolt_config::config;
|
||||
use revolt_database::events::client::EventV1;
|
||||
use revolt_database::{Database, MongoDb};
|
||||
use rocket::{Build, Rocket};
|
||||
use rocket_cors::{AllowedOrigins, CorsOptions};
|
||||
use rocket_prometheus::PrometheusMetrics;
|
||||
@@ -15,26 +18,37 @@ use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
|
||||
use async_std::channel::unbounded;
|
||||
use revolt_quark::authifier::{Authifier, AuthifierEvent};
|
||||
use revolt_quark::events::client::EventV1;
|
||||
use revolt_quark::DatabaseInfo;
|
||||
use authifier::config::{
|
||||
Captcha, Config as AuthifierConfig, EmailVerificationConfig, ResolveIp, SMTPSettings, Shield,
|
||||
Template, Templates,
|
||||
};
|
||||
use authifier::{Authifier, AuthifierEvent};
|
||||
use rocket::data::ToByteUnit;
|
||||
|
||||
pub async fn web() -> Rocket<Build> {
|
||||
// Get settings
|
||||
let config = config().await;
|
||||
|
||||
// Ensure environment variables are present
|
||||
config.preflight_checks();
|
||||
|
||||
// Setup database
|
||||
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Legacy database setup from quark
|
||||
let legacy_db = DatabaseInfo::Auto.connect().await.unwrap();
|
||||
|
||||
// Setup Authifier event channel
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
// Setup Authifier
|
||||
let authifier = Authifier {
|
||||
database: db.clone().into(),
|
||||
config: revolt_quark::util::authifier::config(),
|
||||
database: match db.clone() {
|
||||
Database::Reference(_) => Default::default(),
|
||||
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: Default::default(),
|
||||
// config: authifier_config().await,
|
||||
event_channel: Some(sender),
|
||||
};
|
||||
|
||||
@@ -55,8 +69,10 @@ pub async fn web() -> Rocket<Build> {
|
||||
});
|
||||
|
||||
// Launch background task workers
|
||||
async_std::task::spawn(revolt_database::tasks::start_workers(db.clone()));
|
||||
async_std::task::spawn(revolt_quark::tasks::start_workers(legacy_db.clone()));
|
||||
async_std::task::spawn(revolt_database::tasks::start_workers(
|
||||
db.clone(),
|
||||
authifier.database.clone(),
|
||||
));
|
||||
|
||||
// Configure CORS
|
||||
let cors = CorsOptions {
|
||||
@@ -85,7 +101,7 @@ pub async fn web() -> Rocket<Build> {
|
||||
let rocket = rocket::build();
|
||||
let prometheus = PrometheusMetrics::new();
|
||||
|
||||
routes::mount(rocket)
|
||||
routes::mount(config, rocket)
|
||||
.attach(prometheus.clone())
|
||||
.mount("/metrics", prometheus)
|
||||
.mount("/", rocket_cors::catch_all_options_routes())
|
||||
@@ -93,7 +109,6 @@ pub async fn web() -> Rocket<Build> {
|
||||
.mount("/swagger/", swagger)
|
||||
.manage(authifier)
|
||||
.manage(db)
|
||||
.manage(legacy_db)
|
||||
.manage(cors.clone())
|
||||
.attach(util::ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
@@ -104,13 +119,82 @@ pub async fn web() -> Rocket<Build> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn authifier_config() -> AuthifierConfig {
|
||||
let config = config().await;
|
||||
|
||||
let mut auth_config = AuthifierConfig {
|
||||
email_verification: if !config.api.smtp.host.is_empty() {
|
||||
EmailVerificationConfig::Enabled {
|
||||
smtp: SMTPSettings {
|
||||
from: config.api.smtp.from_address,
|
||||
host: config.api.smtp.host,
|
||||
username: config.api.smtp.username,
|
||||
password: config.api.smtp.password,
|
||||
reply_to: Some(
|
||||
config
|
||||
.api
|
||||
.smtp
|
||||
.reply_to
|
||||
.unwrap_or("support@revolt.chat".into()),
|
||||
),
|
||||
port: config.api.smtp.port,
|
||||
use_tls: config.api.smtp.use_tls,
|
||||
},
|
||||
expiry: Default::default(),
|
||||
templates: Templates {
|
||||
verify: Template {
|
||||
title: "Verify your Revolt account.".into(),
|
||||
text: include_str!("templates/verify.txt").into(),
|
||||
url: format!("{}/login/verify/", config.hosts.app),
|
||||
html: Some(include_str!("templates/verify.html").into()),
|
||||
},
|
||||
reset: Template {
|
||||
title: "Reset your Revolt password.".into(),
|
||||
text: include_str!("templates/reset.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: Some(include_str!("templates/reset.html").into()),
|
||||
},
|
||||
deletion: Template {
|
||||
title: "Confirm account deletion.".into(),
|
||||
text: include_str!("templates/deletion.txt").into(),
|
||||
url: format!("{}/delete/", config.hosts.app),
|
||||
html: Some(include_str!("templates/deletion.html").into()),
|
||||
},
|
||||
welcome: None,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
EmailVerificationConfig::Disabled
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
auth_config.invite_only = config.api.registration.invite_only;
|
||||
|
||||
if !config.api.security.captcha.hcaptcha_key.is_empty() {
|
||||
auth_config.captcha = Captcha::HCaptcha {
|
||||
secret: config.api.security.captcha.hcaptcha_key,
|
||||
};
|
||||
}
|
||||
|
||||
if !config.api.security.authifier_shield_key.is_empty() {
|
||||
auth_config.shield = Shield::Enabled {
|
||||
api_key: config.api.security.authifier_shield_key,
|
||||
strict: false,
|
||||
};
|
||||
}
|
||||
|
||||
if config.api.security.trust_cloudflare {
|
||||
auth_config.resolve_ip = ResolveIp::Cloudflare;
|
||||
}
|
||||
|
||||
auth_config
|
||||
}
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
// Configure logging and environment
|
||||
revolt_quark::configure!();
|
||||
|
||||
// Ensure environment variables are present
|
||||
revolt_quark::variables::delta::preflight_checks();
|
||||
revolt_config::configure!();
|
||||
|
||||
// Start web server
|
||||
web().await
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
mod stats;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![stats::stats]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use revolt_quark::models::stats::Stats;
|
||||
use revolt_quark::{Db, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Query Stats
|
||||
///
|
||||
/// Fetch various technical statistics.
|
||||
#[openapi(tag = "Admin")]
|
||||
#[get("/stats")]
|
||||
pub async fn stats(db: &Db) -> Result<Json<Stats>> {
|
||||
Ok(Json(db.generate_stats().await?))
|
||||
}
|
||||
@@ -48,7 +48,7 @@ mod test {
|
||||
drop(response);
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(|event| match event {
|
||||
.wait_for_event(&bot.id, |event| match event {
|
||||
EventV1::UserUpdate { id, .. } => id == &bot.id,
|
||||
_ => false,
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ pub async fn fetch_bot(
|
||||
}
|
||||
|
||||
Ok(Json(FetchBotResponse {
|
||||
user: db.fetch_user(&bot.id).await?.into(None).await,
|
||||
user: db.fetch_user(&bot.id).await?.into(db, None).await,
|
||||
bot: bot.into(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ pub async fn invite_bot(
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?;
|
||||
|
||||
Member::create(db, &server, &bot_user)
|
||||
Member::create(db, &server, &bot_user, None)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ pub async fn invite_bot(
|
||||
mod test {
|
||||
use crate::{rocket, util::test::TestHarness};
|
||||
use revolt_database::{events::client::EventV1, Bot, Channel, Server};
|
||||
use revolt_models::v0;
|
||||
use revolt_models::v0::{self, DataCreateServer};
|
||||
use rocket::http::{ContentType, Header, Status};
|
||||
|
||||
#[rocket::async_test]
|
||||
@@ -78,20 +78,16 @@ mod test {
|
||||
.await
|
||||
.expect("`Bot`");
|
||||
|
||||
// FIXME: Channel::create_group
|
||||
let group = Channel::Group {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
name: TestHarness::rand_string(),
|
||||
owner: user.id.to_string(),
|
||||
description: None,
|
||||
last_message_id: None,
|
||||
icon: None,
|
||||
nsfw: false,
|
||||
permissions: None,
|
||||
recipients: vec![user.id.to_string()],
|
||||
};
|
||||
|
||||
group.create(&harness.db).await.unwrap();
|
||||
let group = Channel::create_group(
|
||||
&harness.db,
|
||||
v0::DataCreateGroup {
|
||||
name: TestHarness::rand_string(),
|
||||
..Default::default()
|
||||
},
|
||||
user.id.to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
@@ -106,7 +102,7 @@ mod test {
|
||||
drop(response);
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(|event| match event {
|
||||
.wait_for_event(&group.id(), |event| match event {
|
||||
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
|
||||
_ => false,
|
||||
})
|
||||
@@ -129,26 +125,17 @@ mod test {
|
||||
.await
|
||||
.expect("`Bot`");
|
||||
|
||||
// FIXME: Server::create_server
|
||||
let server = Server {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
name: TestHarness::rand_string(),
|
||||
owner: user.id.to_string(),
|
||||
analytics: false,
|
||||
discoverable: false,
|
||||
nsfw: false,
|
||||
banner: None,
|
||||
icon: None,
|
||||
categories: None,
|
||||
channels: vec![],
|
||||
default_permissions: 0,
|
||||
description: None,
|
||||
flags: None,
|
||||
roles: Default::default(),
|
||||
system_messages: None,
|
||||
};
|
||||
|
||||
server.create(&harness.db).await.unwrap();
|
||||
let (server, _) = Server::create(
|
||||
&harness.db,
|
||||
DataCreateServer {
|
||||
name: TestHarness::rand_string(),
|
||||
..Default::default()
|
||||
},
|
||||
&user,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
@@ -168,7 +155,7 @@ mod test {
|
||||
drop(response);
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(|event| match event {
|
||||
.wait_for_event(&server.id, |event| match event {
|
||||
EventV1::ServerMemberJoin { id, .. } => id == &server.id,
|
||||
_ => false,
|
||||
})
|
||||
|
||||
@@ -1,23 +1,84 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, User,
|
||||
};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Acknowledge Message
|
||||
///
|
||||
/// Lets the server and all other clients know that we've seen this message id in this channel.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[put("/<target>/ack/<message>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, message: Ref) -> Result<EmptyResponse> {
|
||||
pub async fn ack(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
message: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(db, Permission::ViewChannel)
|
||||
.await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||
|
||||
channel
|
||||
.ack(&user.id, &message.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{rocket, util::test::TestHarness};
|
||||
use revolt_database::{events::client::EventV1, Channel};
|
||||
use revolt_models::v0::DataCreateGroup;
|
||||
use rocket::http::{Header, Status};
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn success_ack_channel() {
|
||||
let mut harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
|
||||
let group = Channel::create_group(
|
||||
&harness.db,
|
||||
DataCreateGroup {
|
||||
..Default::default()
|
||||
},
|
||||
user.id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("`Channel`");
|
||||
|
||||
let message_id = ulid::Ulid::new().to_string();
|
||||
let response = harness
|
||||
.client
|
||||
.put(format!("/channels/{}/ack/{}", group.id(), message_id))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::NoContent);
|
||||
drop(response);
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(&format!("{}!", user.id), |event| match event {
|
||||
EventV1::ChannelAck { id, .. } => id == &group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
match event {
|
||||
EventV1::ChannelAck {
|
||||
message_id: m_id, ..
|
||||
} => assert_eq!(m_id, message_id),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
use revolt_quark::{
|
||||
models::{channel::PartialChannel, Channel, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, PartialChannel, User,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
|
||||
pub struct OptionsChannelDelete {
|
||||
/// Whether to not send a leave message
|
||||
leave_silently: Option<bool>,
|
||||
}
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Close Channel
|
||||
///
|
||||
/// Deletes a server channel, leaves a group or closes a group.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[delete("/<target>?<options..>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
pub async fn delete(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: OptionsChannelDelete,
|
||||
target: Reference,
|
||||
options: v0::OptionsChannelDelete,
|
||||
) -> Result<EmptyResponse> {
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
let mut perms = perms(&user).channel(&channel);
|
||||
perms.throw_permission(db, Permission::ViewChannel).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
let permissions = calculate_channel_permissions(&mut query).await;
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||
|
||||
match &channel {
|
||||
Channel::SavedMessages { .. } => Err(Error::NoEffect),
|
||||
Channel::SavedMessages { .. } => Err(create_error!(NoEffect)),
|
||||
Channel::DirectMessage { .. } => channel
|
||||
.update(
|
||||
db,
|
||||
@@ -41,20 +39,58 @@ pub async fn req(
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::Group { .. } => channel
|
||||
.remove_user_from_group(
|
||||
db,
|
||||
&user.id,
|
||||
None,
|
||||
options.leave_silently.unwrap_or_default(),
|
||||
)
|
||||
.remove_user_from_group(db, &user, None, options.leave_silently.unwrap_or_default())
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
|
||||
perms
|
||||
.throw_permission(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
||||
channel.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{rocket, util::test::TestHarness};
|
||||
use revolt_database::{events::client::EventV1, Channel};
|
||||
use revolt_models::v0::DataCreateGroup;
|
||||
use rocket::http::{Header, Status};
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn success_delete_group() {
|
||||
let mut harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
|
||||
let group = Channel::create_group(
|
||||
&harness.db,
|
||||
DataCreateGroup {
|
||||
..Default::default()
|
||||
},
|
||||
user.id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("`Channel`");
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
.delete(format!("/channels/{}", group.id()))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::NoContent);
|
||||
drop(response);
|
||||
|
||||
harness
|
||||
.wait_for_event(&group.id(), |event| match event {
|
||||
EventV1::ChannelDelete { id, .. } => id == &group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// TEST: member leaves group (no delete)
|
||||
// TEST: no effect with saved messages
|
||||
// TEST: DM set to inactive
|
||||
// TEST: server channel deleted
|
||||
}
|
||||
|
||||
@@ -1,60 +1,36 @@
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
channel::{Channel, FieldsChannel, PartialChannel},
|
||||
message::SystemMessage,
|
||||
File, User,
|
||||
},
|
||||
perms, Database, Error, Permission, Ref, Result,
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, File, PartialChannel, SystemMessage, User,
|
||||
};
|
||||
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Channel Details
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataEditChannel {
|
||||
/// Channel name
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
name: Option<String>,
|
||||
/// Channel description
|
||||
#[validate(length(min = 0, max = 1024))]
|
||||
description: Option<String>,
|
||||
/// Group owner
|
||||
owner: Option<String>,
|
||||
/// Icon
|
||||
///
|
||||
/// Provide an Autumn attachment Id.
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
icon: Option<String>,
|
||||
/// Whether this channel is age-restricted
|
||||
nsfw: Option<bool>,
|
||||
/// Whether this channel is archived
|
||||
archived: Option<bool>,
|
||||
#[validate(length(min = 1))]
|
||||
remove: Option<Vec<FieldsChannel>>,
|
||||
}
|
||||
|
||||
/// # Edit Channel
|
||||
///
|
||||
/// Edit a channel object by its id.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[patch("/<target>", data = "<data>")]
|
||||
pub async fn req(
|
||||
pub async fn edit(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataEditChannel>,
|
||||
) -> Result<Json<Channel>> {
|
||||
target: Reference,
|
||||
data: Json<v0::DataEditChannel>,
|
||||
) -> Result<Json<v0::Channel>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
data.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
||||
|
||||
if data.name.is_none()
|
||||
&& data.description.is_none()
|
||||
@@ -63,7 +39,7 @@ pub async fn req(
|
||||
&& data.owner.is_none()
|
||||
&& data.remove.is_none()
|
||||
{
|
||||
return Ok(Json(channel));
|
||||
return Ok(Json(channel.into()));
|
||||
}
|
||||
|
||||
let mut partial: PartialChannel = Default::default();
|
||||
@@ -76,12 +52,12 @@ pub async fn req(
|
||||
{
|
||||
// Make sure we are the owner of this group
|
||||
if owner != &user.id {
|
||||
return Err(Error::NotOwner);
|
||||
return Err(create_error!(NotOwner));
|
||||
}
|
||||
|
||||
// Ensure user is part of group
|
||||
if !recipients.contains(&new_owner) {
|
||||
return Err(Error::NotInGroup);
|
||||
return Err(create_error!(NotInGroup));
|
||||
}
|
||||
|
||||
// Transfer ownership
|
||||
@@ -94,10 +70,10 @@ pub async fn req(
|
||||
to: new_owner,
|
||||
}
|
||||
} else {
|
||||
return Err(Error::InvalidOperation);
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.send(db, user.as_author_for_system(), &channel, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
@@ -128,7 +104,7 @@ pub async fn req(
|
||||
..
|
||||
} => {
|
||||
if let Some(fields) = &data.remove {
|
||||
if fields.contains(&FieldsChannel::Icon) {
|
||||
if fields.contains(&v0::FieldsChannel::Icon) {
|
||||
if let Some(icon) = &icon {
|
||||
db.mark_attachment_as_deleted(&icon.id).await?;
|
||||
}
|
||||
@@ -136,10 +112,10 @@ pub async fn req(
|
||||
|
||||
for field in fields {
|
||||
match field {
|
||||
FieldsChannel::Description => {
|
||||
v0::FieldsChannel::Description => {
|
||||
description.take();
|
||||
}
|
||||
FieldsChannel::Icon => {
|
||||
v0::FieldsChannel::Icon => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
@@ -175,7 +151,7 @@ pub async fn req(
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.send(db, user.as_author_for_system(), &channel, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
@@ -185,26 +161,36 @@ pub async fn req(
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.send(db, user.as_author_for_system(), &channel, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if partial.icon.is_some() {
|
||||
SystemMessage::ChannelIconChanged { by: user.id }
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.await
|
||||
.ok();
|
||||
SystemMessage::ChannelIconChanged {
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(db, user.as_author_for_system(), &channel, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
channel
|
||||
.update(db, partial, data.remove.unwrap_or_default())
|
||||
.update(
|
||||
db,
|
||||
partial,
|
||||
data.remove
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
_ => return Err(Error::InvalidOperation),
|
||||
_ => return Err(create_error!(InvalidOperation)),
|
||||
};
|
||||
|
||||
Ok(Json(channel))
|
||||
Ok(Json(channel.into()))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user