Compare commits

...
Author SHA1 Message Date
Paul Makles f9f0f409a8 chore: bump version to 0.6.11 2024-02-10 01:05:08 +00:00
Paul Makles 8f4d6c864b feat: block mentions from low trust users (account created < 12 hours) 2024-02-10 01:04:52 +00:00
Paul Makles 513461c99a chore: bump version 2024-02-06 13:43:00 +00:00
ToastXC 41ef76ff3e feat: member fetching with roles (#299) 2024-02-06 13:20:35 +00:00
Paul Makles c451e431cd merge: pull request #301 from TheBobBobs/bonfire-redis
fix(bonfire): random disconnects from redis
2024-02-06 13:18:55 +00:00
Amy 09a848f5a6 fix: don't exceed the max emoji limit by one
closes #295
2024-02-06 13:17:13 +00:00
Bob Bobs 308193377d fix(bonfire): use REDIS_URI env var 2024-02-02 10:31:11 -07:00
Bob Bobs 3d3326e68a (bonfire): parse correct PayloadType 2024-02-01 20:07:12 -07:00
Bob Bobs 88c2232a6c fix(bonfire): use fred for redis
https://github.com/aembke/fred.rs
2024-02-01 19:00:45 -07:00
Bob Bobs 25016efbc2 fix(database): fetch all server emojis instead of one 2023-12-02 11:46:18 +00:00
Bob Bobs db63ac4064 fix: add emojis to ServerCreate event 2023-12-02 11:46:18 +00:00
TheBobBobs 1f593a1708 fix: allow reacting with existing emojis when at limit 2023-12-02 11:45:23 +00:00
TheBobBobs 219c16a69c fix: set relationship for users in BulkMessageResponse 2023-12-01 20:54:59 +00:00
Paul Makles aed8f69f34 fix(database): set channel ids for new server object 2023-11-20 10:54:48 +00:00
Paul Makles 196a856c68 refactor: move and update fixme / todo comments 2023-10-28 14:53:39 +01:00
Paul Makles 9bf60e87e9 fix: validate masquerade on new message models 2023-10-28 14:53:28 +01:00
Paul Makles b90e7a4412 fix: prevent potential double fetching of cached server 2023-10-28 14:53:12 +01:00
Paul Makles cb813e91f0 chore: cut new 0.6.9 release 2023-10-28 14:39:10 +01:00
Paul Makles a6bc9fcbb3 refactor: implement Server::create 2023-10-28 14:25:29 +01:00
Paul Makles cd0737911b refactor: use fixtures to populate db
refactor: method for creating server channels
2023-10-27 16:31:37 +01:00
Paul Makles 7705354d4d ci: use reference db for generating spec 2023-10-26 15:07:03 +01:00
Paul Makles 39ac8b2278 refactor: don't create unnecessary reference 2023-10-26 14:12:28 +01:00
arslee07 c5494aa1ca fix: respect Permission::SendEmbeds when editing a message 2023-10-26 14:12:28 +01:00
Paul Makles 1a96bf5a4b chore: remove redundant file [skip ci] 2023-10-26 14:05:01 +01:00
Paul Makles 37a1244751 test: ignore hello world test because it breaks cargo test? 2023-10-26 14:05:01 +01:00
Paul Makles bf39f18f8d fix: always generate new test database 2023-10-26 14:05:01 +01:00
Paul Makles 95be7dc87a ci: use nextest because cargo test sig sevs 2023-10-26 14:05:01 +01:00
Paul Makles 9be171c7b6 fix: don't copy the old test file 2023-10-26 14:05:01 +01:00
Paul Makles 786fbb8ae4 chore: disable broken tests 2023-10-26 14:05:01 +01:00
Paul Makles 6c0210e8a1 feat: always use test configuration during tests 2023-10-26 14:05:01 +01:00
Paul Makles 456bf7b42a fix: check server ownership for text channel
refactor: minor cleanup of code
2023-10-26 14:05:01 +01:00
Paul Makles 21ffea0f9f fix(core/database): store member during permission query 2023-10-26 14:05:01 +01:00
Paul Makles 96d90215d2 fix(core/models): validator feature flag not properly gatekept 2023-10-26 14:05:01 +01:00
Paul Makles 29dcf714f2 chore: start work on test for permission issue 2023-10-26 14:05:01 +01:00
Paul Makles cb97004d3f feat(core): provide user profile where appropriate 2023-10-16 09:33:36 +01:00
Paul Makles 0b6e5dd9db chore: add nix file to vscode config 2023-10-16 09:23:11 +01:00
Paul Makles 7132877201 feat(core): validation for files in reference db 2023-10-16 09:22:53 +01:00
Paul Makles 4d42fb74e7 fix(core): publish user settings event 2023-10-16 09:22:17 +01:00
Paul Makles 866ace3381 refactor: use config for max bot count 2023-10-16 09:21:48 +01:00
Paul Makles 0798e81862 feat(core): implement ratelimit events for reference db 2023-10-16 09:21:34 +01:00
Paul Makles 2fa5ac41ac refactor: tests for more group routes 2023-09-22 19:04:17 +01:00
Paul Makles 25ae1555a2 chore: add nextest and fmt to nix 2023-09-22 19:03:49 +01:00
Paul Makles 881888bd1c chore: add timeout for nextest runs 2023-09-22 19:03:29 +01:00
Paul Makles 41f20c2239 ci: use custom Revolt.toml for tests 2023-09-10 15:46:00 +01:00
Paul Makles 38f1381fea chore: switch to resolver=2 2023-09-10 15:19:23 +01:00
Paul Makles 6838b0b1f0 refactor: ensure in-memory database consistency
test: update authifier to allow for in-memory tests
ci: explicitly run in-memory and mongo tests
2023-09-10 14:49:27 +01:00
Paul Makles 055deab105 fix: allow bots to authenticate with new routes 2023-09-10 14:48:13 +01:00
Paul Makles 6d2073120d fix: carry over send embed permission when sending [skip ci] 2023-09-10 12:41:23 +01:00
Paul Makles 9789909061 feat(delta): rewrite emoji routes w/o quark
#283
2023-09-05 12:36:18 +01:00
Paul Makles 895de86f1e fix(core/config): use uppercase filename 2023-09-03 19:00:17 +01:00
Paul Makles 98b8c5403b feat(delta): rewrite channel / message fetch 2023-09-03 18:27:21 +01:00
Paul Makles aa7c75c60e chore: ignore Revolt.toml 2023-09-03 18:12:15 +01:00
Paul Makles 4270f0c5d4 feat(core): implement create group 2023-09-03 18:12:08 +01:00
Paul Makles eb1f45d208 fix(core/database): use attachments collection name 2023-09-03 17:43:13 +01:00
Paul Makles 9929f7499a refactor(core): use core crates for sending messages 2023-09-03 16:31:39 +01:00
Paul Makles 279d9ef1b5 refactor(core): remove quark references from webhook routes 2023-09-03 15:52:18 +01:00
Paul Makles 5a9bb9e68d fix: convert index map/set to hash map/set
fix: idempotency token should use correct crate cfg
2023-08-27 15:51:20 +01:00
Paul Makles a8fc9e928a fix: import validator crate directly in derive 2023-08-27 15:45:23 +01:00
Paul Makles da231ae390 fix: remove indexmap from models 2023-08-27 15:43:57 +01:00
Paul Makles 7a31da4189 fix: add indexmap/serde to model depends 2023-08-27 15:40:05 +01:00
113 changed files with 3175 additions and 1322 deletions
+2
View File
@@ -0,0 +1,2 @@
[profile.default]
slow-timeout = { period = "3s", terminate-after = 2 }
+20 -13
View File
@@ -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,25 +35,27 @@ 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: Run cargo test
env:
TEST_DB: REFERENCE
run: |
cargo nextest run
- name: Run cargo test (with MongoDB)
env:
TEST_DB: MONGODB
MONGODB: mongodb://localhost
run: |
cargo nextest run
- name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'master'
env:
TEST_DB: REFERENCE
run: |
cargo run --bin revolt-delta &
+2
View File
@@ -1,4 +1,6 @@
Rocket.toml
Revolt.toml
target
.data
.env
+2 -1
View File
@@ -1,5 +1,6 @@
{
"editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy",
"nixEnvSelector.suggestion": false
"nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
}
Generated
+285 -115
View File
@@ -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.6.11"
dependencies = [
"async-std",
"async-tungstenite",
"bincode",
"fred",
"futures",
"log",
"once_cell",
@@ -3512,7 +3600,7 @@ dependencies = [
[[package]]
name = "revolt-config"
version = "0.6.8"
version = "0.6.11"
dependencies = [
"async-std",
"cached",
@@ -3524,7 +3612,7 @@ dependencies = [
[[package]]
name = "revolt-database"
version = "0.6.8"
version = "0.6.11"
dependencies = [
"async-lock",
"async-recursion",
@@ -3569,10 +3657,11 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.6.8"
version = "0.6.11"
dependencies = [
"async-channel",
"async-std",
"authifier",
"bitfield",
"chrono",
"dashmap",
@@ -3591,6 +3680,7 @@ dependencies = [
"redis-kiss",
"regex",
"reqwest",
"revolt-config",
"revolt-database",
"revolt-models",
"revolt-permissions",
@@ -3613,7 +3703,7 @@ dependencies = [
[[package]]
name = "revolt-models"
version = "0.6.8"
version = "0.6.11"
dependencies = [
"indexmap",
"iso8601-timestamp 0.2.11",
@@ -3629,7 +3719,7 @@ dependencies = [
[[package]]
name = "revolt-permissions"
version = "0.6.8"
version = "0.6.11"
dependencies = [
"async-std",
"async-trait",
@@ -3644,7 +3734,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
version = "0.6.8"
version = "0.6.11"
dependencies = [
"async-std",
"log",
@@ -3655,7 +3745,7 @@ dependencies = [
[[package]]
name = "revolt-quark"
version = "0.6.8"
version = "0.6.11"
dependencies = [
"async-lock",
"async-recursion",
@@ -3708,7 +3798,7 @@ dependencies = [
[[package]]
name = "revolt-result"
version = "0.6.8"
version = "0.6.11"
dependencies = [
"revolt_okapi",
"revolt_rocket_okapi",
@@ -3828,7 +3918,7 @@ dependencies = [
"atomic",
"atty",
"binascii",
"bytes 1.1.0",
"bytes 1.5.0",
"either",
"figment",
"futures",
@@ -3838,7 +3928,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 +3938,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 +3948,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 +4021,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 +4176,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 +4310,7 @@ dependencies = [
"sentry-debug-images",
"sentry-panic",
"sentry-tracing",
"tokio 1.18.2",
"tokio 1.35.1",
"ureq",
]
@@ -4521,6 +4611,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 +4881,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 +4916,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 +4926,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 +4937,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 +4971,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 +4985,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 +5029,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 +5107,7 @@ dependencies = [
"futures-channel",
"futures-io",
"futures-util",
"idna",
"idna 0.2.3",
"ipnet",
"lazy_static",
"log",
@@ -5016,7 +5115,7 @@ dependencies = [
"smallvec",
"thiserror",
"tinyvec",
"tokio 1.18.2",
"tokio 1.35.1",
"url",
]
@@ -5036,7 +5135,7 @@ dependencies = [
"resolv-conf",
"smallvec",
"thiserror",
"tokio 1.18.2",
"tokio 1.35.1",
"trust-dns-proto",
]
@@ -5054,7 +5153,7 @@ checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5"
dependencies = [
"base64 0.13.0",
"byteorder",
"bytes 1.1.0",
"bytes 1.5.0",
"http",
"httparse",
"log",
@@ -5167,9 +5266,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 +5278,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 +5340,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 +5359,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 +5397,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 +5412,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 +5728,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"
+3
View File
@@ -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" }
+3
View File
@@ -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",
+5 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.6.8"
version = "0.6.11"
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"] }
+5 -1
View File
@@ -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:?}");
});
}
}
+238 -228
View File
@@ -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,249 @@ 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(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| {
info!("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
&& write
.lock()
.await
.send(config.encode(&event))
.await
.is_err()
{
return;
}
}
}
async fn worker(
user_id: String,
config: &ProtocolConfiguration,
mut read: WsReader,
write: &Mutex<WsWriter>,
) {
while let Ok(Some(msg)) = read.try_next().await {
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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.6.8"
version = "0.6.11"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+3
View File
@@ -0,0 +1,3 @@
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"
+3 -1
View File
@@ -48,9 +48,11 @@ max_concurrent_connections = 50
[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
+9 -2
View File
@@ -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()
@@ -97,9 +102,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,
+7 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.6.8"
version = "0.6.11"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,11 +23,11 @@ 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.6.11", path = "../config" }
revolt-result = { version = "0.6.11", path = "../result" }
revolt-models = { version = "0.6.11", path = "../models" }
revolt-presence = { version = "0.6.11", path = "../presence" }
revolt-permissions = { version = "0.6.11", path = "../permissions", features = [
"serde",
"bson",
] }
@@ -86,4 +86,4 @@ fcm = "0.9.2"
web-push = "0.10.0"
# Authifier
authifier = { version = "1.0" }
authifier = { version = "1.0.8" }
@@ -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
}
]
+32 -21
View File
@@ -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,7 @@ use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
MemberCompositeKey, Message, Server, ServerBan, User, UserSettings, Webhook,
MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, User, UserSettings, Webhook,
};
database_derived!(
@@ -19,6 +19,7 @@ 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>>>,
+8 -13
View File
@@ -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, 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,7 +169,7 @@ 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),
@@ -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));
}
@@ -1,11 +1,15 @@
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 crate::{events::client::EventV1, Database, File, IntoDocumentPath, SystemMessage, User};
use crate::{
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server,
SystemMessage, User,
};
auto_derived!(
#[serde(tag = "channel_type")]
@@ -168,7 +172,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 +190,109 @@ 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)
}
/// Add user to a group
@@ -214,10 +321,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 +338,10 @@ impl Channel {
.await
.ok();
EventV1::ChannelCreate(self.clone().into())
.private(user.id.to_string())
.await;
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
@@ -262,7 +369,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, .. }
@@ -581,3 +688,36 @@ 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_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));
});
}
}
@@ -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 {
@@ -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,21 @@
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, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification,
ReplyIntent, SendableEmbed, RE_MENTION,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
use ulid::Ulid;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
Channel, Database, File,
util::idempotency::IdempotencyKey,
Channel, Database, Emoji, File,
};
auto_derived_partial!(
@@ -196,6 +204,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,
@@ -293,6 +460,64 @@ 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(())
}
/// 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))
}
}
}
impl SystemMessage {
@@ -309,6 +534,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 {
@@ -60,7 +60,7 @@ impl AbstractMessages for ReferenceDb {
.cloned()
.collect();
// TODO: sorting, etc
// FIXME: sorting, etc (will be required for tests)
Ok(matched_messages)
@@ -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(&&gte_cmp_id) == Ordering::Greater
&& event.target_id == target_id
&& event.event_type == event_type
})
.count()
>= count)
}
}
@@ -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
@@ -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
@@ -209,80 +254,7 @@ impl Server {
}
}
/* /// 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(
@@ -450,3 +422,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,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,6 +4,7 @@ use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_config::config;
use revolt_result::{create_error, Error, ErrorType, Result};
use ulid::Ulid;
@@ -182,6 +183,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 +219,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
@@ -326,6 +361,167 @@ impl User {
}
}
/// 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)),
}
}
/// Update user data
pub async fn update<'a>(
&mut self,
@@ -1,7 +1,7 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
use crate::{ReferenceDb, Relationship};
use super::AbstractUsers;
@@ -106,19 +106,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);
}
+2 -2
View File
@@ -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 {
+100 -22
View File
@@ -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 {
@@ -275,6 +278,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 {
@@ -329,7 +341,11 @@ impl From<crate::Message> for Message {
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions,
reactions: value
.reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect(),
interactions: value.interactions.into(),
masquerade: value.masquerade.map(|masq| masq.into()),
}
@@ -353,7 +369,12 @@ impl From<crate::PartialMessage> for PartialMessage {
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions,
reactions: value.reactions.map(|reactions| {
reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect()
}),
interactions: value.interactions.map(|interactions| interactions.into()),
masquerade: value.masquerade.map(|masq| masq.into()),
}
@@ -385,7 +406,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 +443,16 @@ 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::ServerBan> for ServerBan {
fn from(value: crate::ServerBan) -> Self {
ServerBan {
@@ -594,35 +638,69 @@ impl From<crate::FieldsRole> for FieldsRole {
}
impl crate::User {
pub async fn into<P>(self, perspective: P) -> User
pub async fn into<'a, P>(self, db: &Database, perspective: P) -> User
where
P: Into<Option<crate::User>>,
P: Into<Option<&'a crate::User>>,
{
let relationship = if let Some(perspective) = perspective.into() {
perspective
.relations
.unwrap_or_default()
.into_iter()
.find(|relationship| relationship.id == self.id)
.map(|relationship| relationship.status.into())
.unwrap_or_default()
} else {
RelationshipStatus::None
};
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);
// do permission stuff here
// TODO: implement permissions =)
let can_see_profile = false;
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()),
+22 -7
View File
@@ -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;
+3
View File
@@ -2,3 +2,6 @@ pub mod bridge;
pub mod idempotency;
pub mod permissions;
pub mod reference;
#[cfg(test)]
pub mod test_fixtures;
+65 -14
View File
@@ -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,
};
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
@@ -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,26 @@ 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
}
}
/// Short-hand for creating a permission calculator
+26 -1
View File
@@ -7,7 +7,7 @@ use schemars::{
JsonSchema,
};
use crate::{Bot, Database, Webhook};
use crate::{Bot, Channel, Database, Emoji, Message, Server, User, Webhook};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
@@ -27,6 +27,31 @@ impl Reference {
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 message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(&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!("../../../fixtures/", $name, ".json")),
)
.await;
$(
let $variable = fixtures.$type(&$database, $id).await;
)+
};
}
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.6.8"
version = "0.6.11"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -9,7 +9,7 @@ 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"]
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
@@ -18,8 +18,8 @@ default = ["serde", "partials"]
[dependencies]
# Core
revolt-config = { version = "0.6.8", path = "../config" }
revolt-permissions = { version = "0.6.8", path = "../permissions" }
revolt-config = { version = "0.6.11", path = "../config" }
revolt-permissions = { version = "0.6.11", path = "../permissions" }
# Utility
regex = "1"
+2 -4
View File
@@ -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(
+15 -2
View File
@@ -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,
}
}
}
+67 -1
View File
@@ -1,10 +1,11 @@
use super::File;
use revolt_permissions::OverrideField;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
auto_derived!(
/// Channel
#[serde(tag = "channel_type")]
pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages
SavedMessages {
@@ -205,4 +206,69 @@ 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>,
}
);
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,
}
}
}
+24
View File
@@ -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,
}
);
+73 -6
View File
@@ -1,11 +1,21 @@
use std::time::SystemTime;
use std::{
collections::{HashMap, HashSet},
time::SystemTime,
};
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_config::config;
use indexmap::{IndexMap, IndexSet};
#[cfg(feature = "validator")]
use validator::Validate;
use iso8601_timestamp::Timestamp;
use super::{Embed, File, MessageWebhook, User, Webhook};
use super::{Embed, File, 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
@@ -45,8 +55,8 @@ auto_derived_partial!(
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
/// Hashmap of emoji IDs to array of user IDs
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub reactions: IndexMap<String, IndexSet<String>>,
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
pub reactions: HashMap<String, HashSet<String>>,
/// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
@@ -86,17 +96,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>,
}
@@ -105,7 +119,7 @@ auto_derived!(
pub struct Interactions {
/// Reactions which should always appear and be distinct
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reactions: Option<IndexSet<String>>,
pub reactions: Option<HashSet<String>>,
/// Whether reactions should be restricted to the given list
///
/// Can only be set to true if reactions list is of at least length 1
@@ -152,6 +166,59 @@ 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 {
#[validate(length(min = 1, max = 128))]
pub icon_url: Option<String>,
#[validate(length(min = 1, max = 256))]
pub url: Option<String>,
#[validate(length(min = 1, max = 100))]
pub title: Option<String>,
#[validate(length(min = 1, max = 2000))]
pub description: Option<String>,
pub media: Option<String>,
#[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`!**
#[validate(length(min = 1, max = 64))]
pub nonce: Option<String>,
/// Message content to send
#[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
#[validate]
pub embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message
#[validate]
pub masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
pub interactions: Option<Interactions>,
}
);
/// Message Author Abstraction
@@ -1,6 +1,27 @@
use super::File;
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use regex::Regex;
/// 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
+28 -2
View File
@@ -1,8 +1,11 @@
use super::File;
use super::{Channel, File};
use revolt_permissions::OverrideField;
use std::collections::HashMap;
#[cfg(feature = "validator")]
use validator::Validate;
auto_derived_partial!(
/// Server
pub struct Server {
@@ -19,7 +22,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"))]
@@ -141,4 +144,27 @@ 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 returned when creating server
pub struct CreateServerLegacyResponse {
/// Server object
pub server: Server,
/// Default channels
pub channels: Vec<Channel>,
}
);
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.6.8"
version = "0.6.11"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+22 -16
View File
@@ -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 {
return 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()
+9 -7
View File
@@ -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,22 @@ 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 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(&mut self, permission: ChannelPermission) -> bool {
pub fn has_channel_permission(&self, permission: ChannelPermission) -> bool {
self.has(permission as u64)
}
/// 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 {
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.6.8"
version = "0.6.11"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
-1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.6.8"
version = "0.6.11"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+3
View File
@@ -60,6 +60,9 @@ pub enum ErrorType {
TooManyAttachments {
max: usize,
},
TooManyEmbeds {
max: usize,
},
TooManyReplies {
max: usize,
},
+1
View File
@@ -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,
+4 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.6.8"
version = "0.6.11"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -57,7 +57,7 @@ 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
@@ -68,6 +68,8 @@ revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
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",
+15 -3
View File
@@ -8,6 +8,7 @@ extern crate serde_json;
pub mod routes;
pub mod util;
use revolt_database::{Database, MongoDb};
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics;
@@ -33,7 +34,12 @@ pub async fn web() -> Rocket<Build> {
// Setup Authifier
let authifier = Authifier {
database: db.clone().into(),
database: match db.clone() {
Database::Reference(_) => Default::default(),
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: revolt_quark::util::authifier::config(),
event_channel: Some(sender),
};
@@ -55,8 +61,14 @@ 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(),
));
async_std::task::spawn(revolt_quark::tasks::start_workers(
legacy_db.clone(),
authifier.database.clone(),
));
// Configure CORS
let cors = CorsOptions {
+1 -1
View File
@@ -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,
})
+1 -1
View File
@@ -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(),
}))
}
+25 -38
View File
@@ -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,8 +1,11 @@
use revolt_quark::{
models::{Channel, User},
perms, Database, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::{serde::json::Json, State};
/// # Fetch Channel
@@ -10,12 +13,54 @@ use rocket::{serde::json::Json, State};
/// Fetch channel by its id.
#[openapi(tag = "Channel Information")]
#[get("/<target>")]
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<Channel>> {
pub async fn fetch_channel(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<v0::Channel>> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
Ok(Json(channel))
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
Ok(Json(channel.into()))
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::Channel;
use revolt_models::v0;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn fetch_channel() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
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
.get(format!("/channels/{}", group.id()))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let channel: v0::Channel = response.into_json().await.expect("`Channel`");
assert_eq!(channel, group.into());
}
}
@@ -1,40 +1,183 @@
use revolt_quark::{
get_relationship,
models::{user::RelationshipStatus, Channel, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Add Member to Group
///
/// Adds another user to the group.
#[openapi(tag = "Groups")]
#[put("/<target>/recipients/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
#[put("/<group_id>/recipients/<member_id>")]
pub async fn req(
db: &State<Database>,
user: User,
group_id: Reference,
member_id: Reference,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let mut channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::InviteOthers)
.await?;
let mut channel = group_id.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
match &channel {
Channel::Group { .. } => {
let member = member.as_user(db).await?;
if !matches!(
get_relationship(&user, &member.id),
RelationshipStatus::Friend
) {
return Err(Error::NotFriends);
// TODO: use permissions here? interesting if users could block new group invites
let member = member_id.as_user(db).await?;
if !user.is_friends_with(&member.id) {
return Err(create_error!(NotFriends));
}
channel
.add_user_to_group(db, &member.id, &user.id)
.add_user_to_group(db, &member, &user.id)
.await
.map(|_| EmptyResponse)
}
_ => Err(Error::InvalidOperation),
_ => Err(create_error!(InvalidOperation)),
}
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel, RelationshipStatus};
use revolt_models::v0;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_add_member() {
let mut harness = TestHarness::new().await;
let (_, session, mut user) = harness.new_user().await;
let (_, _, mut other_user) = harness.new_user().await;
#[allow(clippy::disallowed_methods)]
user.apply_relationship(
&harness.db,
&mut other_user,
RelationshipStatus::Friend,
RelationshipStatus::Friend,
)
.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
.put(format!(
"/channels/{}/recipients/{}",
group.id(),
other_user.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(&format!("{}!", other_user.id), |event| match event {
EventV1::ChannelCreate(channel) => channel.id() == group.id(),
_ => false,
})
.await;
let event = harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
_ => false,
})
.await;
match event {
EventV1::ChannelGroupJoin { user, .. } => assert_eq!(user, other_user.id),
_ => unreachable!(),
};
let message = harness.wait_for_message(&group.id()).await;
assert_eq!(
message.system,
Some(v0::SystemMessage::UserAdded {
id: other_user.id.to_string(),
by: user.id.to_string()
})
);
}
#[rocket::async_test]
async fn fail_add_non_friend() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, _, other_user) = harness.new_user().await;
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
.put(format!(
"/channels/{}/recipients/{}",
group.id(),
other_user.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::Forbidden);
}
#[rocket::async_test]
async fn fail_add_already_in_group() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
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
.put(format!("/channels/{}/recipients/{}", group.id(), user.id))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::Conflict);
}
}
@@ -1,84 +1,102 @@
use std::{collections::HashSet, iter::FromIterator};
use revolt_quark::{
get_relationship,
models::{user::RelationshipStatus, Channel, User},
variables::delta::MAX_GROUP_SIZE,
Db, Error, Result,
};
use revolt_database::{Channel, Database, RelationshipStatus, User};
use revolt_models::v0;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use rocket::State;
use validator::Validate;
/// # Group Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataCreateGroup {
/// Group name
#[validate(length(min = 1, max = 32))]
name: String,
/// Group description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Array of user IDs to add to the group
///
/// Must be friends with these users.
#[validate(length(min = 0, max = 49))]
users: Vec<String>,
/// Whether this group is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Group
///
/// Create a new group channel.
#[openapi(tag = "Groups")]
#[post("/create", data = "<info>")]
pub async fn req(db: &Db, user: User, info: Json<DataCreateGroup>) -> Result<Json<Channel>> {
#[post("/create", data = "<data>")]
pub async fn create_group(
db: &State<Database>,
user: User,
data: Json<v0::DataCreateGroup>,
) -> Result<Json<v0::Channel>> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut set: HashSet<String> = HashSet::from_iter(info.users.into_iter());
set.insert(user.id.clone());
if set.len() > *MAX_GROUP_SIZE {
return Err(Error::GroupTooLarge {
max: *MAX_GROUP_SIZE,
});
}
for target in &set {
match get_relationship(&user, target) {
for target in &data.users {
match user.relationship_with(target) {
RelationshipStatus::Friend | RelationshipStatus::User => {}
_ => {
return Err(Error::NotFriends);
return Err(create_error!(NotFriends));
}
}
}
let group = Channel::Group {
id: Ulid::new().to_string(),
Ok(Json(Channel::create_group(db, data, user.id).await?.into()))
}
name: info.name,
owner: user.id,
description: info.description,
recipients: set.into_iter().collect::<Vec<String>>(),
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::events::client::EventV1;
use revolt_models::v0;
use rocket::http::{ContentType, Header, Status};
icon: None,
last_message_id: None,
#[rocket::async_test]
async fn create_group() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
permissions: None,
let response = harness
.client
.post("/channels/create")
.header(Header::new("x-session-token", session.token.to_string()))
.header(ContentType::JSON)
.body(
json!(v0::DataCreateBot {
name: TestHarness::rand_string(),
})
.to_string(),
)
.dispatch()
.await;
nsfw: info.nsfw.unwrap_or(false),
};
assert_eq!(response.status(), Status::Ok);
group.create(db).await?;
Ok(Json(group))
let channel: v0::Channel = response.into_json().await.expect("`Channel`");
match channel {
v0::Channel::Group {
id,
owner,
recipients,
..
} => {
assert_eq!(owner, user.id);
assert_eq!(recipients.len(), 1);
assert!(harness.db.fetch_channel(&id).await.is_ok());
let event = harness
.wait_for_event(&format!("{}!", user.id), |event| match event {
EventV1::ChannelCreate(channel) => channel.id() == id,
_ => false,
})
.await;
match event {
EventV1::ChannelCreate(v0::Channel::Group {
owner: channel_owner,
..
}) => {
assert_eq!(owner, channel_owner);
}
_ => unreachable!(),
}
}
_ => unreachable!(),
}
}
}
@@ -40,3 +40,151 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyR
_ => Err(Error::InvalidOperation),
}
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel, RelationshipStatus};
use revolt_models::v0;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_remove_member() {
let mut harness = TestHarness::new().await;
let (_, session, mut user) = harness.new_user().await;
let (_, _, mut other_user) = harness.new_user().await;
#[allow(clippy::disallowed_methods)]
user.apply_relationship(
&harness.db,
&mut other_user,
RelationshipStatus::Friend,
RelationshipStatus::Friend,
)
.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
.put(format!(
"/channels/{}/recipients/{}",
group.id(),
other_user.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(&format!("{}!", other_user.id), |event| match event {
EventV1::ChannelCreate(channel) => channel.id() == group.id(),
_ => false,
})
.await;
let event = harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
_ => false,
})
.await;
match event {
EventV1::ChannelGroupJoin { user, .. } => assert_eq!(user, other_user.id),
_ => unreachable!(),
};
let message = harness.wait_for_message(&group.id()).await;
assert_eq!(
message.system,
Some(v0::SystemMessage::UserAdded {
id: other_user.id.to_string(),
by: user.id.to_string()
})
);
}
#[rocket::async_test]
async fn fail_not_in_group() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, _, other_user) = harness.new_user().await;
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
.delete(format!(
"/channels/{}/recipients/{}",
group.id(),
other_user.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
dbg!(response.into_string().await);
// FIXME: finish impl
// assert_eq!(response.status(), Status::NotFound);
}
#[rocket::async_test]
async fn fail_not_group_owner() {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let (_, session, other_user) = harness.new_user().await;
let (_, _, user_to_be_kicked) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
v0::DataCreateGroup {
name: TestHarness::rand_string(),
users: vec![&other_user.id, &user_to_be_kicked.id]
.into_iter()
.cloned()
.collect(),
..Default::default()
},
user.id.to_string(),
)
.await
.unwrap();
let _response = harness
.client
.delete(format!(
"/channels/{}/recipients/{}",
group.id(),
user_to_be_kicked.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
// FIXME: impl assert_eq!(response.status(), Status::Forbidden);
}
}
@@ -78,6 +78,11 @@ pub async fn req(
// 3. Replace if we are given new embeds
if let Some(embeds) = edit.embeds {
// Ensure we have permissions to send embeds
permissions
.throw_permission_and_view_channel(db, Permission::SendEmbeds)
.await?;
new_embeds.clear();
for embed in embeds {
@@ -89,14 +94,19 @@ pub async fn req(
message.update(db, partial).await?;
// Queue up a task for processing embeds
if let Some(content) = edit.content {
revolt_quark::tasks::process_embeds::queue(
message.channel.to_string(),
message.id.to_string(),
content,
)
.await;
// Queue up a task for processing embeds if the we have sufficient permissions
if permissions
.has_permission(db, Permission::SendEmbeds)
.await?
{
if let Some(content) = edit.content {
revolt_quark::tasks::process_embeds::queue(
message.channel.to_string(),
message.id.to_string(),
content,
)
.await;
}
}
Ok(Json(message))
@@ -1,26 +1,34 @@
use revolt_quark::{
models::{Message, User},
perms, Db, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use rocket::serde::json::Json;
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Fetch Message
///
/// Retrieves a message by its id.
#[openapi(tag = "Messaging")]
#[get("/<target>/messages/<msg>")]
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<Json<Message>> {
pub async fn req(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<Json<v0::Message>> {
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)?;
let message = msg.as_message(db).await?;
if message.channel != channel.as_id() {
return Err(Error::NotFound);
if message.channel != channel.id() {
return Err(create_error!(NotFound));
}
Ok(Json(message))
Ok(Json(message.into()))
}
@@ -91,7 +91,7 @@ pub async fn req(
})
.await?;
BulkMessageResponse::transform(db, Some(&channel), messages, include_users)
BulkMessageResponse::transform(db, Some(&channel), messages, &user, include_users)
.await
.map(Json)
}
@@ -1,26 +0,0 @@
use revolt_quark::{models::User, Ref, Result};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct OptionsQueryStale {
/// Array of message IDs
#[validate(length(min = 0, max = 150))]
ids: Vec<String>,
}
/// # Poll Message Changes
///
/// This route returns any changed message objects and tells you if any have been deleted.
///
/// Don't actually poll this route, instead use this to update your local database.
///
/// **DEPRECATED**
#[openapi(tag = "Messaging")]
#[post("/<_target>/messages/stale", data = "<_data>")]
pub async fn req(_user: User, _target: Ref, _data: Json<OptionsQueryStale>) -> Result<()> {
Ok(())
}
@@ -90,7 +90,7 @@ pub async fn req(
})
.await?;
BulkMessageResponse::transform(db, Some(&channel), messages, include_users)
BulkMessageResponse::transform(db, Some(&channel), messages, &user, include_users)
.await
.map(Json)
}
@@ -1,12 +1,14 @@
use revolt_database::util::idempotency::IdempotencyKey;
use revolt_quark::{
models::{message::DataMessageSend, Message, User},
perms,
types::push::MessageAuthor,
Db, Error, Permission, Ref, Result,
use chrono::{Duration, Utc};
use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::{
util::idempotency::IdempotencyKey, util::reference::Reference, Database, User,
};
use revolt_database::{Interactions, Message};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use rocket::State;
use validator::Validate;
/// # Send Message
@@ -15,68 +17,81 @@ use validator::Validate;
#[openapi(tag = "Messaging")]
#[post("/<target>/messages", data = "<data>")]
pub async fn message_send(
db: &Db,
db: &State<Database>,
user: User,
target: Ref,
data: Json<DataMessageSend>,
target: Reference,
data: Json<v0::DataMessageSend>,
idempotency: IdempotencyKey,
) -> Result<Json<Message>> {
) -> Result<Json<v0::Message>> {
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()
})
})?;
// Ensure we have permissions to send a message
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::SendMessage)
.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::SendMessage)?;
// Verify permissions for masquerade
if let Some(masq) = &data.masquerade {
permissions
.throw_permission(db, Permission::Masquerade)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::Masquerade)?;
if masq.colour.is_some() {
permissions
.throw_permission(db, Permission::ManageRole)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?;
}
}
// Check permissions for embeds
if data.embeds.as_ref().is_some_and(|v| !v.is_empty()) {
permissions
.throw_permission(db, Permission::SendEmbeds)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendEmbeds)?;
}
// Check permissions for files
if data.attachments.as_ref().is_some_and(|v| !v.is_empty()) {
permissions
.throw_permission(db, Permission::UploadFiles)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::UploadFiles)?;
}
// Ensure interactions information is correct
if let Some(interactions) = &data.interactions {
interactions.validate(db, &mut permissions).await?;
let interactions: Interactions = interactions.clone().into();
interactions.validate(db, &permissions).await?;
}
// Create the message
let message = channel
.send_message(
db,
data,
MessageAuthor::User(&user),
idempotency,
permissions
.has_permission(db, Permission::SendEmbeds)
.await?,
)
.await?;
// Disallow mentions for new users (TRUST-0: <12 hours age) in public servers
let allow_mentions = if let Some(server) = query.server_ref() {
if server.discoverable {
if (Utc::now() - ulid::Ulid::from_string(&user.id).unwrap().datetime())
< Duration::hours(12)
{
false
} else {
true
}
} else {
true
}
} else {
true
};
Ok(Json(message))
// Create the message
let author: v0::User = user.clone().into(db, Some(&user)).await;
Ok(Json(
Message::create_from_api(
db,
channel,
data,
v0::MessageAuthor::User(&author),
idempotency,
permissions.has_channel_permission(ChannelPermission::SendEmbeds),
allow_mentions,
)
.await?
.into(),
))
}
+2 -4
View File
@@ -16,7 +16,6 @@ mod message_delete;
mod message_edit;
mod message_fetch;
mod message_query;
mod message_query_stale;
mod message_react;
mod message_search;
mod message_send;
@@ -30,7 +29,7 @@ mod webhook_fetch_all;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
channel_ack::req,
channel_fetch::req,
channel_fetch::fetch_channel,
members_fetch::req,
channel_delete::req,
channel_edit::req,
@@ -38,12 +37,11 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
message_send::message_send,
message_query::req,
message_search::req,
message_query_stale::req,
message_fetch::req,
message_edit::req,
message_bulk_delete::req,
message_delete::req,
group_create::req,
group_create::create_group,
group_add_member::req,
group_remove_member::req,
voice_join::req,
@@ -1,23 +1,16 @@
use revolt_database::{Database, Webhook};
use revolt_quark::{
models::{Channel, User},
perms, Db, Error, Permission, Ref, Result,
DEFAULT_WEBHOOK_PERMISSIONS,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User, Webhook,
};
use revolt_models::v0;
use revolt_permissions::{
calculate_channel_permissions, ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS,
};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct CreateWebhookBody {
#[validate(length(min = 1, max = 32))]
name: String,
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
}
/// # Creates a webhook
///
/// Creates a webhook which 3rd party platforms can use to send messages
@@ -25,33 +18,34 @@ pub struct CreateWebhookBody {
#[post("/<target>/webhooks", data = "<data>")]
pub async fn req(
db: &State<Database>,
legacy_db: &Db,
user: User,
target: Ref,
data: Json<CreateWebhookBody>,
) -> Result<Json<revolt_models::v0::Webhook>> {
target: Reference,
data: Json<v0::CreateWebhookBody>,
) -> Result<Json<v0::Webhook>> {
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 channel = target.as_channel(legacy_db).await?;
let channel = target.as_channel(db).await?;
if !matches!(channel, Channel::TextChannel { .. } | Channel::Group { .. }) {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
let mut permissions = perms(&user).channel(&channel);
permissions
.has_permission(legacy_db, Permission::ManageWebhooks)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageWebhooks)?;
let webhook_id = Ulid::new().to_string();
let avatar = match &data.avatar {
Some(id) => Some(
db.find_and_use_attachment(id, "avatars", "user", &webhook_id)
.await
.map_err(Error::from_core)?,
.await?,
),
None => None,
};
@@ -60,12 +54,12 @@ pub async fn req(
id: webhook_id,
name: data.name,
avatar,
channel_id: channel.id().to_string(),
channel_id: channel.id(),
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
token: Some(nanoid::nanoid!(64)),
};
webhook.create(db).await.map_err(Error::from_core)?;
webhook.create(db).await?;
Ok(Json(webhook.into()))
}
@@ -1,6 +1,10 @@
use revolt_database::Database;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_models::v0::Webhook;
use revolt_quark::{models::User, perms, Db, Error, Permission, Ref, Result};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::{serde::json::Json, State};
/// # Gets all webhooks
@@ -10,20 +14,19 @@ use rocket::{serde::json::Json, State};
#[get("/<channel_id>/webhooks")]
pub async fn req(
db: &State<Database>,
legacy_db: &Db,
user: User,
channel_id: Ref,
channel_id: Reference,
) -> Result<Json<Vec<Webhook>>> {
let channel = channel_id.as_channel(legacy_db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.has_permission(legacy_db, Permission::ManageWebhooks)
.await?;
let channel = channel_id.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
Ok(Json(
db.fetch_webhooks_for_channel(channel.id())
.await
.map_err(Error::from_core)?
db.fetch_webhooks_for_channel(&channel.id())
.await?
.into_iter()
.map(|v| v.into())
.collect::<Vec<Webhook>>(),
@@ -1,31 +1,11 @@
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_quark::models::emoji::EmojiParent;
use revolt_quark::models::{Emoji, File, User};
use revolt_quark::variables::delta::MAX_EMOJI_COUNT;
use revolt_quark::{perms, Db, Error, Permission, Result};
use serde::Deserialize;
use revolt_config::config;
use revolt_database::{util::permissions::DatabasePermissionQuery, Database, Emoji, File, User};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use validator::Validate;
use rocket::serde::json::Json;
/// Regex for valid emoji names
///
/// Alphanumeric and underscores
pub static RE_EMOJI: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap());
/// # Emoji Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataCreateEmoji {
/// Server name
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
name: String,
/// Parent information
parent: EmojiParent,
/// Whether the emoji is mature
#[serde(default)]
nsfw: bool,
}
use rocket::{serde::json::Json, State};
/// # Create New Emoji
///
@@ -33,41 +13,45 @@ pub struct DataCreateEmoji {
#[openapi(tag = "Emojis")]
#[put("/emoji/<id>", data = "<data>")]
pub async fn create_emoji(
db: &Db,
db: &State<Database>,
user: User,
id: String,
data: Json<DataCreateEmoji>,
) -> Result<Json<Emoji>> {
data: Json<v0::DataCreateEmoji>,
) -> Result<Json<v0::Emoji>> {
let config = config().await;
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()
})
})?;
// Bots cannot manage emojis
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
// Validate we have permission to write into parent
match &data.parent {
EmojiParent::Server { id } => {
v0::EmojiParent::Server { id } => {
let server = db.fetch_server(id).await?;
// Check for permission
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageCustomisation)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageCustomisation)?;
// Check that there are no more than 100 emoji
// ! FIXME: hardcoded upper limit
// Check that we haven't hit the emoji limit
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
if emojis.len() > *MAX_EMOJI_COUNT {
return Err(Error::TooManyEmoji {
max: *MAX_EMOJI_COUNT,
});
if emojis.len() >= config.features.limits.default.server_emoji {
return Err(create_error!(TooManyEmoji {
max: config.features.limits.default.server_emoji,
}));
}
}
EmojiParent::Detached => return Err(Error::InvalidOperation),
v0::EmojiParent::Detached => return Err(create_error!(InvalidOperation)),
};
// Find the relevant attachment
@@ -76,7 +60,7 @@ pub async fn create_emoji(
// Create the emoji object
let emoji = Emoji {
id,
parent: data.parent,
parent: data.parent.into(),
creator_id: user.id,
name: data.name,
animated: "image/gif" == &attachment.content_type,
@@ -85,5 +69,5 @@ pub async fn create_emoji(
// Save emoji
emoji.create(db).await?;
Ok(Json(emoji))
Ok(Json(emoji.into()))
}
@@ -1,19 +1,30 @@
use revolt_quark::models::{emoji::EmojiParent, User};
use revolt_quark::{perms, Db, EmptyResponse, Error, Permission, Ref, Result};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, EmojiParent, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Delete Emoji
///
/// Delete an emoji by its id.
#[openapi(tag = "Emojis")]
#[delete("/emoji/<id>")]
pub async fn delete_emoji(db: &Db, user: User, id: Ref) -> Result<EmptyResponse> {
#[delete("/emoji/<emoji_id>")]
pub async fn delete_emoji(
db: &State<Database>,
user: User,
emoji_id: Reference,
) -> Result<EmptyResponse> {
// Bots cannot manage emoji
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
// Fetch the emoji
let emoji = id.as_emoji(db).await?;
let emoji = emoji_id.as_emoji(db).await?;
// If we uploaded the emoji, then we have permission to delete it
if emoji.creator_id != user.id {
@@ -23,10 +34,10 @@ pub async fn delete_emoji(db: &Db, user: User, id: Ref) -> Result<EmptyResponse>
let server = db.fetch_server(id).await?;
// Check for permission
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageCustomisation)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageCustomisation)?;
}
EmojiParent::Detached => return Ok(EmptyResponse),
};
@@ -1,13 +1,18 @@
use revolt_quark::models::{Emoji, User};
use revolt_quark::{Db, Ref, Result};
use revolt_database::{util::reference::Reference, Database};
use revolt_models::v0;
use revolt_result::Result;
use rocket::serde::json::Json;
use rocket::{serde::json::Json, State};
/// # Fetch Emoji
///
/// Fetch an emoji by its id.
#[openapi(tag = "Emojis")]
#[get("/emoji/<id>")]
pub async fn fetch_emoji(db: &Db, _user: User, id: Ref) -> Result<Json<Emoji>> {
id.as_emoji(db).await.map(Json)
#[get("/emoji/<emoji_id>")]
pub async fn fetch_emoji(db: &State<Database>, emoji_id: Reference) -> Result<Json<v0::Emoji>> {
emoji_id
.as_emoji(db)
.await
.map(|emoji| emoji.into())
.map(Json)
}
+1
View File
@@ -138,6 +138,7 @@ pub async fn root() -> Result<Json<RevoltConfig>> {
}
#[cfg(test)]
#[cfg(feature = "FIXME: THIS TEST CAUSES cargo test TO SEG FAULT, I HAVE NO CLUE HOW")]
mod test {
use crate::rocket;
use rocket::http::Status;
+27 -107
View File
@@ -1,119 +1,39 @@
use std::collections::HashMap;
use revolt_quark::{
models::{server::PartialServer, Channel, User},
perms, Db, Error, Permission, Ref, Result, variables::delta::MAX_CHANNEL_COUNT,
};
use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::{util::reference::Reference, Channel, Database, User};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use rocket::State;
use validator::Validate;
/// # Channel Type
#[derive(Serialize, Deserialize, JsonSchema, Default)]
enum ChannelType {
/// Text Channel
#[default]
Text,
/// Voice Channel
Voice,
}
/// # Channel Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataCreateChannel {
/// Channel type
#[serde(rename = "type", default = "ChannelType::default")]
channel_type: ChannelType,
/// Channel name
#[validate(length(min = 1, max = 32))]
name: String,
/// Channel description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Channel
///
/// Create a new Text or Voice channel.
#[openapi(tag = "Server Information")]
#[post("/<target>/channels", data = "<info>")]
pub async fn req(
db: &Db,
#[post("/<server>/channels", data = "<data>")]
pub async fn create_server_channel(
db: &State<Database>,
user: User,
target: Ref,
info: Json<DataCreateChannel>,
) -> Result<Json<Channel>> {
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
server: Reference,
data: Json<v0::DataCreateServerChannel>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut server = target.as_server(db).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageChannel)
.await?;
let mut server = server.as_server(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
if server.channels.len() > *MAX_CHANNEL_COUNT {
return Err(Error::TooManyChannels { max: *MAX_CHANNEL_COUNT })
};
let id = Ulid::new().to_string();
let mut channels = server.channels.clone();
channels.push(id.clone());
let DataCreateChannel {
name,
description,
nsfw,
channel_type,
} = info;
let channel = match channel_type {
ChannelType::Text => Channel::TextChannel {
id,
server: server.id.clone(),
name,
description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: nsfw.unwrap_or(false),
},
ChannelType::Voice => Channel::VoiceChannel {
id,
server: server.id.clone(),
name,
description,
icon: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: nsfw.unwrap_or(false),
},
};
channel.create(db).await?;
server
.update(
db,
PartialServer {
channels: Some(channels),
..Default::default()
},
vec![],
)
.await?;
Ok(Json(channel))
Channel::create_server_channel(db, &mut server, data, true)
.await
.map(|channel| channel.into())
.map(Json)
}
@@ -1,17 +1,25 @@
use revolt_quark::{
models::{Member, User},
perms, Db, Ref, Result,
};
use revolt_quark::models::server_member::MemberResponse;
use revolt_quark::{models::User, perms, Db, Ref, Result};
use rocket::serde::json::Json;
/// # Fetch Member
///
/// Retrieve a member.
#[openapi(tag = "Server Members")]
#[get("/<target>/members/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<Json<Member>> {
#[get("/<target>/members/<member>?<roles>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
member: Ref,
roles: Option<bool>,
) -> Result<Json<MemberResponse>> {
let server = target.as_server(db).await?;
perms(&user).server(&server).calc(db).await?;
member.as_member(db, &server.id).await.map(Json)
let member_response: MemberResponse = match roles {
Some(true) => member.as_member_with_roles(db, &server.id).await?.into(),
_ => member.as_member(db, &server.id).await?.into(),
};
Ok(Json(member_response))
}
+4 -2
View File
@@ -17,6 +17,7 @@ mod permissions_set_default;
mod roles_create;
mod roles_delete;
mod roles_edit;
mod roles_fetch;
mod server_ack;
mod server_create;
mod server_delete;
@@ -25,12 +26,12 @@ mod server_fetch;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
server_create::req,
server_create::create_server,
server_delete::req,
server_fetch::req,
server_edit::req,
server_ack::req,
channel_create::req,
channel_create::create_server_channel,
member_fetch_all::req,
member_remove::req,
member_fetch::req,
@@ -42,6 +43,7 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
invites_fetch::req,
roles_create::req,
roles_edit::req,
roles_fetch::req,
roles_delete::req,
permissions_set::req,
permissions_set_default::req,
@@ -0,0 +1,19 @@
use revolt_quark::{models::server::Role, Db, Error, Ref, Result};
use rocket::serde::json::Json;
/// # Fetch Role
///
/// Fetch a role by its id.
#[openapi(tag = "Server Permissions")]
#[get("/<target>/roles/<role_id>")]
pub async fn req(db: &Db, target: Ref, role_id: String) -> Result<Json<Role>> {
let server = target.as_server(db).await?;
let role = server.roles.get(&role_id);
if let Some(role) = role {
Ok(Json(role.clone()))
} else {
Err(Error::NotFound)
}
}
@@ -1,102 +1,39 @@
use std::collections::HashMap;
use revolt_quark::{
models::{Channel, Server, User},
variables::delta::MAX_SERVER_COUNT,
Db, Error, Result, DEFAULT_PERMISSION_SERVER,
};
use revolt_database::{Database, Member, Server, User};
use revolt_models::v0;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use rocket::State;
use validator::Validate;
/// # Server Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataCreateServer {
/// Server name
#[validate(length(min = 1, max = 32))]
name: String,
/// Server description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Whether this server is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Server Response
#[derive(Validate, Serialize, JsonSchema)]
pub struct CreateServerResponse {
/// Server object
server: Server,
/// Default channels
channels: Vec<Channel>,
}
/// # Create Server
///
/// Create a new server.
#[openapi(tag = "Server Information")]
#[post("/create", data = "<info>")]
pub async fn req(
db: &Db,
#[post("/create", data = "<data>")]
pub async fn create_server(
db: &State<Database>,
user: User,
info: Json<DataCreateServer>,
) -> Result<Json<CreateServerResponse>> {
data: Json<v0::DataCreateServer>,
) -> Result<Json<v0::CreateServerLegacyResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
if !user.can_acquire_server(db).await? {
return Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
});
}
user.can_acquire_server(db).await?;
let DataCreateServer {
name,
description,
nsfw,
} = info;
let (server, channels) = Server::create(db, data, &user, true).await?;
let channels = Member::create(db, &server, &user, Some(channels)).await?;
let channel_id = Ulid::new().to_string();
let server_id = Ulid::new().to_string();
let channel = Channel::TextChannel {
id: channel_id.clone(),
server: server_id.clone(),
name: "General".into(),
description: None,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: nsfw.unwrap_or(false),
};
db.insert_channel(&channel).await?;
let server = Server {
id: server_id.clone(),
owner: user.id.clone(),
name,
description,
channels: vec![channel_id],
nsfw: nsfw.unwrap_or(false),
default_permissions: *DEFAULT_PERMISSION_SERVER as i64,
..Default::default()
};
server.create(db).await?;
let channels = server.create_member(db, user, Some(vec![channel])).await?;
Ok(Json(CreateServerResponse { server, channels }))
Ok(Json(v0::CreateServerLegacyResponse {
server: server.into(),
channels: channels.into_iter().map(|channel| channel.into()).collect(),
}))
}
@@ -1,5 +1,9 @@
use revolt_database::{util::reference::Reference, Database};
use revolt_quark::{models::User, perms, Db, Error, Permission, Result};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
@@ -10,21 +14,16 @@ use rocket_empty::EmptyResponse;
#[delete("/<webhook_id>")]
pub async fn webhook_delete(
db: &State<Database>,
legacy_db: &Db,
user: User,
webhook_id: Reference,
) -> Result<EmptyResponse> {
let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
let webhook = webhook_id.as_webhook(db).await?;
let channel = db.fetch_channel(&webhook.channel_id).await?;
perms(&user)
.channel(&channel)
.throw_permission(legacy_db, Permission::ManageWebhooks)
.await?;
webhook
.delete(db)
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.map(|_| EmptyResponse)
.map_err(Error::from_core)
.throw_if_lacking_channel_permission(ChannelPermission::ManageWebhooks)?;
webhook.delete(db).await.map(|_| EmptyResponse)
}
@@ -1,6 +1,10 @@
use revolt_database::{util::reference::Reference, Database, PartialWebhook};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, PartialWebhook, User,
};
use revolt_models::v0::{DataEditWebhook, Webhook};
use revolt_quark::{models::User, perms, Db, Error, Permission, Result};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
@@ -11,22 +15,24 @@ use validator::Validate;
#[patch("/<webhook_id>", data = "<data>")]
pub async fn webhook_edit(
db: &State<Database>,
legacy_db: &Db,
webhook_id: Reference,
user: User,
data: Json<DataEditWebhook>,
) -> Result<Json<Webhook>> {
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 webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
let mut webhook = webhook_id.as_webhook(db).await?;
let channel = db.fetch_channel(&webhook.channel_id).await?;
perms(&user)
.channel(&channel)
.throw_permission(legacy_db, Permission::ManageWebhooks)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageWebhooks)?;
if data.name.is_none() && data.avatar.is_none() && data.remove.is_empty() {
return Ok(Json(webhook.into()));
@@ -48,16 +54,14 @@ pub async fn webhook_edit(
if let Some(avatar) = avatar {
let file = db
.find_and_use_attachment(&avatar, "avatars", "user", &webhook.id)
.await
.map_err(Error::from_core)?;
.await?;
partial.avatar = Some(file)
}
webhook
.update(db, partial, remove.into_iter().map(|v| v.into()).collect())
.await
.map_err(Error::from_core)?;
.await?;
Ok(Json(webhook.into()))
}
@@ -1,12 +1,10 @@
use revolt_database::{
util::{idempotency::IdempotencyKey, reference::Reference},
Database,
};
use revolt_quark::{
models::message::{DataMessageSend, Message},
types::push::MessageAuthor,
Db, Error, Result,
Database, Message,
};
use revolt_models::v0;
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
@@ -18,31 +16,53 @@ use validator::Validate;
#[post("/<webhook_id>/<token>", data = "<data>")]
pub async fn webhook_execute(
db: &State<Database>,
legacy_db: &Db,
webhook_id: Reference,
token: String,
data: Json<DataMessageSend>,
data: Json<v0::DataMessageSend>,
idempotency: IdempotencyKey,
) -> Result<Json<Message>> {
) -> Result<Json<v0::Message>> {
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 webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
webhook.assert_token(&token).map_err(Error::from_core)?;
let webhook = webhook_id.as_webhook(db).await?;
webhook.assert_token(&token)?;
data.validate_webhook_permissions(webhook.permissions)?;
let permissions: PermissionValue = webhook.permissions.into();
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?;
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
let message = channel
.send_message(
legacy_db,
if data.attachments.as_ref().map_or(false, |v| !v.is_empty()) {
permissions.throw_if_lacking_channel_permission(ChannelPermission::UploadFiles)?;
}
if data.embeds.as_ref().map_or(false, |v| !v.is_empty()) {
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendEmbeds)?;
}
if data.masquerade.is_some() {
permissions.throw_if_lacking_channel_permission(ChannelPermission::Masquerade)?;
}
if data.interactions.is_some() {
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
}
let channel = db.fetch_channel(&webhook.channel_id).await?;
Ok(Json(
Message::create_from_api(
db,
channel,
data,
MessageAuthor::Webhook(&webhook.into()),
v0::MessageAuthor::Webhook(&webhook.into()),
idempotency,
true,
true,
)
.await?;
Ok(Json(message))
.await?
.into(),
))
}
@@ -1,10 +1,6 @@
use revolt_database::{util::reference::Reference, Database};
use revolt_models::v0::Webhook;
use revolt_quark::{
models::{message::SendableEmbed, Message},
types::push::MessageAuthor,
Db, Error, Result,
};
use revolt_database::{util::reference::Reference, Database, Message};
use revolt_models::v0::{MessageAuthor, SendableEmbed, Webhook};
use revolt_result::{create_error, Error, Result};
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
@@ -639,7 +635,7 @@ impl<'r> FromRequest<'r> for EventHeader<'r> {
async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
let headers = request.headers();
let Some(event) = headers.get_one("X-GitHub-Event") else {
return rocket::request::Outcome::Failure((Status::BadRequest, Error::InvalidOperation))
return rocket::request::Outcome::Failure((Status::BadRequest, create_error!(InvalidOperation)))
};
rocket::request::Outcome::Success(Self(event))
@@ -702,7 +698,7 @@ fn safe_from_str<T: for<'de> Deserialize<'de>>(data: &str) -> Result<T> {
Ok(output) => Ok(output),
Err(err) => {
log::error!("{err:?}");
Err(Error::InvalidOperation)
Err(create_error!(InvalidOperation))
}
}
}
@@ -727,7 +723,7 @@ fn convert_event(data: &str, event_name: &str) -> Result<Event> {
"issue_comment" => BaseEvent::IssueComment(safe_from_str(data)?),
"issues" => BaseEvent::Issues(safe_from_str(data)?),
"pull_request" => BaseEvent::PullRequest(safe_from_str(data)?),
_ => return Err(Error::InvalidOperation),
_ => return Err(create_error!(InvalidOperation)),
};
let _Event {
@@ -751,16 +747,15 @@ fn convert_event(data: &str, event_name: &str) -> Result<Event> {
#[post("/<webhook_id>/<token>/github", data = "<data>")]
pub async fn webhook_execute_github(
db: &State<Database>,
legacy_db: &Db,
webhook_id: Reference,
token: String,
event: EventHeader<'_>,
data: String,
) -> Result<()> {
let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
webhook.assert_token(&token).map_err(Error::from_core)?;
let webhook = webhook_id.as_webhook(db).await?;
webhook.assert_token(&token)?;
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
let channel = db.fetch_channel(&webhook.channel_id).await?;
let event = convert_event(&data, &event)?;
let sendable_embed = match event.event {
@@ -1058,30 +1053,25 @@ pub async fn webhook_execute_github(
},
};
sendable_embed
.validate()
.map_err(|error| Error::FailedValidation { error })?;
sendable_embed.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let message_id = Ulid::new().to_string();
let embed = sendable_embed
.into_embed(legacy_db, &message_id)
.await?;
let mut message = Message {
id: message_id,
author: webhook.id.clone(),
channel: webhook.channel_id.clone(),
embeds: Some(vec![embed]),
webhook: Some(std::convert::Into::<Webhook>::into(webhook.clone()).into()),
..Default::default()
};
#[allow(clippy::disallowed_methods)]
message.attach_sendable_embed(db, sendable_embed).await?;
message
.create(
legacy_db,
&channel,
Some(MessageAuthor::Webhook(&webhook.into())),
)
.send(db, MessageAuthor::Webhook(&webhook.into()), &channel, false)
.await
}
@@ -1,6 +1,10 @@
use revolt_database::{util::reference::Reference, Database};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_models::v0::{ResponseWebhook, Webhook};
use revolt_quark::{models::User, perms, Db, Error, Permission, Result};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::{serde::json::Json, State};
/// # Gets a webhook
@@ -10,17 +14,16 @@ use rocket::{serde::json::Json, State};
#[get("/<webhook_id>")]
pub async fn webhook_fetch(
db: &State<Database>,
legacy_db: &Db,
webhook_id: Reference,
user: User,
) -> Result<Json<ResponseWebhook>> {
let webhook = webhook_id.as_webhook(db).await.map_err(Error::from_core)?;
let channel = legacy_db.fetch_channel(&webhook.channel_id).await?;
let webhook = webhook_id.as_webhook(db).await?;
let channel = db.fetch_channel(&webhook.channel_id).await?;
perms(&user)
.channel(&channel)
.throw_permission(legacy_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)?;
Ok(Json(std::convert::Into::<Webhook>::into(webhook).into()))
}
+39 -14
View File
@@ -1,7 +1,8 @@
use futures::StreamExt;
use rand::Rng;
use redis_kiss::redis::aio::PubSub;
use revolt_database::{events::client::EventV1, Database, DatabaseInfo, User};
use revolt_database::{events::client::EventV1, Database, User};
use revolt_models::v0;
use revolt_quark::authifier::{
models::{Account, Session},
Authifier,
@@ -13,7 +14,7 @@ pub struct TestHarness {
authifier: Authifier,
pub db: Database,
sub: PubSub,
event_buffer: Vec<EventV1>,
event_buffer: Vec<(String, EventV1)>,
}
impl TestHarness {
@@ -24,20 +25,27 @@ impl TestHarness {
.await
.expect("valid rocket instance");
let db = DatabaseInfo::Auto.connect().await.expect("`Database`");
let mut sub = redis_kiss::open_pubsub_connection()
.await
.expect("`PubSub`");
sub.psubscribe("*").await.unwrap();
let db = client
.rocket()
.state::<Database>()
.expect("`Database`")
.clone();
let authifier = client
.rocket()
.state::<Authifier>()
.expect("`Authifier`")
.clone();
TestHarness {
client,
authifier: Authifier {
database: db.clone().into(),
config: revolt_quark::util::authifier::config(),
event_channel: None,
},
authifier,
db,
sub,
event_buffer: vec![],
@@ -80,12 +88,12 @@ impl TestHarness {
(account, session, user)
}
pub async fn wait_for_event<F>(&mut self, predicate: F) -> EventV1
pub async fn wait_for_event<F>(&mut self, topic: &str, predicate: F) -> EventV1
where
F: Fn(&EventV1) -> bool,
{
for event in &self.event_buffer {
if predicate(event) {
for (msg_topic, event) in &self.event_buffer {
if topic == msg_topic && predicate(event) {
// does not remove from buffer
return event.clone();
}
@@ -93,13 +101,15 @@ impl TestHarness {
let mut stream = self.sub.on_message();
while let Some(item) = stream.next().await {
let payload: EventV1 = redis_kiss::decode_payload(&item.unwrap()).unwrap();
let item = item.unwrap();
let msg_topic = item.get_channel_name();
let payload: EventV1 = redis_kiss::decode_payload(&item).unwrap();
if predicate(&payload) {
if topic == msg_topic && predicate(&payload) {
return payload;
}
self.event_buffer.push(payload);
self.event_buffer.push((msg_topic.to_string(), payload));
}
// WARNING: if predicate is never satisfied, this will never return
@@ -107,4 +117,19 @@ impl TestHarness {
unreachable!()
}
pub async fn wait_for_message(&mut self, channel_id: &str) -> v0::Message {
dbg!(&self.event_buffer);
match self
.wait_for_event(channel_id, |event| match event {
EventV1::Message(v0::Message { channel, .. }) => channel == channel_id,
_ => false,
})
.await
{
EventV1::Message(message) => message,
_ => unreachable!(),
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-quark"
version = "0.6.8"
version = "0.6.11"
edition = "2021"
license = "AGPL-3.0-or-later"
@@ -88,7 +88,7 @@ rocket_empty = { version = "0.1.1", optional = true, features = ["schema"] }
rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" }
# Authifier
authifier = { version = "1.0.7", features = ["async-std-runtime"] }
authifier = { version = "1.0.8", features = ["async-std-runtime"] }
# Sentry
sentry = "0.31.5"
+12 -11
View File
@@ -31,6 +31,18 @@ impl DatabaseInfo {
pub async fn connect(self) -> Result<Database, String> {
Ok(match self {
DatabaseInfo::Auto => {
if let Ok(test_db) = env::var("TEST_DB") {
return match test_db.as_str() {
"REFERENCE" => DatabaseInfo::Dummy.connect().await,
"MONGODB" => {
DatabaseInfo::MongoDb(env::var("MONGODB").expect("`MONGODB` env"))
.connect()
.await
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
};
}
if let Ok(uri) = env::var("MONGODB") {
return DatabaseInfo::MongoDb(uri).connect().await;
}
@@ -61,17 +73,6 @@ impl Deref for Database {
}
}
impl From<Database> for authifier::Database {
fn from(val: Database) -> Self {
match val {
Database::Dummy(_) => authifier::Database::default(),
Database::MongoDb(MongoDb(client)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
}
}
}
impl From<Database> for revolt_database::Database {
fn from(val: Database) -> Self {
match val {
+1
View File
@@ -143,6 +143,7 @@ pub enum EventV1 {
id: String,
server: Server,
channels: Vec<Channel>,
emojis: Vec<Emoji>,
},
/// Update existing server
+2 -2
View File
@@ -401,6 +401,7 @@ impl State {
id,
server,
channels,
emojis: _,
} => {
self.insert_subscription(id.clone());
self.cache.servers.insert(id.clone(), server.clone());
@@ -568,8 +569,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;
@@ -1,4 +1,6 @@
use crate::models::server_member::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::models::server_member::{
FieldsMember, Member, MemberCompositeKey, MemberWithRoles, PartialMember,
};
use crate::{AbstractServerMember, Result};
use super::super::DummyDb;
@@ -8,6 +10,9 @@ impl AbstractServerMember for DummyDb {
async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> {
Ok(Member::new(server.into(), user.into()))
}
async fn fetch_member_with_roles(&self, server: &str, user: &str) -> Result<MemberWithRoles> {
Ok(MemberWithRoles::new(server.into(), user.into()))
}
async fn insert_member(&self, member: &Member) -> Result<()> {
info!("Create {member:?}");
@@ -192,7 +192,7 @@ impl Message {
/// 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
if self.reactions.len() >= 20 {
if self.reactions.len() >= 20 && !self.reactions.contains_key(emoji) {
return Err(Error::InvalidOperation);
}
@@ -382,11 +382,16 @@ impl BulkMessageResponse {
db: &Database,
channel: Option<&Channel>,
messages: Vec<Message>,
user: &User,
include_users: Option<bool>,
) -> Result<BulkMessageResponse> {
if let Some(true) = include_users {
let user_ids = messages.get_user_ids();
let users = User::fetch_foreign_users(db, &user_ids).await?;
let users = User::fetch_foreign_users(db, &user_ids)
.await?
.into_iter()
.map(|x| x.with_relationship(user))
.collect();
Ok(match channel {
Some(Channel::TextChannel { server, .. })
@@ -206,6 +206,8 @@ impl Server {
}
}
let emojis = db.fetch_emoji_by_parent_id(&self.id).await?;
EventV1::ServerMemberJoin {
id: self.id.clone(),
user: user.id.clone(),
@@ -217,6 +219,7 @@ impl Server {
id: self.id.clone(),
server: self.clone(),
channels: channels.clone(),
emojis,
}
.private(user.id.clone())
.await;

Some files were not shown because too many files have changed in this diff Show More