Compare commits

...
60 Commits
Author SHA1 Message Date
Paul Makles 0fc59b8e2a fix: add validator dependency to database 2024-04-07 23:49:09 +01:00
Paul Makles 265d3ba8ef chore: bump version to 0.7.0 2024-04-07 23:45:29 +01:00
Paul Makles 20d398d02c chore: remove quark dependency from delta 🎉
closes #283

fix: allow setting port and use_tls from config
closes #143
2024-04-07 23:41:31 +01:00
Paul Makles 6bc74749d2 refactor(quark): port message_query, message_search
#283
2024-04-07 23:13:45 +01:00
Paul Makles 301676fb54 refactor(quark): port report_content & code clean-up
#283
2024-04-07 22:57:55 +01:00
Paul Makles 6b488f347e feat(core/database): implement reports & snapshots 2024-04-07 22:42:46 +01:00
Paul Makles 1a8e43d280 ci: also update the first job 2024-04-07 22:29:24 +01:00
Paul Makles acb4190f70 fix: delete associated server objects 2024-04-07 22:26:56 +01:00
Paul Makles 9e42567050 refactor(quark): port roles_create, roles_delete, roles_edit, roles_fetch, server_ack, server_delete, server_edit, server_fetch
#283
2024-04-07 22:26:56 +01:00
Sophie L 78fb6e1982 fix(ci): remnants from branch rename 2024-04-07 22:26:31 +01:00
Paul Makles 9de963fb56 refactor(quark): port member_edit, member_experimental_query, member_fetch_all, member_remove, permissions_set_default, permissions_set
#283
2024-04-07 21:35:45 +01:00
Paul Makles 03340a2b5a refactor(quark): port ban_create, ban_list, ban_remove, emoji_list, invites_fetch
#283
2024-04-07 20:48:36 +01:00
Paul Makles b2d3344ddd refactor(quark): port edit_user, fetch_dms, fetch_profile, fetch_self, fetch_user_flags, fetch_user, find_mutual, open_dm
#283
2024-04-07 19:38:08 +01:00
Paul Makles 49bb235938 refactor(quark): port add_friend, block_user, change_username, remove_friend, send_friend_request, unblock_user
#283
2024-04-07 18:30:59 +01:00
Paul Makles 4e7049d51e fix: fully qualify create_error in create_database_error macro 2024-04-07 18:11:37 +01:00
Paul Makles 2de7598f8d refactor(quark): port get/set_settings, get_unreads
#283
2024-04-07 18:10:11 +01:00
Paul Makles f6a565385e refactor(quark): port invite_delete, invite_fetch, invite_join 2024-04-07 17:47:59 +01:00
Paul Makles 569bd1d5e1 refactor(quark): port message_react, message_send, message_unreact, permissions_set_default, permissions_set
#283
2024-04-07 17:12:59 +01:00
Paul Makles aca1fe6dff refactor(quark): port message_edit, message_fetch, voice_join 2024-04-07 16:47:46 +01:00
Paul Makles aded2e3239 fix: add elevation check to role edit route 2024-04-07 15:52:14 +01:00
Paul Makles 54a4eff623 refactor(quark): port channel_ack, channel_delete, channel_ edit, group_remove_member, invite_create, members_fetch, message_bulk_delete, message_clear_reactions, message_delete
#283
2024-04-07 15:41:43 +01:00
Paul Makles 50c36dcefd docs: update environment variables [skip ci] 2024-03-29 11:06:09 +00:00
insertish f16c275bcb ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2024-03-28 13:37:54 +00:00
Paul Makles 74ff072199 docs: deployment lifecycle added to README 2024-03-13 22:43:49 +00:00
Paul Makles 52431bd1dd docs: update README [skip ci] 2024-03-13 22:08:31 +00:00
Paul Makles fa67e87467 chore: switch branch name to main [skip ci] 2024-03-13 22:08:23 +00:00
Bob Bobs 8f9b18444a chore(bonfire): log errors from websocket stream 2024-03-13 21:47:34 +00:00
Paul Makles 7b18e983c9 chore: faster tag and release procedure 2024-02-12 21:40:51 +00:00
Paul Makles c0e64c9052 chore: bump version 2024-02-12 21:34:49 +00:00
Paul Makles 0d54ccd342 chore: update justfile for faster releases 2024-02-12 21:33:01 +00:00
Paul Makles d4d5b23c7d fix: use a valid default permission set for groups 2024-02-12 21:23:38 +00:00
Paul Makles a641034895 test: write a unit test for group chat permissions 2024-02-12 21:19:36 +00:00
Paul Makles 9dd24526ef chore: clippy recommendations 2024-02-12 21:13:31 +00:00
Bob Bobs 6d4f772d78 fix(ci): only start run command in background 2024-02-12 21:12:24 +00:00
Bob Bobs 4c4dada3da fix(ci): build API then run in background to prevent timeout 2024-02-11 14:16:31 +00:00
Paul Makles cf82972a80 chore: bump version 2024-02-10 23:51:04 +00:00
Bob Bobs ea00f0fec1 fix(delta): set ratelimit headers on 429 responses 2024-02-10 23:49:15 +00:00
Bob Bobs 226dbca6e0 fix(core/models): use IndexMap for reactions 2024-02-10 23:45:34 +00:00
Bob Bobs e4b5a23f71 fix(delta): MemberResponse only include roles member has 2024-02-10 23:44:20 +00:00
Bob Bobs 9663caa1ff fix(delta): use untagged enum for MemberResponse 2024-02-10 23:44:20 +00:00
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
193 changed files with 5681 additions and 2844 deletions
+25
View File
@@ -1,5 +1,7 @@
# MongoDB URI
MONGODB=mongodb://localhost
AUTUMN_MONGO_URI=mongodb://database
REDIS_URI=redis://localhost/
# URL to where the Revolt app is publicly accessible
REVOLT_APP_URL=http://local.revolt.chat:5000
@@ -80,3 +82,26 @@ REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nf
##
# VOSO_MANAGE_TOKEN=CHANGEME
##
## Autumn configuration
##
# S3 Region
AUTUMN_S3_REGION=minio
# S3 Endpoint
AUTUMN_S3_ENDPOINT=http://minio:9000
# MinIO Root User
MINIO_ROOT_USER=minioautumn
# MinIO Root Password
MINIO_ROOT_PASSWORD=minioautumn
# AWS Access Key ID
AWS_ACCESS_KEY_ID=minioautumn
# AWS Secret Key
AWS_SECRET_ACCESS_KEY=minioautumn
+2 -2
View File
@@ -3,7 +3,7 @@ name: Docker Test & Publish
on:
push:
branches:
- "master"
- "main"
tags:
- "*"
paths-ignore:
@@ -15,7 +15,7 @@ on:
- "README"
pull_request:
branches:
- "master"
- "main"
paths:
- "Dockerfile"
workflow_dispatch:
+7 -7
View File
@@ -3,7 +3,7 @@ name: Rust build, test, and generate specification
on:
push:
pull_request:
branches: [master]
branches: [main]
env:
CARGO_TERM_COLOR: always
@@ -53,20 +53,20 @@ jobs:
cargo nextest run
- name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
env:
TEST_DB: REFERENCE
run: |
cargo run --bin revolt-delta &
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
- name: Wait for API to go up
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: nev7n/wait_for_response@v1
with:
url: "http://localhost:8000/"
- name: Checkout API repository
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: actions/checkout@v3
with:
repository: revoltchat/api
@@ -74,11 +74,11 @@ jobs:
token: ${{ secrets.PAT }}
- name: Download OpenAPI specification
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
- name: Commit changes
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: EndBug/add-and-commit@v4
with:
cwd: "api"
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectV2(number: 3) {
projectV2(number: 5) {
id
fields(first:20) {
nodes {
@@ -35,7 +35,7 @@ jobs:
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
Generated
+286 -112
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",
]
@@ -588,9 +594,19 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
[[package]]
name = "bytes"
version = "1.1.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "bytes-utils"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35"
dependencies = [
"bytes 1.5.0",
"either",
]
[[package]]
name = "cache-padded"
@@ -612,7 +628,7 @@ dependencies = [
"instant",
"once_cell",
"thiserror",
"tokio 1.18.2",
"tokio 1.35.1",
]
[[package]]
@@ -712,11 +728,11 @@ version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"futures-core",
"memchr",
"pin-project-lite 0.2.9",
"tokio 1.18.2",
"pin-project-lite 0.2.13",
"tokio 1.35.1",
"tokio-util 0.7.2",
]
@@ -784,6 +800,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "cookie-factory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
[[package]]
name = "core-foundation"
version = "0.9.3"
@@ -809,6 +831,12 @@ dependencies = [
"libc",
]
[[package]]
name = "crc16"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
[[package]]
name = "crossbeam-queue"
version = "0.3.5"
@@ -1002,7 +1030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a2561fd313df162315935989dceb8c99db4ee1933358270a57a3cfb8c957f3"
dependencies = [
"crossbeam-queue",
"tokio 1.18.2",
"tokio 1.35.1",
]
[[package]]
@@ -1348,6 +1376,15 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -1371,14 +1408,40 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "fred"
version = "8.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3b2a2ac060e3266004c552235c241b481e438e2b1ea75715ea1176914ef2868"
dependencies = [
"arc-swap",
"async-trait",
"bytes 1.5.0",
"bytes-utils",
"crossbeam-queue",
"float-cmp",
"futures",
"lazy_static",
"log",
"parking_lot",
"rand 0.8.5",
"redis-protocol",
"semver 1.0.9",
"socket2 0.5.5",
"tokio 1.35.1",
"tokio-stream",
"tokio-util 0.7.2",
"url",
"urlencoding",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@@ -1460,7 +1523,7 @@ dependencies = [
"futures-io",
"memchr",
"parking",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"waker-fn",
]
@@ -1472,7 +1535,7 @@ checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06"
dependencies = [
"futures-channel",
"futures-task",
"tokio 1.18.2",
"tokio 1.35.1",
]
[[package]]
@@ -1517,7 +1580,7 @@ dependencies = [
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"pin-utils",
"slab",
]
@@ -1661,7 +1724,7 @@ version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"fnv",
"futures-core",
"futures-sink",
@@ -1669,7 +1732,7 @@ dependencies = [
"http",
"indexmap",
"slab",
"tokio 1.18.2",
"tokio 1.35.1",
"tokio-util 0.7.2",
"tracing",
]
@@ -1802,7 +1865,7 @@ version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"fnv",
"itoa 1.0.2",
]
@@ -1823,9 +1886,9 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"http",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
]
[[package]]
@@ -1885,7 +1948,7 @@ version = "0.14.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"futures-channel",
"futures-core",
"futures-util",
@@ -1895,9 +1958,9 @@ dependencies = [
"httparse",
"httpdate 1.0.2",
"itoa 1.0.2",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"socket2 0.4.4",
"tokio 1.18.2",
"tokio 1.35.1",
"tower-service",
"tracing",
"want",
@@ -1922,10 +1985,10 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"hyper 0.14.19",
"native-tls",
"tokio 1.18.2",
"tokio 1.35.1",
"tokio-native-tls",
]
@@ -1946,6 +2009,16 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "if_chain"
version = "1.0.2"
@@ -2183,7 +2256,7 @@ dependencies = [
"futures-util",
"hostname",
"httpdate 1.0.2",
"idna",
"idna 0.2.3",
"mime",
"native-tls",
"nom",
@@ -2194,9 +2267,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.126"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libgit2-sys"
@@ -2429,14 +2502,13 @@ dependencies = [
[[package]]
name = "mio"
version = "0.8.3"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
"windows-sys 0.48.0",
]
[[package]]
@@ -2466,7 +2538,7 @@ dependencies = [
"log",
"metrics",
"thiserror",
"tokio 1.18.2",
"tokio 1.35.1",
"tracing",
"tracing-subscriber",
]
@@ -2518,7 +2590,7 @@ dependencies = [
"strsim",
"take_mut",
"thiserror",
"tokio 1.18.2",
"tokio 1.35.1",
"tokio-rustls",
"tokio-util 0.7.2",
"trust-dns-proto",
@@ -2534,7 +2606,7 @@ version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"encoding_rs",
"futures-util",
"http",
@@ -2543,7 +2615,7 @@ dependencies = [
"memchr",
"mime",
"spin 0.9.3",
"tokio 1.18.2",
"tokio 1.35.1",
"tokio-util 0.6.10",
"version_check",
]
@@ -2846,7 +2918,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
"windows-sys 0.36.1",
]
[[package]]
@@ -2924,9 +2996,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.1.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
@@ -2999,9 +3071,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777"
[[package]]
name = "pin-project-lite"
version = "0.2.9"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@@ -3114,9 +3186,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.56"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
@@ -3334,15 +3406,15 @@ source = "git+https://github.com/revoltchat/redis-rs?rev=1a41faf356fd21aebba71ce
dependencies = [
"async-std",
"async-trait",
"bytes 1.1.0",
"bytes 1.5.0",
"combine",
"futures-util",
"itoa 1.0.2",
"percent-encoding",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"ryu",
"sha1_smol",
"tokio 1.18.2",
"tokio 1.35.1",
"tokio-util 0.7.2",
"url",
]
@@ -3354,14 +3426,14 @@ source = "git+https://github.com/revoltchat/redis-rs?rev=f8ca28ab85da59d2ccde526
dependencies = [
"async-std",
"async-trait",
"bytes 1.1.0",
"bytes 1.5.0",
"combine",
"futures-util",
"itoa 1.0.2",
"percent-encoding",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"ryu",
"tokio 1.18.2",
"tokio 1.35.1",
"tokio-util 0.7.2",
"url",
]
@@ -3382,6 +3454,20 @@ dependencies = [
"serde_json",
]
[[package]]
name = "redis-protocol"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c31deddf734dc0a39d3112e73490e88b61a05e83e074d211f348404cee4d2c6"
dependencies = [
"bytes 1.5.0",
"bytes-utils",
"cookie-factory",
"crc16",
"log",
"nom",
]
[[package]]
name = "redox_syscall"
version = "0.2.13"
@@ -3453,7 +3539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
dependencies = [
"base64 0.13.0",
"bytes 1.1.0",
"bytes 1.5.0",
"encoding_rs",
"futures-core",
"futures-util",
@@ -3469,11 +3555,11 @@ dependencies = [
"mime",
"native-tls",
"percent-encoding",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"serde",
"serde_json",
"serde_urlencoded",
"tokio 1.18.2",
"tokio 1.35.1",
"tokio-native-tls",
"url",
"wasm-bindgen",
@@ -3494,10 +3580,12 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"async-std",
"async-tungstenite",
"bincode",
"fred",
"futures",
"log",
"once_cell",
@@ -3512,19 +3600,23 @@ dependencies = [
[[package]]
name = "revolt-config"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"async-std",
"cached",
"config",
"dotenv",
"futures-locks",
"log",
"once_cell",
"pretty_env_logger",
"sentry",
"serde",
]
[[package]]
name = "revolt-database"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"async-lock",
"async-recursion",
@@ -3564,12 +3656,13 @@ dependencies = [
"ulid 1.0.0",
"unicode-segmentation",
"url-escape",
"validator 0.16.0",
"web-push 0.10.0",
]
[[package]]
name = "revolt-delta"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"async-channel",
"async-std",
@@ -3581,6 +3674,7 @@ dependencies = [
"env_logger",
"futures",
"impl_ops",
"iso8601-timestamp 0.2.11",
"lettre",
"linkify 0.6.0",
"log",
@@ -3596,7 +3690,6 @@ dependencies = [
"revolt-database",
"revolt-models",
"revolt-permissions",
"revolt-quark",
"revolt-result",
"revolt_rocket_okapi",
"rocket",
@@ -3615,7 +3708,7 @@ dependencies = [
[[package]]
name = "revolt-models"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"indexmap",
"iso8601-timestamp 0.2.11",
@@ -3624,6 +3717,7 @@ dependencies = [
"revolt-config",
"revolt-permissions",
"revolt_optional_struct",
"rocket",
"schemars",
"serde",
"validator 0.16.0",
@@ -3631,7 +3725,7 @@ dependencies = [
[[package]]
name = "revolt-permissions"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"async-std",
"async-trait",
@@ -3646,7 +3740,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"async-std",
"log",
@@ -3657,7 +3751,7 @@ dependencies = [
[[package]]
name = "revolt-quark"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"async-lock",
"async-recursion",
@@ -3710,7 +3804,7 @@ dependencies = [
[[package]]
name = "revolt-result"
version = "0.6.8"
version = "0.7.0"
dependencies = [
"revolt_okapi",
"revolt_rocket_okapi",
@@ -3830,7 +3924,7 @@ dependencies = [
"atomic",
"atty",
"binascii",
"bytes 1.1.0",
"bytes 1.5.0",
"either",
"figment",
"futures",
@@ -3840,7 +3934,7 @@ dependencies = [
"multer",
"num_cpus",
"parking_lot",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"rand 0.8.5",
"ref-cast",
"rocket_codegen",
@@ -3850,7 +3944,7 @@ dependencies = [
"state",
"tempfile",
"time 0.3.17",
"tokio 1.18.2",
"tokio 1.35.1",
"tokio-stream",
"tokio-util 0.7.2",
"ubyte",
@@ -3933,14 +4027,14 @@ dependencies = [
"memchr",
"pear",
"percent-encoding",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"ref-cast",
"serde",
"smallvec",
"stable-pattern",
"state",
"time 0.3.17",
"tokio 1.18.2",
"tokio 1.35.1",
"uncased",
]
@@ -4088,7 +4182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
dependencies = [
"lazy_static",
"windows-sys",
"windows-sys 0.36.1",
]
[[package]]
@@ -4222,7 +4316,7 @@ dependencies = [
"sentry-debug-images",
"sentry-panic",
"sentry-tracing",
"tokio 1.18.2",
"tokio 1.35.1",
"ureq",
]
@@ -4523,6 +4617,16 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "socket2"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "spin"
version = "0.5.2"
@@ -4783,33 +4887,32 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.18.2"
version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395"
checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104"
dependencies = [
"bytes 1.1.0",
"backtrace",
"bytes 1.5.0",
"libc",
"memchr",
"mio 0.8.3",
"mio 0.8.10",
"num_cpus",
"once_cell",
"parking_lot",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"signal-hook-registry",
"socket2 0.4.4",
"socket2 0.5.5",
"tokio-macros",
"winapi 0.3.9",
"windows-sys 0.48.0",
]
[[package]]
name = "tokio-macros"
version = "1.7.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote 1.0.26",
"syn 1.0.107",
"syn 2.0.15",
]
[[package]]
@@ -4819,7 +4922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
dependencies = [
"native-tls",
"tokio 1.18.2",
"tokio 1.35.1",
]
[[package]]
@@ -4829,7 +4932,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
dependencies = [
"rustls",
"tokio 1.18.2",
"tokio 1.35.1",
"webpki",
]
@@ -4840,8 +4943,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
dependencies = [
"futures-core",
"pin-project-lite 0.2.9",
"tokio 1.18.2",
"pin-project-lite 0.2.13",
"tokio 1.35.1",
]
[[package]]
@@ -4874,12 +4977,12 @@ version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"futures-core",
"futures-sink",
"log",
"pin-project-lite 0.2.9",
"tokio 1.18.2",
"pin-project-lite 0.2.13",
"tokio 1.35.1",
]
[[package]]
@@ -4888,12 +4991,12 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c"
dependencies = [
"bytes 1.1.0",
"bytes 1.5.0",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite 0.2.9",
"tokio 1.18.2",
"pin-project-lite 0.2.13",
"tokio 1.35.1",
"tracing",
]
@@ -4932,7 +5035,7 @@ checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
dependencies = [
"cfg-if 1.0.0",
"log",
"pin-project-lite 0.2.9",
"pin-project-lite 0.2.13",
"tracing-attributes",
"tracing-core",
]
@@ -5010,7 +5113,7 @@ dependencies = [
"futures-channel",
"futures-io",
"futures-util",
"idna",
"idna 0.2.3",
"ipnet",
"lazy_static",
"log",
@@ -5018,7 +5121,7 @@ dependencies = [
"smallvec",
"thiserror",
"tinyvec",
"tokio 1.18.2",
"tokio 1.35.1",
"url",
]
@@ -5038,7 +5141,7 @@ dependencies = [
"resolv-conf",
"smallvec",
"thiserror",
"tokio 1.18.2",
"tokio 1.35.1",
"trust-dns-proto",
]
@@ -5056,7 +5159,7 @@ checksum = "d96a2dea40e7570482f28eb57afbe42d97551905da6a9400acc5c328d24004f5"
dependencies = [
"base64 0.13.0",
"byteorder",
"bytes 1.1.0",
"bytes 1.5.0",
"http",
"httparse",
"log",
@@ -5169,9 +5272,9 @@ dependencies = [
[[package]]
name = "unicode-bidi"
version = "0.3.8"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
@@ -5181,9 +5284,9 @@ checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
@@ -5243,13 +5346,12 @@ dependencies = [
[[package]]
name = "url"
version = "2.2.2"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"idna 0.5.0",
"percent-encoding",
"serde",
]
@@ -5263,6 +5365,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf-8"
version = "0.7.6"
@@ -5295,7 +5403,7 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f07b0a1390e01c0fc35ebb26b28ced33c9a3808f7f9fbe94d3cc01e233bfeed5"
dependencies = [
"idna",
"idna 0.2.3",
"lazy_static",
"regex",
"serde",
@@ -5310,7 +5418,7 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591"
dependencies = [
"idna",
"idna 0.2.3",
"lazy_static",
"regex",
"serde",
@@ -5626,43 +5734,109 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winreg"
version = "0.7.0"
+98 -20
View File
@@ -2,34 +2,112 @@
This is a monorepo for the Revolt backend.
| Crate | Path | Description |
| ---------------- | ---------------------------------------------- | ------------------------------------ |
| `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
| `quark` | [crates/quark](crates/quark) | Models and logic |
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer |
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer |
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |-->
| Crate | Path | Description |
| ------------------ | -------------------------------------------------- | --------------------------------- |
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |
| `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
| `quark` | [crates/quark](crates/quark) | Models and logic (**DEPRECATED**) |
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
## Resources
### Revolt
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing)
## Minimum Supported Rust Version
Rust 1.70 or higher.
## Contributing
## Development Guide
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
Please note that a pull request should only take care of one issue so that we can review it quickly.
Before getting started, you'll want to install:
- Rust toolchain (rustup recommended)
- Docker
- Git
- mold (optional, faster compilation)
> A **default.nix** is available for Nix users!
> Just run `nix-shell` and continue.
Now you can clone and build the project:
```bash
git clone https://github.com/revoltchat/backend revolt-backend
cd revolt-backend
cargo build
```
If you want to run the API and event servers:
```bash
# create environment file (will be deprecated in future)
cp .env.example .env
# (optionally) copy the default configuration file
cp crates/core/config/Revolt.toml Revolt.toml
# configure as necessary...
# start other necessary services
docker compose up -d
# run the API server
cargo run --bin revolt-delta
# run the events server
cargo run --bin revolt-bonfire
# hint:
# mold -run <cargo build, cargo run, etc...>
```
You can start a web client by doing the following:
```bash
# if you do not have yarn yet and have a modern Node.js:
corepack enable
# clone the web client and run it:
git clone --recursive https://github.com/revoltchat/revite
cd revite
yarn
yarn build:deps
yarn dev --port 3001
```
Then go to https://local.revolt.chat:3001
## Deployment Guide
### Cutting new crate releases
Begin by bumping crate versions:
```bash
just patch # 0.0.X
just minor # 0.X.0
just major # X.0.0
```
Then commit the changes to package files.
Proceed to publish all the new crates:
```bash
just publish
```
### Cutting new binary releases
Tag and push a new release by running:
```bash
just release
```
## License
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). Please check individual crates for further license information.
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
**Individual crates may supply their own licenses!**
+5 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.6.8"
version = "0.7.0"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -18,6 +18,7 @@ querystring = "1.1.0"
revolt-quark = { path = "../quark" }
# serde
bincode = "1.3.3"
serde_json = "1.0.79"
rmp-serde = "1.0.0"
serde = "1.0.136"
@@ -34,3 +35,6 @@ async-std = { version = "1.8.0", features = [
# core
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
sentry = "0.31.5"
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }
+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:?}");
});
}
}
+253 -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,264 @@ use revolt_quark::{
state::{State, SubscriptionStateChange},
},
models::{user::UserHint, User},
redis_kiss, Database,
redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI},
Database,
};
use async_std::{net::TcpStream, sync::Mutex, task};
use async_std::{net::TcpStream, sync::Mutex};
use crate::config::WebsocketHandshakeCallback;
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
/// Spawn a new WebSocket client worker given access to the database,
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
/// Start a new WebSocket client worker given access to the database,
/// the relevant TCP stream and the remote address of the client.
pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
// Spawn a new Async task to work on.
task::spawn(async move {
info!("User connected from {addr:?}");
pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
// Upgrade the TCP connection to a WebSocket connection.
// In this process, we also parse any additional parameters given.
// e.g. wss://example.com?format=json&version=1
let (sender, receiver) = oneshot::channel();
let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
stream,
WebsocketHandshakeCallback::from(sender),
None,
)
.await
else {
return;
};
// Verify we've received a valid config, otherwise we should just drop the connection.
let Ok(mut config) = receiver.await else {
return;
};
info!(
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
config.get_protocol_version(),
config.get_protocol_format()
);
// Upgrade the TCP connection to a WebSocket connection.
// In this process, we also parse any additional parameters given.
// e.g. wss://example.com?format=json&version=1
let (sender, receiver) = oneshot::channel();
if let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
stream,
WebsocketHandshakeCallback::from(sender),
None,
)
// Split the socket for simultaneously read and write.
let (mut write, mut read) = ws.split();
// If the user has not provided authentication, request information.
if config.get_session_token().is_none() {
while let Ok(message) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) =
config.decode(message.as_ref().unwrap())
{
config.set_session_token(token);
break;
}
}
}
// Try to authenticate the user.
let Some(token) = config.get_session_token().as_ref() else {
return;
};
let user = match User::from_token(db, token, UserHint::Any).await {
Ok(user) => user,
Err(err) => {
write.send(config.encode(&err)).await.ok();
return;
}
};
info!("User {addr:?} authenticated as @{}", user.username);
// Create local state.
let mut state = State::from(user);
let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated.
if write
.send(config.encode(&EventV1::Authenticated))
.await
{
// Verify we've received a valid config, otherwise we should just drop the connection.
if let Ok(mut config) = receiver.await {
info!(
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
config.get_protocol_version(),
config.get_protocol_format()
);
.is_err()
{
return;
}
// Split the socket for simultaneously read and write.
let (write, mut read) = ws.split();
let write = Mutex::new(write);
// Download required data to local cache and send Ready payload.
let Ok(ready_payload) = state.generate_ready_payload(db).await else {
return;
};
if write.send(config.encode(&ready_payload)).await.is_err() {
return;
}
// If the user has not provided authentication, request information.
if config.get_session_token().is_none() {
'outer: while let Ok(message) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) =
config.decode(message.as_ref().unwrap())
{
config.set_session_token(token);
break 'outer;
}
}
// Create presence session.
let (first_session, session_id) = create_session(&user_id, 0).await;
// If this was the first session, notify other users that we just went online.
if first_session {
state.broadcast_presence_change(true).await;
}
{
let write = Mutex::new(write);
// Create a PubSub connection to poll on.
let listener = listener(db, &mut state, addr, &config, &write).fuse();
// Read from WebSocket stream.
let worker = worker(addr, user_id.clone(), &config, read, &write).fuse();
// Pin both tasks.
pin_mut!(listener, worker);
// Wait for either disconnect or for listener to die.
select!(
() = listener => {},
() = worker => {}
);
}
// Clean up presence session.
let last_session = delete_session(&user_id, session_id).await;
// If this was the last session, notify other users that we just went offline.
if last_session {
state.broadcast_presence_change(false).await;
}
}
async fn listener(
db: &'static Database,
state: &mut State,
addr: SocketAddr,
config: &ProtocolConfiguration,
write: &Mutex<WsWriter>,
) {
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
let Ok(subscriber) = fred::types::Builder::from_config(redis_config).build_subscriber_client()
else {
return;
};
if subscriber.init().await.is_err() {
return;
};
let mut message_rx = subscriber.message_rx();
loop {
// Check for state changes for subscriptions.
match state.apply_state() {
SubscriptionStateChange::Reset => {
subscriber.unsubscribe_all().await.unwrap();
for id in state.iter_subscriptions() {
subscriber.subscribe(id).await.unwrap();
}
// Try to authenticate the user.
if let Some(token) = config.get_session_token().as_ref() {
match User::from_token(db, token, UserHint::Any).await {
Ok(user) => {
info!("User {addr:?} authenticated as @{}", user.username);
#[cfg(debug_assertions)]
info!("{addr:?} has reset their subscriptions");
}
SubscriptionStateChange::Change { add, remove } => {
for id in remove {
#[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}");
// Create local state.
let mut state = State::from(user);
let user_id = state.cache.user_id.clone();
subscriber.unsubscribe(id).await.unwrap();
}
// Create presence session.
let (first_session, session_id) = create_session(&user_id, 0).await;
for id in add {
#[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
// Notify socket we have authenticated.
write
.lock()
.await
.send(config.encode(&EventV1::Authenticated))
.await
.ok();
// Download required data to local cache and send Ready payload.
if let Ok(ready_payload) = state.generate_ready_payload(db).await {
write
.lock()
.await
.send(config.encode(&ready_payload))
.await
.ok();
// If this was the first session, notify other users that we just went online.
if first_session {
state.broadcast_presence_change(true).await;
}
// Create a PubSub connection to poll on.
let listener = async {
if let Ok(mut conn) = redis_kiss::open_pubsub_connection().await
{
loop {
// Check for state changes for subscriptions.
match state.apply_state() {
SubscriptionStateChange::Reset => {
for id in state.iter_subscriptions() {
conn.subscribe(id).await.unwrap();
}
#[cfg(debug_assertions)]
info!("{addr:?} has reset their subscriptions");
}
SubscriptionStateChange::Change { add, remove } => {
for id in remove {
#[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}");
conn.unsubscribe(id).await.unwrap();
}
for id in add {
#[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
conn.subscribe(id).await.unwrap();
}
}
SubscriptionStateChange::None => {}
}
// * Debug logging of current subscriptions.
/*#[cfg(debug_assertions)]
info!(
"User {addr:?} is subscribed to {:?}",
state
.iter_subscriptions()
.collect::<Vec<&String>>()
);*/
// Handle incoming events.
match conn.on_message().next().await.map(|res| {
res.map(|item|(
item.get_channel_name().to_string(),
redis_kiss::decode_payload::<EventV1>(&item),
))
}) {
Some(Ok((channel, item))) => {
if let Ok(mut event) = item {
if state
.handle_incoming_event_v1(
db, &mut event,
)
.await
&& write.lock().await
.send(config.encode(&event))
.await
.is_err()
{
break;
}
} else {
warn!("Failed to deserialise an event for {channel}!");
}
}
Some(Err(e)) => {
info!("Error while consuming pub/sub messages: {e:?}");
sentry::capture_error(&e);
break
}
// No more data, assume we disconnected or otherwise
// something bad occurred, so disconnect user.
None => break,
}
}
}
}
.fuse();
// Read from WebSocket stream.
let worker =
async {
while let Ok(Some(msg)) = read.try_next().await {
if let Ok(payload) = config.decode(&msg) {
match payload {
ClientMessage::BeginTyping { channel } => {
EventV1::ChannelStartTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::EndTyping { channel } => {
EventV1::ChannelStopTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::Ping { data, responded } => {
if responded.is_none() {
write
.lock()
.await
.send(config.encode(
&EventV1::Pong { data },
))
.await
.ok();
}
}
_ => {}
}
}
}
}
.fuse();
// Pin both tasks.
pin_mut!(listener, worker);
// Wait for either disconnect or for listener to die.
select!(
() = listener => {},
() = worker => {}
);
// * Combine the streams back once we are ready to disconnect.
/* ws = read.reunite(write).unwrap(); */
}
// Clean up presence session.
let last_session = delete_session(&user_id, session_id).await;
// If this was the last session, notify other users that we just went offline.
if last_session {
state.broadcast_presence_change(false).await;
}
}
Err(err) => {
write.lock().await.send(config.encode(&err)).await.ok();
}
}
subscriber.subscribe(id).await.unwrap();
}
}
// * Disconnect the WebSocket if it isn't already.
/*ws.close(Some(CloseFrame {
code: CloseCode::Normal,
reason: std::borrow::Cow::from(""),
}))
.await
.unwrap();*/
SubscriptionStateChange::None => {}
}
info!("User disconnected from {addr:?}");
});
// Handle incoming events.
let Ok(message) = message_rx.recv().await.map_err(|e| {
warn!("Error while consuming pub/sub messages: {e:?}");
sentry::capture_error(&e);
}) else {
return;
};
let event = match *REDIS_PAYLOAD_TYPE {
PayloadType::Json => message
.value
.as_str()
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
PayloadType::Msgpack => message
.value
.as_bytes()
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
PayloadType::Bincode => message
.value
.as_bytes()
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
};
let Some(mut event) = event else {
warn!("Failed to deserialise an event for {}!", message.channel);
return;
};
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send {
continue;
}
let result = write.lock().await.send(config.encode(&event)).await;
if let Err(e) = result {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
warn!("Error while sending an event to {addr:?}: {e:?}");
}
return;
}
}
}
async fn worker(
addr: SocketAddr,
user_id: String,
config: &ProtocolConfiguration,
mut read: WsReader,
write: &Mutex<WsWriter>,
) {
loop {
let result = read.try_next().await;
let msg = match result {
Ok(Some(msg)) => msg,
Ok(None) => return,
Err(e) => {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
warn!("Error while reading an event from {addr:?}: {e:?}");
}
return;
}
};
let Ok(payload) = config.decode(&msg) else {
continue;
};
match payload {
ClientMessage::BeginTyping { channel } => {
EventV1::ChannelStartTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::EndTyping { channel } => {
EventV1::ChannelStopTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::Ping { data, responded } => {
if responded.is_none() {
write
.lock()
.await
.send(config.encode(&EventV1::Pong { data }))
.await
.ok();
}
}
_ => {}
}
}
}
+9 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.6.8"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -14,6 +14,7 @@ default = ["test"]
[dependencies]
# Utility
dotenv = "0.15.0"
config = "0.13.3"
cached = "0.44.0"
once_cell = "1.18.0"
@@ -24,3 +25,10 @@ serde = { version = "1", features = ["derive"] }
# Async
futures-locks = "0.7.1"
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Logging
log = "0.4.14"
pretty_env_logger = "0.4.0"
# Sentry
sentry = "0.31.5"
+4
View File
@@ -1,3 +1,5 @@
sentry_dsn = ""
[database]
mongodb = "mongodb://database"
redis = "redis://redis/"
@@ -33,6 +35,7 @@ api_key = ""
[api.security]
authifier_shield_key = ""
voso_legacy_token = ""
trust_cloudflare = false
[api.security.captcha]
hcaptcha_key = ""
@@ -42,6 +45,7 @@ hcaptcha_sitekey = ""
max_concurrent_connections = 50
[features]
webhooks_enabled = false
[features.limits]
+71
View File
@@ -56,6 +56,9 @@ pub struct ApiSmtp {
pub username: String,
pub password: String,
pub from_address: String,
pub reply_to: Option<String>,
pub port: Option<i32>,
pub use_tls: Option<bool>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -80,6 +83,7 @@ pub struct ApiSecurity {
pub authifier_shield_key: String,
pub voso_legacy_token: String,
pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool,
}
#[derive(Deserialize, Debug, Clone)]
@@ -131,6 +135,7 @@ pub struct FeaturesLimitsCollection {
#[derive(Deserialize, Debug, Clone)]
pub struct Features {
pub limits: FeaturesLimitsCollection,
pub webhooks_enabled: bool,
}
#[derive(Deserialize, Debug, Clone)]
@@ -139,6 +144,31 @@ pub struct Settings {
pub hosts: Hosts,
pub api: Api,
pub features: Features,
pub sentry_dsn: String,
}
impl Settings {
pub fn preflight_checks(&self) {
if self.api.smtp.host.is_empty() {
#[cfg(not(debug_assertions))]
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
panic!("Running in production without email is not recommended, set REVOLT_UNSAFE_NO_EMAIL=1 to override.");
}
#[cfg(debug_assertions)]
log::warn!("No SMTP settings specified! Remember to configure email.");
}
if self.api.security.captcha.hcaptcha_key.is_empty() {
#[cfg(not(debug_assertions))]
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
panic!("Running in production without CAPTCHA is not recommended, set REVOLT_UNSAFE_NO_CAPTCHA=1 to override.");
}
#[cfg(debug_assertions)]
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
}
}
}
pub async fn init() {
@@ -157,6 +187,47 @@ pub async fn config() -> Settings {
read().await.try_deserialize::<Settings>().unwrap()
}
/// Configure logging and common Rust variables
pub async fn setup_logging(release: &'static str) -> Option<sentry::ClientInitGuard> {
dotenv::dotenv().ok();
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
if std::env::var("ROCKET_ADDRESS").is_err() {
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
}
pretty_env_logger::init();
log::info!("Starting {release}");
let config = config().await;
if config.sentry_dsn.is_empty() {
None
} else {
Some(sentry::init((
config.sentry_dsn,
sentry::ClientOptions {
release: Some(release.into()),
..Default::default()
},
)))
}
}
#[macro_export]
macro_rules! configure {
() => {
let _sentry = $crate::setup_logging(concat!(
env!("CARGO_PKG_NAME"),
"@",
env!("CARGO_PKG_VERSION")
))
.await;
};
}
#[cfg(feature = "test")]
#[cfg(test)]
mod tests {
+9 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.6.8"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,11 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.6.8", path = "../config" }
revolt-result = { version = "0.6.8", path = "../result" }
revolt-models = { version = "0.6.8", path = "../models" }
revolt-presence = { version = "0.6.8", path = "../presence" }
revolt-permissions = { version = "0.6.8", path = "../permissions", features = [
revolt-config = { version = "0.7.0", path = "../config" }
revolt-result = { version = "0.7.0", path = "../result" }
revolt-models = { version = "0.7.0", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.7.0", path = "../presence" }
revolt-permissions = { version = "0.7.0", path = "../permissions", features = [
"serde",
"bson",
] }
@@ -45,6 +47,7 @@ decancer = "1.6.2"
deadqueue = "0.2.4"
linkify = { optional = true, version = "0.8.1" }
url-escape = { optional = true, version = "0.1.1" }
validator = { version = "0.16", features = ["derive"] }
isahc = { optional = true, version = "1.7", features = ["json"] }
# Serialisation
@@ -0,0 +1,28 @@
[
{
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Member",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "Member",
"discriminator": "0002"
},
{
"_object_type": "Channel",
"_id": "__ID:3__",
"channel_type": "Group",
"name": "My Group",
"owner": "__ID:0__",
"recipients": ["__ID:0__", "__ID:1__"]
}
]
@@ -0,0 +1,89 @@
[
{
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Moderator",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "User",
"discriminator": "0001"
},
{
"_object_type": "Channel",
"_id": "__ID:3__",
"channel_type": "TextChannel",
"name": "General",
"server": "__ID:4__",
"default_permissions": {
"a": 0,
"d": 1048576
},
"role_permissions": {
"__ID:5__": {
"a": 1048576,
"d": 0
}
}
},
{
"_object_type": "Server",
"_id": "__ID:4__",
"owner": "__ID:0__",
"name": "Server",
"channels": ["__ID:3__"],
"roles": {
"__ID:5__": {
"name": "Moderator",
"permissions": {
"a": 545270208,
"d": 0
},
"rank": 3
},
"__ID:6__": {
"name": "Owner",
"permissions": {
"a": 0,
"d": 0
},
"rank": 0
}
},
"default_permissions": 4000322560
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:0__",
"server": "__ID:4__"
},
"roles": ["__ID:6__"],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:1__",
"server": "__ID:4__"
},
"roles": ["__ID:5__"],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:2__",
"server": "__ID:4__"
},
"joined_at": 1698318340195
}
]
@@ -4,7 +4,8 @@ use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, User, UserSettings, Webhook,
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
UserSettings, Webhook,
};
database_derived!(
@@ -25,7 +26,7 @@ database_derived!(
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
}
);
+5 -5
View File
@@ -4,7 +4,7 @@ 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, User,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
UserSettings, Webhook,
};
use revolt_result::Error;
@@ -110,6 +110,7 @@ pub enum EventV1 {
id: String,
server: Server,
channels: Vec<Channel>,
emojis: Vec<Emoji>,
},
/// Update existing server
@@ -175,8 +176,8 @@ pub enum EventV1 {
/// Delete emoji
EmojiDelete { id: String },
/*/// New report
ReportCreate(Report), */
/// New report
ReportCreate(Report),
/// New channel
ChannelCreate(Channel),
@@ -243,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;
@@ -93,7 +93,7 @@ impl Bot {
Some(id.to_string()),
Some(PartialUser {
bot: Some(BotInformation {
owner: id.to_string(),
owner: owner.id.to_string(),
}),
..Default::default()
}),
@@ -1,12 +1,12 @@
use revolt_result::{create_error, Result};
use crate::Database;
use crate::{Channel, Database, User};
/* static ALPHABET: [char; 54] = [
static ALPHABET: [char; 54] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
]; */
];
auto_derived!(
/// Invite
@@ -56,9 +56,13 @@ impl Invite {
}
/// Create a new invite from given information
/*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> {
pub async fn create_channel_invite(
db: &Database,
creator: &User,
channel: &Channel,
) -> Result<Invite> {
let code = nanoid::nanoid!(8, &ALPHABET);
let invite = match &target {
let invite = match &channel {
Channel::Group { id, .. } => Ok(Invite::Group {
code,
creator: creator.id.clone(),
@@ -72,12 +76,12 @@ impl Invite {
channel: id.clone(),
})
}
_ => Err(Error::InvalidOperation),
_ => Err(create_error!(InvalidOperation)),
}?;
db.insert_invite(&invite).await?;
Ok(invite)
}*/
}
/// Resolve an invite by its ID or by a public server ID
pub async fn find(db: &Database, code: &str) -> Result<Invite> {
+168 -121
View File
@@ -5,8 +5,12 @@ use revolt_models::v0::{self, MessageAuthor};
use revolt_permissions::OverrideField;
use revolt_result::Result;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::{events::client::EventV1, Database, File, IntoDocumentPath, SystemMessage, User};
use crate::{
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
Server, SystemMessage, User,
};
auto_derived!(
#[serde(tag = "channel_type")]
@@ -189,6 +193,67 @@ 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,
@@ -231,6 +296,43 @@ impl Channel {
Ok(channel)
}
/// Create a DM (or return the existing one / saved messages)
pub async fn create_dm(db: &Database, user_a: &User, user_b: &User) -> Result<Channel> {
// Try to find existing channel
if let Ok(channel) = db.find_direct_message_channel(&user_a.id, &user_b.id).await {
Ok(channel)
} else {
let channel = if user_a.id == user_b.id {
// Create a new saved messages channel
Channel::SavedMessages {
id: Ulid::new().to_string(),
user: user_a.id.to_string(),
}
} else {
// Create a new DM channel
Channel::DirectMessage {
id: Ulid::new().to_string(),
active: true, // show by default
recipients: vec![user_a.id.clone(), user_b.id.clone()],
last_message_id: None,
}
};
db.insert_channel(&channel).await?;
match &channel {
Channel::DirectMessage { .. } => {
let event = EventV1::ChannelCreate(channel.clone().into());
event.clone().private(user_a.id.clone()).await;
event.private(user_b.id.clone()).await;
}
_ => {}
};
Ok(channel)
}
}
/// Add user to a group
pub async fn add_user_to_group(
&mut self,
@@ -243,6 +345,13 @@ impl Channel {
return Err(create_error!(AlreadyInGroup));
}
let config = config().await;
if recipients.len() >= config.features.limits.default.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size
}));
}
recipients.push(String::from(&user.id));
}
@@ -518,6 +627,28 @@ impl Channel {
}
}
/// Acknowledge a message
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
EventV1::ChannelAck {
id: self.id().to_string(),
user: user.to_string(),
message_id: message.to_string(),
}
.private(user.to_string())
.await;
crate::tasks::ack::queue(
self.id().to_string(),
user.to_string(),
AckEvent::AckMessage {
id: message.to_string(),
},
)
.await;
Ok(())
}
/// Remove user from a group
pub async fn remove_user_from_group(
&self,
@@ -563,8 +694,7 @@ impl Channel {
.await
.ok();
} else {
db.delete_channel(self).await?;
return Ok(());
return self.delete(db).await;
}
}
@@ -611,6 +741,9 @@ impl Channel {
pub async fn delete(&self, db: &Database) -> Result<()> {
let id = self.id().to_string();
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
// TODO: missing functionality:
// - group invites
// - channels list / categories list on server
db.delete_channel(self).await
}
}
@@ -627,130 +760,44 @@ impl IntoDocumentPath for FieldsChannel {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, OverrideField};
use crate::{fixture, util::permissions::DatabasePermissionQuery};
use crate::{
util::permissions::DatabasePermissionQuery, Channel, Member, MemberCompositeKey, Role,
Server, User,
};
#[async_std::test]
async fn permissions_group_channel() {
database_test!(|db| async move {
fixture!(db, "group_with_members",
owner user 0
member1 user 1
member2 user 2
channel channel 3);
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &member1).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &member2).channel(&channel);
assert!(!calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
});
}
#[async_std::test]
async fn permissions_text_channel() {
database_test!(|db| async move {
let owner = User::create(&db, "Owner".to_string(), None, None)
.await
.unwrap();
let moderator = User::create(&db, "Moderator".to_string(), None, None)
.await
.unwrap();
let user = User::create(&db, "User".to_string(), None, None)
.await
.unwrap();
let server_id = ulid::Ulid::new().to_string();
let channel = Channel::TextChannel {
id: ulid::Ulid::new().to_string(),
server: server_id.clone(),
name: "Channel".to_string(),
description: None,
icon: None,
last_message_id: None,
default_permissions: Some(OverrideField {
d: 1048576, // TODO: bitfield
..Default::default()
}),
role_permissions: HashMap::from([(
"01F9HFTSBWTNA2F4TMSV7VM3FG".to_string(),
OverrideField {
a: 1048576, // TODO: bitfield
..Default::default()
},
)]),
nsfw: false,
};
let server = Server {
id: server_id,
owner: owner.id.clone(),
name: "My Server".to_string(),
description: None,
channels: vec![channel.id()],
categories: None,
system_messages: None,
roles: HashMap::from([
(
"01F9HFTSBWTNA2F4TMSV7VM3FG".to_string(),
Role {
name: "Moderator".to_string(),
permissions: OverrideField {
a: 545270208, // TODO: explicit
..Default::default()
},
colour: None,
hoist: true,
rank: 3,
},
),
(
"01FBF9DNHSRPVTWFMNB3JNB8FK".to_string(),
Role {
name: "Owner".to_string(),
permissions: Default::default(),
colour: None,
hoist: true,
rank: 0,
},
),
]),
default_permissions: 4000322560, // TODO: use bitfield
icon: None,
banner: None,
flags: None,
nsfw: false,
analytics: false,
discoverable: false,
};
// TODO: proper creation
db.insert_channel(&channel).await.unwrap();
server.create(&db).await.unwrap();
db.insert_member(&Member {
id: MemberCompositeKey {
user: owner.id.clone(),
server: server.id.clone(),
},
roles: vec!["01FBF9DNHSRPVTWFMNB3JNB8FK".to_string()],
..Default::default()
})
.await
.unwrap();
db.insert_member(&Member {
id: MemberCompositeKey {
user: moderator.id.clone(),
server: server.id.clone(),
},
roles: vec!["01F9HFTSBWTNA2F4TMSV7VM3FG".to_string()],
..Default::default()
})
.await
.unwrap();
db.insert_member(&Member {
id: MemberCompositeKey {
user: user.id.clone(),
server: server.id.clone(),
},
..Default::default()
})
.await
.unwrap();
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)
@@ -1,6 +1,6 @@
use super::AbstractChannels;
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::Document;
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::{Bson, Document};
use futures::StreamExt;
use revolt_permissions::OverrideField;
use revolt_result::Result;
@@ -188,6 +188,125 @@ impl AbstractChannels for MongoDb {
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let id = channel.id().to_string();
let server_id = match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server)
}
_ => None,
};
// Delete invites and unreads.
self.delete_associated_channel_objects(Bson::String(id.to_string()))
.await?;
// Delete messages.
self.delete_bulk_messages(doc! {
"channel": &id
})
.await?;
// Remove from server object.
if let Some(server) = server_id {
let server = self.fetch_server(server).await?;
let mut update = doc! {
"$pull": {
"channels": &id
}
};
if let Some(sys) = &server.system_messages {
let mut unset = doc! {};
if let Some(cid) = &sys.user_joined {
if &id == cid {
unset.insert("system_messages.user_joined", 1_i32);
}
}
if let Some(cid) = &sys.user_left {
if &id == cid {
unset.insert("system_messages.user_left", 1_i32);
}
}
if let Some(cid) = &sys.user_kicked {
if &id == cid {
unset.insert("system_messages.user_kicked", 1_i32);
}
}
if let Some(cid) = &sys.user_banned {
if &id == cid {
unset.insert("system_messages.user_banned", 1_i32);
}
}
if !unset.is_empty() {
update.insert("$unset", unset);
}
}
self.col::<Document>("servers")
.update_one(
doc! {
"_id": server.id
},
update,
None,
)
.await
.map_err(|_| create_database_error!("update_one", "servers"))?;
}
// Delete associated attachments
self.delete_many_attachments(doc! {
"object_id": &id
})
.await?;
// Delete the channel itself
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
}
}
impl MongoDb {
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
// Delete all invites to these channels.
self.col::<Document>("channel_invites")
.delete_many(
doc! {
"channel": &id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
// Delete unread message objects on channels.
self.col::<Document>("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
.map(|_| ())?;
// update many attachments with parent id
// Delete all webhooks on this channel.
self.col::<Document>("webhooks")
.delete_many(
doc! {
"channel": &id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "webhooks"))
.map(|_| ())
}
}
@@ -24,13 +24,12 @@ impl AbstractEmojis for MongoDb {
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
query!(
self,
find_one,
find,
COL,
doc! {
"parent.id": parent_id
}
)?
.ok_or_else(|| create_error!(NotFound))
)
}
/// Fetch emoji by their parent ids
@@ -115,3 +115,21 @@ impl AbstractAttachments for MongoDb {
.map_err(|_| create_database_error!("update_one", COL))
}
}
impl MongoDb {
pub async fn delete_many_attachments(&self, projection: Document) -> Result<()> {
self.col::<Document>(COL)
.update_many(
projection,
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
}
}
@@ -4,18 +4,19 @@ use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_models::v0::{
self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification,
ReplyIntent, SendableEmbed, RE_MENTION,
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::idempotency::IdempotencyKey,
Channel, Database, Emoji, File,
Channel, Database, Emoji, File, User,
};
auto_derived_partial!(
@@ -155,6 +156,7 @@ auto_derived!(
}
/// Message Filter
#[derive(Default)]
pub struct MessageFilter {
/// Parent channel ID
pub channel: Option<String>,
@@ -212,6 +214,7 @@ impl Message {
author: MessageAuthor<'_>,
mut idempotency: IdempotencyKey,
generate_embeds: bool,
allow_mentions: bool,
) -> Result<Message> {
let config = config().await;
@@ -272,10 +275,12 @@ impl Message {
// Parse mentions in message.
let mut mentions = HashSet::new();
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());
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());
}
}
}
}
@@ -292,7 +297,7 @@ impl Message {
for ReplyIntent { id, mention } in entries {
let message = db.fetch_message(&id).await?;
if mention {
if mention && allow_mentions {
mentions.insert(message.author.to_owned());
}
@@ -438,6 +443,93 @@ impl Message {
Ok(())
}
/// Create text embed from sendable embed
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
embed.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let media = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
.await?,
)
} else {
None
};
Ok(Embed::Text(Text {
icon_url: embed.icon_url,
url: embed.url,
title: embed.title,
description: embed.description,
media: media.map(|m| m.into()),
colour: embed.colour,
}))
}
/// Update message data
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
self.apply_options(partial.clone());
db.update_message(&self.id, &partial).await?;
EventV1::MessageUpdate {
id: self.id.clone(),
channel: self.channel.clone(),
data: partial.into(),
}
.p(self.channel.clone())
.await;
Ok(())
}
/// Helper function to fetch many messages with users
pub async fn fetch_with_users(
db: &Database,
query: MessageQuery,
perspective: &User,
include_users: Option<bool>,
server_id: Option<String>,
) -> Result<BulkMessageResponse> {
let messages: Vec<v0::Message> = db
.fetch_messages(query)
.await?
.into_iter()
.map(Into::into)
.collect();
if let Some(true) = include_users {
let user_ids = messages
.iter()
.map(|m| m.author.clone())
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>();
let users = User::fetch_many_ids_as_mutuals(db, perspective, &user_ids).await?;
Ok(BulkMessageResponse::MessagesAndUsers {
messages,
users,
members: if let Some(server_id) = server_id {
Some(
db.fetch_members(&server_id, &user_ids)
.await?
.into_iter()
.map(Into::into)
.collect(),
)
} else {
None
},
})
} else {
Ok(BulkMessageResponse::JustMessages(messages))
}
}
/// Append content to message
pub async fn append(
db: &Database,
@@ -492,6 +584,40 @@ impl Message {
Ok(())
}
/// Add a reaction to a message
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
// Check how many reactions are already on the message
let config = config().await;
if self.reactions.len() >= config.features.limits.default.message_reactions
&& !self.reactions.contains_key(emoji)
{
return Err(create_error!(InvalidOperation));
}
// Check if the emoji is whitelisted
if !self.interactions.can_use(emoji) {
return Err(create_error!(InvalidOperation));
}
// Check if the emoji is usable by us
if !Emoji::can_use(db, emoji).await? {
return Err(create_error!(InvalidOperation));
}
// Send reaction event
EventV1::MessageReact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.id.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
// Add emoji
db.add_reaction(&self.id, emoji, &user.id).await
}
/// Validate the sum of content of a message is under threshold
pub fn validate_sum(
content: &Option<String>,
@@ -515,6 +641,95 @@ impl Message {
Err(create_error!(PayloadTooLarge))
}
}
/// Delete a message
pub async fn delete(self, db: &Database) -> Result<()> {
let file_ids: Vec<String> = self
.attachments
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
.unwrap_or_default();
if !file_ids.is_empty() {
db.mark_attachments_as_deleted(&file_ids).await?;
}
db.delete_message(&self.id).await?;
EventV1::MessageDelete {
id: self.id,
channel: self.channel.clone(),
}
.p(self.channel)
.await;
Ok(())
}
/// Bulk delete messages
pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec<String>) -> Result<()> {
let valid_ids = db
.fetch_messages_by_id(&ids)
.await?
.into_iter()
.filter(|msg| msg.channel == channel)
.map(|msg| msg.id)
.collect::<Vec<String>>();
db.delete_messages(channel, &valid_ids).await?;
EventV1::BulkMessageDelete {
channel: channel.to_string(),
ids: valid_ids,
}
.p(channel.to_string())
.await;
Ok(())
}
/// Remove a reaction from a message
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
// Check if it actually exists
let empty = if let Some(users) = self.reactions.get(emoji) {
if !users.contains(user) {
return Err(create_error!(NotFound));
}
users.len() == 1
} else {
return Err(create_error!(NotFound));
};
// Send reaction event
EventV1::MessageUnreact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
if empty {
// If empty, remove the reaction entirely
db.clear_reaction(&self.id, emoji).await
} else {
// Otherwise only remove that one reaction
db.remove_reaction(&self.id, emoji, user).await
}
}
/// Remove a reaction from a message
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
// Send reaction event
EventV1::MessageRemoveReaction {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
// Write to database
db.clear_reaction(&self.id, emoji).await
}
}
impl SystemMessage {
@@ -16,6 +16,9 @@ pub trait AbstractMessages: Sync + Send {
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
@@ -4,7 +4,9 @@ use mongodb::options::FindOptions;
use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage};
use crate::{
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
};
use super::AbstractMessages;
@@ -159,6 +161,21 @@ impl AbstractMessages for MongoDb {
}
}
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
self.find_with_options(
COL,
doc! {
"ids": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
@@ -278,3 +295,57 @@ impl AbstractMessages for MongoDb {
.map_err(|_| create_database_error!("delete_many", COL))
}
}
impl MongoDb {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone();
for_attachments.insert(
"attachments",
doc! {
"$exists": 1_i32
},
);
// Check if there are any attachments we need to delete.
let message_ids_with_attachments = self
.find_with_options::<_, DocumentId>(
COL,
for_attachments,
FindOptions::builder()
.projection(doc! { "_id": 1_i32 })
.build(),
)
.await
.map_err(|_| create_database_error!("find_many", "attachments"))?
.into_iter()
.map(|x| x.id)
.collect::<Vec<String>>();
// If we found any, mark them as deleted.
if !message_ids_with_attachments.is_empty() {
self.col::<Document>("attachments")
.update_many(
doc! {
"message_id": {
"$in": message_ids_with_attachments
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "attachments"))?;
}
// And then delete said messages.
self.col::<Document>(COL)
.delete_many(projection, None)
.await
.map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL))
}
}
@@ -1,3 +1,4 @@
use futures::future::try_join_all;
use indexmap::IndexSet;
use revolt_result::Result;
@@ -60,7 +61,7 @@ impl AbstractMessages for ReferenceDb {
.cloned()
.collect();
// TODO: sorting, etc (will be required for tests)
// FIXME: sorting, etc (will be required for tests)
Ok(matched_messages)
@@ -176,6 +177,11 @@ impl AbstractMessages for ReferenceDb {
}*/
}
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
try_join_all(ids.iter().map(|id| self.fetch_message(id))).await
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
let mut messages = self.messages.lock().await;
+6
View File
@@ -8,6 +8,8 @@ mod emojis;
mod files;
mod messages;
mod ratelimit_events;
mod safety_reports;
mod safety_snapshots;
mod server_bans;
mod server_members;
mod servers;
@@ -24,6 +26,8 @@ pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use ratelimit_events::*;
pub use safety_reports::*;
pub use safety_snapshots::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
@@ -45,6 +49,8 @@ pub trait AbstractDatabase:
+ files::AbstractAttachments
+ messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents
+ safety_reports::AbstractReport
+ safety_snapshots::AbstractSnapshot
+ server_bans::AbstractServerBans
+ server_members::AbstractServerMembers
+ servers::AbstractServers
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,22 @@
use revolt_models::v0::{ReportStatus, ReportedContent};
auto_derived!(
/// User-generated platform moderation report
pub struct Report {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Id of the user creating this report
pub author_id: String,
/// Reported content
pub content: ReportedContent,
/// Additional report context
pub additional_context: String,
/// Status of the report
#[serde(flatten)]
pub status: ReportStatus,
/// Additional notes included on the report
#[serde(default)]
pub notes: String,
}
);
@@ -0,0 +1,12 @@
use revolt_result::Result;
use crate::Report;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractReport: Sync + Send {
/// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()>;
}
@@ -0,0 +1,16 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::Report;
use super::AbstractReport;
static COL: &str = "safety_reports";
#[async_trait]
impl AbstractReport for MongoDb {
/// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()> {
query!(self, insert_one, COL, &report).map(|_| ())
}
}
@@ -0,0 +1,20 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::Report;
use super::AbstractReport;
#[async_trait]
impl AbstractReport for ReferenceDb {
/// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()> {
let mut reports = self.safety_reports.lock().await;
if reports.contains_key(&report.id) {
Err(create_database_error!("insert", "report"))
} else {
reports.insert(report.id.to_string(), report.clone());
Ok(())
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,121 @@
use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, Server, User};
auto_derived!(
/// Snapshot of some content
pub struct Snapshot {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Report parent Id
pub report_id: String,
/// Snapshot of content
pub content: SnapshotContent,
}
/// Enum to map into different models
/// that can be saved in a snapshot
#[serde(tag = "_type")]
pub enum SnapshotContent {
Message {
/// Context before the message
#[serde(rename = "_prior_context", default)]
prior_context: Vec<Message>,
/// Context after the message
#[serde(rename = "_leading_context", default)]
leading_context: Vec<Message>,
/// Message
#[serde(flatten)]
message: Message,
},
Server(Server),
User(User),
}
);
impl SnapshotContent {
/// Generate snapshot from a given message
pub async fn generate_from_message(
db: &Database,
message: Message,
) -> Result<(SnapshotContent, Vec<String>)> {
// Collect message attachments
let files = message
.attachments
.as_ref()
.map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect())
.unwrap_or_default();
// Collect prior context
let prior_context = db
.fetch_messages(MessageQuery {
filter: MessageFilter {
channel: Some(message.channel.to_string()),
..Default::default()
},
limit: Some(15),
time_period: MessageTimePeriod::Absolute {
before: Some(message.id.to_string()),
after: None,
sort: Some(MessageSort::Latest),
},
})
.await?;
// Collect leading context
let leading_context = db
.fetch_messages(MessageQuery {
filter: MessageFilter {
channel: Some(message.channel.to_string()),
..Default::default()
},
limit: Some(15),
time_period: MessageTimePeriod::Absolute {
before: None,
after: Some(message.id.to_string()),
sort: Some(MessageSort::Oldest),
},
})
.await?;
Ok((
SnapshotContent::Message {
message,
prior_context: prior_context.into_iter().map(Into::into).collect(),
leading_context: leading_context.into_iter().map(Into::into).collect(),
},
files,
))
}
/// Generate snapshot from a given server
pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec<String>)> {
// Collect server's icon and banner
let files = [&server.icon, &server.banner]
.iter()
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
.collect();
Ok((SnapshotContent::Server(server), files))
}
/// Generate snapshot from a given user
pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> {
// Collect user's avatar and profile background
let files = [
user.avatar.as_ref(),
user.profile
.as_ref()
.and_then(|profile| profile.background.as_ref()),
]
.iter()
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
.collect();
Ok((SnapshotContent::User(user), files))
}
}
@@ -0,0 +1,12 @@
use revolt_result::Result;
use crate::Snapshot;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractSnapshot: Sync + Send {
/// Insert a new snapshot into the database
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>;
}
@@ -0,0 +1,16 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::Snapshot;
use super::AbstractSnapshot;
static COL: &str = "safety_snapshots";
#[async_trait]
impl AbstractSnapshot for MongoDb {
/// Insert a new snapshot into the database
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
query!(self, insert_one, COL, &snapshot).map(|_| ())
}
}
@@ -0,0 +1,20 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::Snapshot;
use super::AbstractSnapshot;
#[async_trait]
impl AbstractSnapshot for ReferenceDb {
/// Insert a new report into the database
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
let mut snapshots = self.safety_snapshots.lock().await;
if snapshots.contains_key(&snapshot.id) {
Err(create_database_error!("insert", "snapshot"))
} else {
snapshots.insert(snapshot.id.to_string(), snapshot.clone());
Ok(())
}
}
}
@@ -1,4 +1,6 @@
use crate::MemberCompositeKey;
use revolt_result::Result;
use crate::{Database, MemberCompositeKey, Server};
auto_derived!(
/// Server Ban
@@ -10,3 +12,24 @@ auto_derived!(
pub reason: Option<String>,
}
);
impl ServerBan {
/// Create ban
pub async fn create(
db: &Database,
server: &Server,
user_id: &str,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan {
id: MemberCompositeKey {
server: server.id.to_string(),
user: user_id.to_string(),
},
reason,
};
db.insert_ban(&ban).await?;
Ok(ban)
}
}
@@ -3,8 +3,8 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Database, File, Server,
SystemMessage, User,
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
Server, SystemMessage, User,
};
auto_derived_partial!(
@@ -80,9 +80,8 @@ impl Member {
db: &Database,
server: &Server,
user: &User,
// channels: Option<Vec<Channel>>,
//) -> Result<Vec<Channel>> {
) -> Result<()> {
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
return Err(create_error!(Banned));
}
@@ -101,9 +100,10 @@ impl Member {
db.insert_member(&member).await?;
let mut channels = vec![];
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if true {
if should_fetch {
let query = DatabasePermissionQuery::new(db, user).server(server);
let existing_channels = db.fetch_channels(&server.channels).await?;
@@ -119,6 +119,8 @@ impl Member {
}
}
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
EventV1::ServerMemberJoin {
id: server.id.clone(),
user: user.id.clone(),
@@ -134,6 +136,7 @@ impl Member {
.into_iter()
.map(|channel| channel.into())
.collect(),
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
}
.private(user.id.clone())
.await;
@@ -152,8 +155,7 @@ impl Member {
.ok();
}
// Ok(channels)
Ok(())
Ok(channels)
}
/// Update member data
@@ -213,4 +215,47 @@ impl Member {
false
}
}
/// Remove member from server
pub async fn remove(
self,
db: &Database,
server: &Server,
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&self.id).await?;
EventV1::ServerMemberLeave {
id: self.id.server.to_string(),
user: self.id.user.to_string(),
}
.p(self.id.server.to_string())
.await;
if !silent {
if let Some(id) = server
.system_messages
.as_ref()
.and_then(|x| match intention {
RemovalIntention::Leave => x.user_left.as_ref(),
RemovalIntention::Kick => x.user_kicked.as_ref(),
RemovalIntention::Ban => x.user_banned.as_ref(),
})
{
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user },
}
.into_message(id.to_string())
// TODO: support notifications here in the future?
.send_without_notifications(db, false, false)
.await
.ok();
}
}
Ok(())
}
}
+57 -241
View File
@@ -1,10 +1,11 @@
use std::collections::{HashMap, HashSet};
use revolt_permissions::OverrideField;
use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_permissions::{OverrideField, DEFAULT_PERMISSION_SERVER};
use revolt_result::Result;
use ulid::Ulid;
use crate::{events::client::EventV1, Database, File};
use crate::{events::client::EventV1, Channel, Database, File, User};
auto_derived_partial!(
/// Server
@@ -22,7 +23,7 @@ auto_derived_partial!(
pub description: Option<String>,
/// Channels within this server
// ! FIXME: this may be redundant
// TODO: investigate if this is redundant and can be removed
pub channels: Vec<String>,
/// Categories for this server
#[serde(skip_serializing_if = "Option::is_none")]
@@ -131,8 +132,52 @@ auto_derived!(
#[allow(clippy::disallowed_methods)]
impl Server {
/// Create a server
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_server(self).await
pub async fn create(
db: &Database,
data: v0::DataCreateServer,
owner: &User,
create_default_channels: bool,
) -> Result<(Server, Vec<Channel>)> {
let mut server = Server {
id: ulid::Ulid::new().to_string(),
owner: owner.id.to_string(),
name: data.name,
description: data.description,
channels: vec![],
nsfw: data.nsfw.unwrap_or(false),
default_permissions: *DEFAULT_PERMISSION_SERVER as i64,
analytics: false,
banner: None,
categories: None,
discoverable: false,
flags: None,
icon: None,
roles: HashMap::new(),
system_messages: None,
};
let channels: Vec<Channel> = if create_default_channels {
vec![
Channel::create_server_channel(
db,
&mut server,
DataCreateServerChannel {
channel_type: v0::LegacyServerChannelType::Text,
name: "General".to_string(),
..Default::default()
},
false,
)
.await?,
]
} else {
vec![]
};
server.channels = channels.iter().map(|c| c.id()).collect();
db.insert_server(&server).await?;
Ok((server, channels))
}
/// Update server data
@@ -208,144 +253,6 @@ impl Server {
Err(create_error!(NotFound))
}
}
/* /// Create a new member in a server
pub async fn create_member(
&self,
db: &Database,
user: User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
return Err(Error::Banned);
}
let member = Member {
id: MemberCompositeKey {
server: self.id.clone(),
user: user.id.clone(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
};
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let perm = perms(&user).server(self).member(&member);
let existing_channels = db.fetch_channels(&self.channels).await?;
for channel in existing_channels {
if perm
.clone()
.channel(&channel)
.has_permission(db, Permission::ViewChannel)
.await?
{
channels.push(channel);
}
}
}
/* // TODO: EventV1::ServerMemberJoin {
id: self.id.clone(),
user: user.id.clone(),
}
.p(self.id.clone())
.await;
EventV1::ServerCreate {
id: self.id.clone(),
server: self.clone(),
channels: channels.clone(),
}
.private(user.id.clone())
.await; */
if let Some(id) = self
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
Ok(channels)
}
/// Remove a member from a server
pub async fn remove_member(
&self,
db: &Database,
member: Member,
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&member.id).await?;
/* // TODO: EventV1::ServerMemberLeave {
id: self.id.to_string(),
user: member.id.user.clone(),
}
.p(member.id.server)
.await; */
if !silent {
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
RemovalIntention::Leave => x.user_left.as_ref(),
RemovalIntention::Kick => x.user_kicked.as_ref(),
RemovalIntention::Ban => x.user_banned.as_ref(),
}) {
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
}
Ok(())
}
/// Create ban
pub async fn ban_user(
self,
db: &Database,
id: MemberCompositeKey,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan { id, reason };
db.insert_ban(&ban).await?;
Ok(ban)
}
/// Ban a member from a server
pub async fn ban_member(
self,
db: &Database,
member: Member,
reason: Option<String>,
) -> Result<ServerBan> {
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
.await?;
self.ban_user(db, member.id, reason).await
} */
}
impl Role {
@@ -453,109 +360,18 @@ impl SystemMessageChannels {
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_permissions::{calculate_server_permissions, ChannelPermission, OverrideField};
use crate::{
util::permissions::DatabasePermissionQuery, Member, MemberCompositeKey, Role, Server, User,
};
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[async_std::test]
async fn permissions() {
database_test!(|db| async move {
let owner = User::create(&db, "Owner".to_string(), None, None)
.await
.unwrap();
let moderator = User::create(&db, "Moderator".to_string(), None, None)
.await
.unwrap();
let user = User::create(&db, "User".to_string(), None, None)
.await
.unwrap();
let server_id = ulid::Ulid::new().to_string();
// TODO: seeder functions
// e.g. seed!("channel", "file.json")
let server = Server {
id: server_id,
owner: owner.id.clone(),
name: "My Server".to_string(),
description: None,
channels: vec![],
categories: None,
system_messages: None,
roles: HashMap::from([
(
"01F9HFTSBWTNA2F4TMSV7VM3FG".to_string(),
Role {
name: "Moderator".to_string(),
permissions: OverrideField {
a: 545270208, // TODO: explicit
..Default::default()
},
colour: None,
hoist: true,
rank: 3,
},
),
(
"01FBF9DNHSRPVTWFMNB3JNB8FK".to_string(),
Role {
name: "Owner".to_string(),
permissions: Default::default(),
colour: None,
hoist: true,
rank: 0,
},
),
]),
default_permissions: 4000322560, // TODO: use bitfield
icon: None,
banner: None,
flags: None,
nsfw: false,
analytics: false,
discoverable: false,
};
// TODO: proper creation
server.create(&db).await.unwrap();
db.insert_member(&Member {
id: MemberCompositeKey {
user: owner.id.clone(),
server: server.id.clone(),
},
roles: vec!["01FBF9DNHSRPVTWFMNB3JNB8FK".to_string()],
..Default::default()
})
.await
.unwrap();
db.insert_member(&Member {
id: MemberCompositeKey {
user: moderator.id.clone(),
server: server.id.clone(),
},
roles: vec!["01F9HFTSBWTNA2F4TMSV7VM3FG".to_string()],
..Default::default()
})
.await
.unwrap();
db.insert_member(&Member {
id: MemberCompositeKey {
user: user.id.clone(),
server: server.id.clone(),
},
..Default::default()
})
.await
.unwrap();
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)
@@ -1,4 +1,4 @@
use bson::{to_document, Document};
use bson::{to_document, Bson, Document};
use futures::StreamExt;
use revolt_result::Result;
@@ -67,6 +67,7 @@ impl AbstractServers for MongoDb {
/// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()> {
self.delete_associated_server_objects(id).await?;
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
@@ -181,3 +182,81 @@ impl IntoDocumentPath for FieldsRole {
})
}
}
impl MongoDb {
pub async fn delete_associated_server_objects(&self, server_id: &str) -> Result<()> {
// Find all channels
let channels: Vec<String> = self
.col::<Document>("channels")
.find(
doc! {
"server": server_id
},
None,
)
.await
.map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async {
s.map(|d| d.get_str("_id").map(|s| s.to_string()).ok())
.ok()
.flatten()
})
.collect()
.await;
// Check if there are any attachments we need to delete.
self.delete_bulk_messages(doc! {
"channel": {
"$in": &channels
}
})
.await?;
// Delete all emoji.
self.col::<Document>("emojis")
.delete_many(
doc! {
"parent.id": &server_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "emojis"))?;
// Delete all channels.
self.col::<Document>("channels")
.delete_many(
doc! {
"server": &server_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "channels"))?;
// Delete any associated objects, e.g. unreads and invites.
self.delete_associated_channel_objects(Bson::Document(doc! { "$in": &channels }))
.await?;
// Delete members and bans.
for with in &["server_members", "server_bans"] {
self.col::<Document>(with)
.delete_many(
doc! {
"_id.server": &server_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", with))?;
}
// Update many attachments with parent id.
self.delete_many_attachments(doc! {
"object_id": &server_id
})
.await?;
Ok(())
}
}
+45 -21
View File
@@ -4,7 +4,10 @@ use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_result::{create_error, Error, ErrorType, Result};
use revolt_config::config;
use revolt_models::v0;
use revolt_presence::filter_online;
use revolt_result::{create_error, Result};
use ulid::Ulid;
auto_derived_partial!(
@@ -93,20 +96,22 @@ auto_derived!(
}
/// User's active status
#[derive(Default)]
pub struct UserStatus {
/// Custom status text
#[serde(skip_serializing_if = "String::is_empty", default)]
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
/// Current presence option
#[serde(skip_serializing_if = "Option::is_none")]
pub presence: Option<Presence>,
}
/// User's profile
#[derive(Default)]
pub struct UserProfile {
/// Text content on user's profile
#[serde(skip_serializing_if = "String::is_empty", default)]
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<File>,
@@ -218,6 +223,18 @@ impl User {
.is_empty())
}
/// Check if this user can acquire another server
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
let config = config().await;
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
Ok(())
} else {
Err(create_error!(TooManyServers {
max: config.features.limits.default.servers
}))
}
}
/// Sanitise and validate a username can be used
pub fn validate_username(username: String) -> Result<String> {
// Copy the username for validation
@@ -249,7 +266,27 @@ impl User {
Ok(username)
}
// Find a free discriminator for a given username
/// Helper function to fetch many users as a mutually connected user
/// (while optimising the online ID query)
pub async fn fetch_many_ids_as_mutuals(
db: &Database,
perspective: &User,
ids: &[String],
) -> Result<Vec<v0::User>> {
let online_ids = filter_online(ids).await;
Ok(db
.fetch_users(ids)
.await?
.into_iter()
.map(|user| {
let is_online = online_ids.contains(&user.id);
user.into_known(perspective, is_online)
})
.collect())
}
/// Find a free discriminator for a given username
pub async fn find_discriminator(
db: &Database,
username: &str,
@@ -335,19 +372,6 @@ impl User {
}
}
/// Check whether a username is already in use by another user
#[allow(dead_code)]
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
match db.fetch_user_by_username(username).await {
Ok(_) => Ok(true),
Err(Error {
error_type: ErrorType::NotFound,
..
}) => Ok(false),
Err(error) => Err(error),
}
}
/// Set a relationship to another user
pub async fn set_relationship(
&mut self,
@@ -541,7 +565,7 @@ impl User {
FieldsUser::Avatar => self.avatar = None,
FieldsUser::StatusText => {
if let Some(x) = self.status.as_mut() {
x.text = String::new();
x.text = None;
}
}
FieldsUser::StatusPresence => {
@@ -551,7 +575,7 @@ impl User {
}
FieldsUser::ProfileContent => {
if let Some(x) = self.profile.as_mut() {
x.content = String::new();
x.content = None;
}
}
FieldsUser::ProfileBackground => {
+1 -1
View File
@@ -14,7 +14,7 @@ pub trait AbstractUsers: Sync + Send {
async fn fetch_user(&self, id: &str) -> Result<User>;
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User>;
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
@@ -25,13 +25,14 @@ impl AbstractUsers for MongoDb {
}
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
query!(
self,
find_one_with_options,
COL,
doc! {
"username": username
"username": username,
"discriminator": discriminator
},
FindOneOptions::builder()
.collation(
@@ -28,12 +28,14 @@ impl AbstractUsers for ReferenceDb {
}
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
let users = self.users.lock().await;
let lowercase = username.to_lowercase();
users
.values()
.find(|user| user.username.to_lowercase() == lowercase)
.find(|user| {
user.username.to_lowercase() == lowercase && user.discriminator == discriminator
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
@@ -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 {
+156 -11
View File
@@ -15,6 +15,7 @@ impl crate::Bot {
description: user
.profile
.map(|profile| profile.content)
.flatten()
.unwrap_or_default(),
}
}
@@ -341,11 +342,7 @@ impl From<crate::Message> for Message {
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value
.reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect(),
reactions: value.reactions,
interactions: value.interactions.into(),
masquerade: value.masquerade.map(|masq| masq.into()),
}
@@ -369,12 +366,7 @@ impl From<crate::PartialMessage> for PartialMessage {
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions.map(|reactions| {
reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect()
}),
reactions: value.reactions,
interactions: value.interactions.map(|interactions| interactions.into()),
masquerade: value.masquerade.map(|masq| masq.into()),
}
@@ -453,6 +445,19 @@ impl From<Masquerade> for crate::Masquerade {
}
}
impl From<crate::Report> for Report {
fn from(value: crate::Report) -> Self {
Report {
id: value.id,
author_id: value.author_id,
content: value.content,
additional_context: value.additional_context,
status: value.status,
notes: value.notes,
}
}
}
impl From<crate::ServerBan> for ServerBan {
fn from(value: crate::ServerBan) -> Self {
ServerBan {
@@ -508,6 +513,17 @@ impl From<crate::FieldsMember> for FieldsMember {
}
}
impl From<FieldsMember> for crate::FieldsMember {
fn from(value: FieldsMember) -> crate::FieldsMember {
match value {
FieldsMember::Avatar => crate::FieldsMember::Avatar,
FieldsMember::Nickname => crate::FieldsMember::Nickname,
FieldsMember::Roles => crate::FieldsMember::Roles,
FieldsMember::Timeout => crate::FieldsMember::Timeout,
}
}
}
impl From<crate::RemovalIntention> for RemovalIntention {
fn from(value: crate::RemovalIntention) -> Self {
match value {
@@ -584,6 +600,18 @@ impl From<crate::FieldsServer> for FieldsServer {
}
}
impl From<FieldsServer> for crate::FieldsServer {
fn from(value: FieldsServer) -> crate::FieldsServer {
match value {
FieldsServer::Banner => crate::FieldsServer::Banner,
FieldsServer::Categories => crate::FieldsServer::Categories,
FieldsServer::Description => crate::FieldsServer::Description,
FieldsServer::Icon => crate::FieldsServer::Icon,
FieldsServer::SystemMessages => crate::FieldsServer::SystemMessages,
}
}
}
impl From<crate::Category> for Category {
fn from(value: crate::Category) -> Self {
Category {
@@ -594,6 +622,16 @@ impl From<crate::Category> for Category {
}
}
impl From<Category> for crate::Category {
fn from(value: Category) -> Self {
crate::Category {
id: value.id,
title: value.title,
channels: value.channels,
}
}
}
impl From<crate::SystemMessageChannels> for SystemMessageChannels {
fn from(value: crate::SystemMessageChannels) -> Self {
SystemMessageChannels {
@@ -605,6 +643,17 @@ impl From<crate::SystemMessageChannels> for SystemMessageChannels {
}
}
impl From<SystemMessageChannels> for crate::SystemMessageChannels {
fn from(value: SystemMessageChannels) -> Self {
crate::SystemMessageChannels {
user_joined: value.user_joined,
user_left: value.user_left,
user_kicked: value.user_kicked,
user_banned: value.user_banned,
}
}
}
impl From<crate::Role> for Role {
fn from(value: crate::Role) -> Self {
Role {
@@ -637,6 +686,14 @@ impl From<crate::FieldsRole> for FieldsRole {
}
}
impl From<FieldsRole> for crate::FieldsRole {
fn from(value: FieldsRole) -> Self {
match value {
FieldsRole::Colour => crate::FieldsRole::Colour,
}
}
}
impl crate::User {
pub async fn into<'a, P>(self, db: &Database, perspective: P) -> User
where
@@ -710,6 +767,75 @@ impl crate::User {
}
}
/// Convert user object into user model assuming mutual connection
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
where
P: Into<Option<&'a crate::User>>,
{
let perspective = perspective.into();
let (relationship, can_see_profile) = if self.bot.is_some() {
(RelationshipStatus::None, true)
} else if let Some(perspective) = perspective {
if perspective.id == self.id {
(RelationshipStatus::User, true)
} else {
let relationship = perspective
.relations
.as_ref()
.map(|relations| {
relations
.iter()
.find(|relationship| relationship.id == self.id)
.map(|relationship| relationship.status.clone().into())
.unwrap_or_default()
})
.unwrap_or_default();
let can_see_profile = relationship != RelationshipStatus::BlockedOther;
(relationship, can_see_profile)
}
} else {
(RelationshipStatus::None, false)
};
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: if let Some(crate::User { id, .. }) = perspective {
if id == &self.id {
self.relations
.unwrap_or_default()
.into_iter()
.map(|relation| relation.into())
.collect()
} else {
vec![]
}
} else {
vec![]
},
badges: self.badges.unwrap_or_default() as u32,
status: if can_see_profile {
self.status.map(|status| status.into())
} else {
None
},
profile: if can_see_profile {
self.profile.map(|profile| profile.into())
} else {
None
},
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship,
online: can_see_profile && is_online,
id: self.id,
}
}
pub async fn into_self(self) -> User {
User {
username: self.username,
@@ -736,6 +862,13 @@ impl crate::User {
id: self.id,
}
}
pub fn as_author_for_system(&self) -> MessageAuthor {
MessageAuthor::System {
username: &self.username,
avatar: self.avatar.as_ref().map(|file| file.id.as_ref()),
}
}
}
impl From<crate::PartialUser> for PartialUser {
@@ -823,6 +956,18 @@ impl From<crate::Presence> for Presence {
}
}
impl From<Presence> for crate::Presence {
fn from(value: Presence) -> crate::Presence {
match value {
Presence::Online => crate::Presence::Online,
Presence::Idle => crate::Presence::Idle,
Presence::Focus => crate::Presence::Focus,
Presence::Busy => crate::Presence::Busy,
Presence::Invisible => crate::Presence::Invisible,
}
}
}
impl From<crate::UserStatus> for UserStatus {
fn from(value: crate::UserStatus) -> Self {
UserStatus {
+1
View File
@@ -2,3 +2,4 @@ pub mod bridge;
pub mod idempotency;
pub mod permissions;
pub mod reference;
pub mod test_fixtures;
+46 -3
View File
@@ -2,7 +2,7 @@ use std::borrow::Cow;
use revolt_permissions::{
calculate_user_permissions, ChannelType, Override, PermissionQuery, PermissionValue,
RelationshipStatus,
RelationshipStatus, DEFAULT_PERMISSION_DIRECT_MESSAGE,
};
use crate::{Channel, Database, Member, Server, User};
@@ -209,7 +209,7 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
match channel {
Cow::Borrowed(Channel::Group { permissions, .. })
| Cow::Owned(Channel::Group { permissions, .. }) => Override {
allow: permissions.unwrap_or_default() as u64,
allow: permissions.unwrap_or(*DEFAULT_PERMISSION_DIRECT_MESSAGE as i64) as u64,
deny: 0,
},
Cow::Borrowed(Channel::TextChannel {
@@ -344,7 +344,23 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
| Cow::Owned(Channel::TextChannel { server, .. })
| Cow::Borrowed(Channel::VoiceChannel { server, .. })
| Cow::Owned(Channel::VoiceChannel { server, .. }) => {
// FIXME: may double fetch
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));
}
@@ -428,6 +444,33 @@ impl<'a> DatabasePermissionQuery<'a> {
..self
}
}
/// Access the underlying user
pub fn user_ref(&self) -> &Option<Cow<User>> {
&self.user
}
/// Access the underlying server
pub fn channel_ref(&self) -> &Option<Cow<Channel>> {
&self.channel
}
/// Access the underlying server
pub fn server_ref(&self) -> &Option<Cow<Server>> {
&self.server
}
/// Access the underlying member
pub fn member_ref(&self) -> &Option<Cow<Member>> {
&self.member
}
/// Get the known member's current ranking
pub fn get_member_rank(&self) -> Option<i64> {
self.member
.as_ref()
.map(|member| member.get_ranking(self.server.as_ref().unwrap()))
}
}
/// Short-hand for creating a permission calculator
+33 -1
View File
@@ -7,7 +7,9 @@ use schemars::{
JsonSchema,
};
use crate::{Bot, Channel, Database, Emoji, Message, User, Webhook};
use crate::{
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
@@ -22,6 +24,11 @@ impl Reference {
Reference { id }
}
/// Fetch ban from Ref
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
db.fetch_ban(server, &self.id).await
}
/// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await
@@ -37,11 +44,36 @@ impl Reference {
db.fetch_channel(&self.id).await
}
/// Fetch invite from Ref
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
db.fetch_invite(&self.id).await
}
/// Fetch message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(&self.id).await
}
/// Fetch message from Ref and validate channel
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
let msg = db.fetch_message(&self.id).await?;
if msg.channel != channel {
return Err(create_error!(NotFound));
}
Ok(msg)
}
/// Fetch member from Ref
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
db.fetch_member(server, &self.id).await
}
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await
}
/// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> {
db.fetch_user(&self.id).await
@@ -0,0 +1,103 @@
use std::collections::HashMap;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use serde_json::from_str;
use crate::{Channel, Database, Member, Server, User};
static RE_ID: Lazy<Regex> = Lazy::new(|| Regex::new("__ID:(\\d+)__").unwrap());
#[derive(Debug, Deserialize)]
#[serde(tag = "_object_type")]
enum LoadedFixture {
User(User),
Channel(Channel),
Server(Server),
ServerMember(Member),
}
pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String> {
let mut ids = HashMap::<String, String>::new();
let input = RE_ID.replace_all(input, |cap: &Captures| {
let d = cap.get(1).unwrap().as_str();
if !ids.contains_key(d) {
ids.insert(d.to_string(), ulid::Ulid::new().to_string());
}
ids.get(d).unwrap().clone()
});
// Deserialise the fixtures
let items: Vec<LoadedFixture> = from_str(&input).expect("Failed to deserialise fixture");
// Load all of the items within
for item in items {
#[allow(clippy::disallowed_methods)]
match item {
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
}
}
// Return IDs for ease of use
ids
}
#[async_trait]
pub trait FetchFixture {
async fn user(&self, db: &Database, d: usize) -> User;
async fn channel(&self, db: &Database, d: usize) -> Channel;
async fn server(&self, db: &Database, d: usize) -> Server;
async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member;
}
#[async_trait]
impl FetchFixture for HashMap<String, String> {
async fn user(&self, db: &Database, d: usize) -> User {
db.fetch_user(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn channel(&self, db: &Database, d: usize) -> Channel {
db.fetch_channel(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn server(&self, db: &Database, d: usize) -> Server {
db.fetch_server(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member {
db.fetch_member(
self.get(&d_server.to_string()).unwrap(),
self.get(&d_user.to_string()).unwrap(),
)
.await
.unwrap()
}
}
#[macro_export]
macro_rules! fixture {
( $database:expr, $name:expr, $( $variable:ident $type:ident $id: expr )+ ) => {
use $crate::util::test_fixtures::FetchFixture;
let fixtures = $crate::util::test_fixtures::load_fixture(
&$database,
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/", $name, ".json")),
)
.await;
$(
let $variable = fixtures.$type(&$database, $id).await;
)+
};
}
+9 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.6.8"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -12,27 +12,31 @@ description = "Revolt Backend: API Models"
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
schemas = ["dep:schemars", "revolt-permissions/schemas"]
validator = ["dep:validator"]
rocket = ["dep:rocket"]
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
default = ["serde", "partials"]
default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.6.8", path = "../config" }
revolt-permissions = { version = "0.6.8", path = "../permissions" }
revolt-config = { version = "0.7.0", path = "../config" }
revolt-permissions = { version = "0.7.0", path = "../permissions" }
# Utility
regex = "1"
indexmap = "1.9.3"
once_cell = "1.17.1"
# Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
# Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
# Spec Generation
schemars = { version = "0.8.8", optional = true }
schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] }
# Validation
validator = { version = "0.16.0", optional = true, features = ["derive"] }
@@ -1,3 +1,5 @@
use super::{Channel, File, Server, User};
auto_derived!(
/// Invite
pub enum Invite {
@@ -24,4 +26,77 @@ auto_derived!(
channel: String,
},
}
/// Public invite response
#[allow(clippy::large_enum_variant)]
#[serde(tag = "type")]
pub enum InviteResponse {
/// Server channel invite
Server {
/// Invite code
code: String,
/// Id of the server
server_id: String,
/// Name of the server
server_name: String,
/// Attachment for server icon
#[serde(skip_serializing_if = "Option::is_none")]
server_icon: Option<File>,
/// Attachment for server banner
#[serde(skip_serializing_if = "Option::is_none")]
server_banner: Option<File>,
/// Enum of server flags
#[serde(skip_serializing_if = "Option::is_none")]
server_flags: Option<i32>,
/// Id of server channel
channel_id: String,
/// Name of server channel
channel_name: String,
/// Description of server channel
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
/// Name of user who created the invite
user_name: String,
/// Avatar of the user who created the invite
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
/// Number of members in this server
member_count: i64,
},
/// Group channel invite
Group {
/// Invite code
code: String,
/// Id of group channel
channel_id: String,
/// Name of group channel
channel_name: String,
/// Description of group channel
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
/// Name of user who created the invite
user_name: String,
/// Avatar of the user who created the invite
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
},
}
/// Invite join response
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum InviteJoinResponse {
Server {
/// Channels in the server
channels: Vec<Channel>,
/// Server we are joining
server: Server,
},
Group {
/// Group channel we are joining
channel: Channel,
/// Members of this group
users: Vec<User>,
},
}
);
+68 -5
View File
@@ -1,8 +1,11 @@
use super::File;
use revolt_permissions::OverrideField;
use revolt_permissions::{Override, OverrideField};
use std::collections::{HashMap, HashSet};
#[cfg(feature = "rocket")]
use rocket::FromForm;
auto_derived!(
/// Channel
#[serde(tag = "channel_type")]
@@ -212,24 +215,84 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateGroup {
/// Group name
#[validate(length(min = 1, max = 32))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Group description
#[validate(length(min = 0, max = 1024))]
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Group icon
#[validate(length(min = 1, max = 128))]
#[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.
#[validate(length(min = 0, max = 49))]
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 49)))]
#[serde(default)]
pub users: HashSet<String>,
/// Whether this group is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
}
/// Server Channel Type
#[derive(Default)]
pub enum LegacyServerChannelType {
/// Text Channel
#[default]
Text,
/// Voice Channel
Voice,
}
/// Create new server channel
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateServerChannel {
/// Channel type
#[serde(rename = "type", default = "LegacyServerChannelType::default")]
pub channel_type: LegacyServerChannelType,
/// Channel name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Channel description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
}
/// New default permissions
#[serde(untagged)]
pub enum DataDefaultChannelPermissions {
Value {
/// Permission values to set for members in a `Group`
permissions: u64,
},
Field {
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
permissions: Override,
},
}
/// New role permissions
pub struct DataSetRolePermissions {
/// Allow / deny values to set for this role
pub permissions: Override,
}
/// Options when deleting a channel
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
}
/// Voice server token response
pub struct LegacyCreateVoiceUserResponse {
/// Token for authenticating with the voice server
token: String,
}
);
impl Channel {
+129 -18
View File
@@ -1,8 +1,6 @@
use std::{
collections::{HashMap, HashSet},
time::SystemTime,
};
use std::time::SystemTime;
use indexmap::{IndexMap, IndexSet};
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_config::config;
@@ -10,9 +8,12 @@ use revolt_config::config;
#[cfg(feature = "validator")]
use validator::Validate;
#[cfg(feature = "rocket")]
use rocket::{FromForm, FromFormField};
use iso8601_timestamp::Timestamp;
use super::{Embed, File, MessageWebhook, User, Webhook, RE_COLOUR};
use super::{Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR};
pub static RE_MENTION: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
@@ -55,8 +56,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 = "HashMap::is_empty", default)]
pub reactions: HashMap<String, HashSet<String>>,
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub reactions: IndexMap<String, IndexSet<String>>,
/// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
@@ -68,6 +69,24 @@ auto_derived_partial!(
);
auto_derived!(
/// Bulk Message Response
#[serde(untagged)]
pub enum BulkMessageResponse {
JustMessages(
/// List of messages
Vec<Message>,
),
MessagesAndUsers {
/// List of messages
messages: Vec<Message>,
/// List of users
users: Vec<User>,
/// List of members
#[serde(skip_serializing_if = "Option::is_none")]
members: Option<Vec<Member>>,
},
}
/// System Event
#[serde(tag = "type")]
pub enum SystemMessage {
@@ -98,17 +117,19 @@ auto_derived!(
/// Name and / or avatar override information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct Masquerade {
// FIXME: missing validation
/// 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>,
}
@@ -117,7 +138,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<HashSet<String>>,
pub reactions: Option<IndexSet<String>>,
/// Whether reactions should be restricted to the given list
///
/// Can only be set to true if reactions list is of at least length 1
@@ -136,6 +157,7 @@ auto_derived!(
///
/// Sort used for retrieving messages
#[derive(Default)]
#[cfg_attr(feature = "rocket", derive(FromFormField))]
pub enum MessageSort {
/// Sort by the most relevant messages
#[default]
@@ -169,16 +191,19 @@ auto_derived!(
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct SendableEmbed {
#[validate(length(min = 1, max = 128))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub icon_url: Option<String>,
#[validate(length(min = 1, max = 256))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub url: Option<String>,
#[validate(length(min = 1, max = 100))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
pub title: Option<String>,
#[validate(length(min = 1, max = 2000))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
pub description: Option<String>,
pub media: Option<String>,
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
#[cfg_attr(
feature = "validator",
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
)]
pub colour: Option<String>,
}
@@ -196,11 +221,11 @@ auto_derived!(
/// Unique token to prevent duplicate message sending
///
/// **This is deprecated and replaced by `Idempotency-Key`!**
#[validate(length(min = 1, max = 64))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
pub nonce: Option<String>,
/// Message content to send
#[validate(length(min = 0, max = 2000))]
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
pub content: Option<String>,
/// Attachments to include in message
pub attachments: Option<Vec<String>>,
@@ -209,14 +234,100 @@ auto_derived!(
/// Embeds to include in message
///
/// Text embed content contributes to the content length cap
#[validate]
#[cfg_attr(feature = "validator", validate)]
pub embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message
#[validate]
#[cfg_attr(feature = "validator", validate)]
pub masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
pub interactions: Option<Interactions>,
}
/// Options for querying messages
#[cfg_attr(feature = "validator", derive(Validate))]
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsQueryMessages {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
pub limit: Option<i64>,
/// Message id before which messages should be fetched
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub before: Option<String>,
/// Message id after which messages should be fetched
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub after: Option<String>,
/// Message sort direction
pub sort: Option<MessageSort>,
/// Message id to search around
///
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
/// It will also take half of limit rounded as the limits to each side.
/// It also fetches the message ID specified.
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub nearby: Option<String>,
/// Whether to include user (and member, if server channel) objects
pub include_users: Option<bool>,
}
/// Options for searching for messages
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataMessageSearch {
/// Full-text search query
///
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
pub query: String,
/// Maximum number of messages to fetch
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
pub limit: Option<i64>,
/// Message id before which messages should be fetched
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub before: Option<String>,
/// Message id after which messages should be fetched
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub after: Option<String>,
/// Message sort direction
///
/// By default, it will be sorted by latest.
#[cfg_attr(feature = "serde", serde(default = "MessageSort::default"))]
pub sort: MessageSort,
/// Whether to include user (and member, if server channel) objects
pub include_users: Option<bool>,
}
/// Changes to make to message
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditMessage {
/// New message content
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
pub content: Option<String>,
/// Embeds to include in the message
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 10)))]
pub embeds: Option<Vec<SendableEmbed>>,
}
/// Options for bulk deleting messages
#[cfg_attr(
feature = "validator",
cfg_attr(feature = "validator", derive(Validate))
)]
pub struct OptionsBulkDelete {
/// Message IDs
#[validate(length(min = 1, max = 100))]
pub ids: Vec<String>,
}
/// Options for removing reaction
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsUnreact {
/// Remove a specific user's reaction
pub user_id: Option<String>,
/// Remove all reactions
pub remove_all: Option<bool>,
}
);
/// Message Author Abstraction
+2
View File
@@ -7,6 +7,7 @@ mod embeds;
mod emojis;
mod files;
mod messages;
mod safety_reports;
mod server_bans;
mod server_members;
mod servers;
@@ -22,6 +23,7 @@ pub use embeds::*;
pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use safety_reports::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
+148
View File
@@ -0,0 +1,148 @@
use iso8601_timestamp::Timestamp;
auto_derived!(
/// User-generated platform moderation report
pub struct Report {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Id of the user creating this report
pub author_id: String,
/// Reported content
pub content: ReportedContent,
/// Additional report context
pub additional_context: String,
/// Status of the report
#[serde(flatten)]
pub status: ReportStatus,
/// Additional notes included on the report
#[serde(default)]
pub notes: String,
}
/// Reason for reporting content (message or server)
pub enum ContentReportReason {
/// No reason has been specified
NoneSpecified,
/// Illegal content catch-all reason
Illegal,
/// Selling or facilitating use of drugs or other illegal goods
IllegalGoods,
/// Extortion or blackmail
IllegalExtortion,
/// Revenge or child pornography
IllegalPornography,
/// Illegal hacking activity
IllegalHacking,
/// Extreme violence, gore, or animal cruelty
/// With exception to violence potrayed in media / creative arts
ExtremeViolence,
/// Content that promotes harm to others / self
PromotesHarm,
/// Unsolicited advertisements
UnsolicitedSpam,
/// This is a raid
Raid,
/// Spam or platform abuse
SpamAbuse,
/// Scams or fraud
ScamsFraud,
/// Distribution of malware or malicious links
Malware,
/// Harassment or abuse targeted at another user
Harassment,
}
/// Reason for reporting a user
pub enum UserReportReason {
/// No reason has been specified
NoneSpecified,
/// Unsolicited advertisements
UnsolicitedSpam,
/// User is sending spam or otherwise abusing the platform
SpamAbuse,
/// User's profile contains inappropriate content for a general audience
InappropriateProfile,
/// User is impersonating another user
Impersonation,
/// User is evading a ban
BanEvasion,
/// User is not of minimum age to use the platform
Underage,
}
/// The content being reported
#[serde(tag = "type")]
pub enum ReportedContent {
/// Report a message
Message {
/// ID of the message
id: String,
/// Reason for reporting message
report_reason: ContentReportReason,
},
/// Report a server
Server {
/// ID of the server
id: String,
/// Reason for reporting server
report_reason: ContentReportReason,
},
/// Report a user
User {
/// ID of the user
id: String,
/// Reason for reporting a user
report_reason: UserReportReason,
/// Message context
message_id: Option<String>,
},
}
/// Status of the report
#[serde(tag = "status")]
pub enum ReportStatus {
/// Report is waiting for triage / action
Created {},
/// Report was rejected
Rejected {
rejection_reason: String,
closed_at: Option<Timestamp>,
},
/// Report was actioned and resolved
Resolved { closed_at: Option<Timestamp> },
}
/// Just the status of the report
pub enum ReportStatusString {
/// Report is waiting for triage / action
Created,
/// Report was rejected
Rejected,
/// Report was actioned and resolved
Resolved,
}
);
+44 -1
View File
@@ -1,4 +1,7 @@
use super::MemberCompositeKey;
use super::{File, MemberCompositeKey, User};
#[cfg(feature = "validator")]
use validator::Validate;
auto_derived!(
/// Server Ban
@@ -9,4 +12,44 @@ auto_derived!(
/// Reason for ban creation
pub reason: Option<String>,
}
/// Information for new server ban
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataBanCreate {
/// Ban reason
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub reason: Option<String>,
}
/// Just enough information to list a ban
pub struct BannedUser {
/// Id of the banned user
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// Username of the banned user
pub username: String,
/// Discriminator of the banned user
pub discriminator: String,
/// Avatar of the banned user
pub avatar: Option<File>,
}
/// Ban list result
pub struct BanListResult {
/// Users objects
pub users: Vec<BannedUser>,
/// Ban objects
pub bans: Vec<ServerBan>,
}
);
impl From<User> for BannedUser {
fn from(user: User) -> Self {
BannedUser {
id: user.id,
username: user.username,
discriminator: user.discriminator,
avatar: user.avatar,
}
}
}
+51 -1
View File
@@ -1,9 +1,17 @@
use super::File;
use std::collections::HashMap;
use super::{File, Role, User};
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use regex::Regex;
#[cfg(feature = "validator")]
use validator::Validate;
#[cfg(feature = "rocket")]
use rocket::FromForm;
/// Regex for valid role colours
///
/// Allows the use of named colours, rgb(a), variables and all gradients.
@@ -77,4 +85,46 @@ auto_derived!(
Kick,
Ban,
}
/// Member response
#[serde(untagged)]
pub enum MemberResponse {
Member(Member),
MemberWithRoles {
member: Member,
roles: HashMap<String, Role>,
},
}
/// Options for fetching all members
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsFetchAllMembers {
/// Whether to exclude offline users
pub exclude_offline: Option<bool>,
}
/// Response with all members
pub struct AllMemberResponse {
/// List of members
pub members: Vec<Member>,
/// List of users
pub users: Vec<User>,
}
/// New member information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataMemberEdit {
/// Member nickname
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub nickname: Option<String>,
/// Attachment Id to set for avatar
pub avatar: Option<String>,
/// Array of role ids
pub roles: Option<Vec<String>>,
/// Timestamp this member is timed out until
pub timeout: Option<Timestamp>,
/// Fields to remove from channel object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsMember>>,
}
);
+148 -3
View File
@@ -1,8 +1,14 @@
use super::File;
use super::{Channel, File, RE_COLOUR};
use revolt_permissions::OverrideField;
use revolt_permissions::{Override, OverrideField};
use std::collections::HashMap;
#[cfg(feature = "validator")]
use validator::Validate;
#[cfg(feature = "rocket")]
use rocket::FromForm;
auto_derived_partial!(
/// Server
pub struct Server {
@@ -19,7 +25,7 @@ auto_derived_partial!(
pub description: Option<String>,
/// Channels within this server
// ! FIXME: this may be redundant
// TODO: investigate if this is redundant and can be removed
pub channels: Vec<String>,
/// Categories for this server
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
@@ -117,10 +123,13 @@ auto_derived!(
}
/// Channel category
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct Category {
/// Unique ID for this category
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub id: String,
/// Title for this category
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub title: String,
/// Channels in this category
pub channels: Vec<String>,
@@ -141,4 +150,140 @@ auto_derived!(
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_banned: Option<String>,
}
/// Information about new server to create
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateServer {
/// Server name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Server description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Whether this server is age-restricted
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub nsfw: Option<bool>,
}
/// Information about new role to create
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateRole {
/// Role name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Ranking position
///
/// Smaller values take priority.
pub rank: Option<i64>,
}
/// Response after creating new role
pub struct NewRoleResponse {
/// Id of the role
pub id: String,
/// New role
pub role: Role,
}
/// Information returned when creating server
pub struct CreateServerLegacyResponse {
/// Server object
pub server: Server,
/// Default channels
pub channels: Vec<Channel>,
}
/// Options when fetching server
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsFetchServer {
/// Whether to include channels
pub include_channels: Option<bool>,
}
/// Fetch server information
#[serde(untagged)]
pub enum FetchServerResponse {
JustServer(Server),
ServerWithChannels {
#[serde(flatten)]
server: Server,
channels: Vec<Channel>,
},
}
/// New server information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditServer {
/// Server name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: Option<String>,
/// Server description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Attachment Id for icon
pub icon: Option<String>,
/// Attachment Id for banner
pub banner: Option<String>,
/// Category structure for server
#[cfg_attr(feature = "validator", validate)]
pub categories: Option<Vec<Category>>,
/// System message configuration
pub system_messages: Option<SystemMessageChannels>,
/// Bitfield of server flags
#[cfg_attr(feature = "validator", serde(skip_serializing_if = "Option::is_none"))]
pub flags: Option<i32>,
// Whether this server is age-restricted
// nsfw: Option<bool>,
/// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg)
pub discoverable: Option<bool>,
/// Whether analytics should be collected for this server
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
pub analytics: Option<bool>,
/// Fields to remove from server object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsServer>>,
}
/// New role information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditRole {
/// Role name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: Option<String>,
/// Role colour
#[cfg_attr(
feature = "validator",
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
)]
pub colour: Option<String>,
/// Whether this role should be displayed separately
pub hoist: Option<bool>,
/// Ranking position
///
/// Smaller values take priority.
pub rank: Option<i64>,
/// Fields to remove from role object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsRole>>,
}
/// New role permissions
pub struct DataSetServerRolePermission {
/// Allow / deny values for the role in this server.
pub permissions: Override,
}
/// Options when leaving a server
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsServerDelete {
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
}
);
@@ -1,6 +1,26 @@
#[cfg(feature = "rocket")]
use rocket::FromForm;
use std::collections::HashMap;
/// HashMap of user settings
/// Each key is mapped to a tuple consisting of the
/// revision timestamp and serialised data (in JSON format)
pub type UserSettings = HashMap<String, (i64, String)>;
auto_derived!(
/// Options for fetching settings
pub struct OptionsFetchSettings {
/// Keys to fetch
pub keys: Vec<String>,
}
/// Additional options for inserting settings
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsSetSettings {
/// Timestamp of settings change.
///
/// Used to avoid feedback loops.
pub timestamp: Option<i64>,
}
);
+86 -4
View File
@@ -3,12 +3,21 @@ use regex::Regex;
use super::File;
#[cfg(feature = "validator")]
use validator::Validate;
/// Regex for valid usernames
///
/// Block zero width space
/// Block lookalike characters
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
/// Regex for valid display names
///
/// Block zero width space
/// Block newline and carriage return
pub static RE_DISPLAY_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap());
auto_derived_partial!(
/// User
pub struct User {
@@ -123,20 +132,26 @@ auto_derived!(
}
/// User's active status
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct UserStatus {
/// Custom status text
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub text: String,
#[validate(length(min = 0, max = 128))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub text: Option<String>,
/// Current presence option
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub presence: Option<Presence>,
}
/// User's profile
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct UserProfile {
/// Text content on user's profile
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub content: String,
#[validate(length(min = 0, max = 2000))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub content: Option<String>,
/// Background visible on user's profile
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub background: Option<File>,
@@ -182,12 +197,79 @@ auto_derived!(
Spam = 8,
}
/// New user profile data
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataUserProfile {
/// Text to set as user profile description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub content: Option<String>,
/// Attachment Id for background
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub background: Option<String>,
}
/// New user information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditUser {
/// New display name
#[cfg_attr(
feature = "validator",
validate(length(min = 2, max = 32), regex = "RE_DISPLAY_NAME")
)]
pub display_name: Option<String>,
/// Attachment Id for avatar
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub avatar: Option<String>,
/// New user status
#[cfg_attr(feature = "validator", validate)]
pub status: Option<UserStatus>,
/// New user profile data
///
/// This is applied as a partial.
#[cfg_attr(feature = "validator", validate)]
pub profile: Option<DataUserProfile>,
/// Bitfield of user badges
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub badges: Option<i32>,
/// Enum of user flags
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub flags: Option<i32>,
/// Fields to remove from user object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsUser>>,
}
/// User flag reponse
pub struct FlagResponse {
/// Flags
pub flags: i32,
}
/// Mutual friends and servers response
pub struct MutualResponse {
/// Array of mutual user IDs that both users are friends with
pub users: Vec<String>,
/// Array of mutual server IDs that both users are in
pub servers: Vec<String>,
}
/// Bot information for if the user is a bot
pub struct BotInformation {
/// Id of the owner of this bot
#[cfg_attr(feature = "serde", serde(rename = "owner"))]
pub owner_id: String,
}
/// User lookup information
pub struct DataSendFriendRequest {
/// Username and discriminator combo separated by #
pub username: String,
}
);
pub trait CheckRelationship {
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.6.8"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Core
revolt-result = { version = "0.6.5", path = "../result" }
revolt-result = { version = "0.7.0", path = "../result" }
# Utility
auto_ops = "0.3.0"
+7 -1
View File
@@ -37,6 +37,12 @@ pub async fn calculate_user_permissions<P: PermissionQuery>(query: &mut P) -> Pe
} else {
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
@@ -105,7 +111,7 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
query.set_server_from_channel().await;
if query.are_we_server_owner().await {
return ChannelPermission::GrantAllSafe.into();
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);
+40
View File
@@ -53,6 +53,17 @@ impl PermissionValue {
self.has(permission as u64)
}
/// Throw if missing user permission
pub fn throw_if_lacking_user_permission(&self, permission: UserPermission) -> Result<()> {
if self.has_user_permission(permission) {
Ok(())
} else {
Err(create_error!(MissingPermission {
permission: permission.to_string()
}))
}
}
/// Throw if missing channel permission
pub fn throw_if_lacking_channel_permission(&self, permission: ChannelPermission) -> Result<()> {
if self.has_channel_permission(permission) {
@@ -63,6 +74,35 @@ impl PermissionValue {
}))
}
}
/// Throw an error if we cannot grant permissions on either allows or denies
/// going from the previous given value to the next given value.
///
/// We need to check any:
/// - allows added (permissions now granted)
/// - denies removed (permissions now neutral or granted)
pub async fn throw_permission_override<C>(
&self,
current_value: C,
next_value: &Override,
) -> Result<()>
where
C: Into<Option<Override>>,
{
let current_value = current_value.into();
if let Some(current_value) = current_value {
if !self.has(!current_value.allows() & next_value.allows())
|| !self.has(current_value.denies() & !next_value.denies())
{
return Err(create_error!(CannotGiveMissingPermissions));
}
} else if !self.has(next_value.allows()) {
return Err(create_error!(CannotGiveMissingPermissions));
}
Ok(())
}
}
impl From<i64> for PermissionValue {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.6.8"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.6.8"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -147,7 +147,7 @@ macro_rules! create_error {
#[macro_export]
macro_rules! create_database_error {
( $operation: expr, $collection: expr ) => {
create_error!(DatabaseError {
$crate::create_error!(DatabaseError {
operation: $operation.to_string(),
collection: $collection.to_string()
})
+7 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.6.8"
version = "0.7.0"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -36,6 +36,7 @@ nanoid = "0.4.0"
serde_json = "1.0.57"
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.16", features = ["derive"] }
iso8601-timestamp = { version = "0.2.11", features = [] }
# async
futures = "0.3.8"
@@ -64,9 +65,6 @@ rocket_prometheus = "0.10.0-rc.3"
schemars = "0.8.8"
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
# quark
revolt-quark = { path = "../quark" }
# core
authifier = "1.0.8"
revolt-config = { path = "../core/config" }
@@ -74,7 +72,11 @@ revolt-database = { path = "../core/database", features = [
"rocket-impl",
"redis-is-patched",
] }
revolt-models = { path = "../core/models", features = ["schemas", "validator"] }
revolt-models = { path = "../core/models", features = [
"schemas",
"validator",
"rocket",
] }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
+89 -17
View File
@@ -8,6 +8,8 @@ extern crate serde_json;
pub mod routes;
pub mod util;
use revolt_config::config;
use revolt_database::events::client::EventV1;
use revolt_database::{Database, MongoDb};
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
@@ -16,19 +18,24 @@ use std::net::Ipv4Addr;
use std::str::FromStr;
use async_std::channel::unbounded;
use revolt_quark::authifier::{Authifier, AuthifierEvent};
use revolt_quark::events::client::EventV1;
use revolt_quark::DatabaseInfo;
use authifier::config::{
Captcha, Config as AuthifierConfig, EmailVerificationConfig, ResolveIp, SMTPSettings, Shield,
Template, Templates,
};
use authifier::{Authifier, AuthifierEvent};
use rocket::data::ToByteUnit;
pub async fn web() -> Rocket<Build> {
// Get settings
let config = config().await;
// Ensure environment variables are present
config.preflight_checks();
// Setup database
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
db.migrate_database().await.unwrap();
// Legacy database setup from quark
let legacy_db = DatabaseInfo::Auto.connect().await.unwrap();
// Setup Authifier event channel
let (sender, receiver) = unbounded();
@@ -40,7 +47,8 @@ pub async fn web() -> Rocket<Build> {
authifier::database::MongoDb(client.database("revolt")),
),
},
config: revolt_quark::util::authifier::config(),
config: Default::default(),
// config: authifier_config().await,
event_channel: Some(sender),
};
@@ -65,10 +73,6 @@ pub async fn web() -> Rocket<Build> {
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 {
@@ -97,7 +101,7 @@ pub async fn web() -> Rocket<Build> {
let rocket = rocket::build();
let prometheus = PrometheusMetrics::new();
routes::mount(rocket)
routes::mount(config, rocket)
.attach(prometheus.clone())
.mount("/metrics", prometheus)
.mount("/", rocket_cors::catch_all_options_routes())
@@ -105,7 +109,6 @@ pub async fn web() -> Rocket<Build> {
.mount("/swagger/", swagger)
.manage(authifier)
.manage(db)
.manage(legacy_db)
.manage(cors.clone())
.attach(util::ratelimiter::RatelimitFairing)
.attach(cors)
@@ -116,13 +119,82 @@ pub async fn web() -> Rocket<Build> {
})
}
pub async fn authifier_config() -> AuthifierConfig {
let config = config().await;
let mut auth_config = AuthifierConfig {
email_verification: if !config.api.smtp.host.is_empty() {
EmailVerificationConfig::Enabled {
smtp: SMTPSettings {
from: config.api.smtp.from_address,
host: config.api.smtp.host,
username: config.api.smtp.username,
password: config.api.smtp.password,
reply_to: Some(
config
.api
.smtp
.reply_to
.unwrap_or("support@revolt.chat".into()),
),
port: config.api.smtp.port,
use_tls: config.api.smtp.use_tls,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("templates/deletion.html").into()),
},
welcome: None,
},
}
} else {
EmailVerificationConfig::Disabled
},
..Default::default()
};
auth_config.invite_only = config.api.registration.invite_only;
if !config.api.security.captcha.hcaptcha_key.is_empty() {
auth_config.captcha = Captcha::HCaptcha {
secret: config.api.security.captcha.hcaptcha_key,
};
}
if !config.api.security.authifier_shield_key.is_empty() {
auth_config.shield = Shield::Enabled {
api_key: config.api.security.authifier_shield_key,
strict: false,
};
}
if config.api.security.trust_cloudflare {
auth_config.resolve_ip = ResolveIp::Cloudflare;
}
auth_config
}
#[launch]
async fn rocket() -> _ {
// Configure logging and environment
revolt_quark::configure!();
// Ensure environment variables are present
revolt_quark::variables::delta::preflight_checks();
revolt_config::configure!();
// Start web server
web().await
-8
View File
@@ -1,8 +0,0 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod stats;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![stats::stats]
}
-13
View File
@@ -1,13 +0,0 @@
use revolt_quark::models::stats::Stats;
use revolt_quark::{Db, Result};
use rocket::serde::json::Json;
/// # Query Stats
///
/// Fetch various technical statistics.
#[openapi(tag = "Admin")]
#[get("/stats")]
pub async fn stats(db: &Db) -> Result<Json<Stats>> {
Ok(Json(db.generate_stats().await?))
}
+13 -22
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]
@@ -125,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
@@ -1,23 +1,84 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Acknowledge Message
///
/// Lets the server and all other clients know that we've seen this message id in this channel.
#[openapi(tag = "Messaging")]
#[put("/<target>/ack/<message>")]
pub async fn req(db: &Db, user: User, target: Ref, message: Ref) -> Result<EmptyResponse> {
pub async fn ack(
db: &State<Database>,
user: User,
target: Reference,
message: Reference,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
channel
.ack(&user.id, &message.id)
.await
.map(|_| EmptyResponse)
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel};
use revolt_models::v0::DataCreateGroup;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_ack_channel() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
DataCreateGroup {
..Default::default()
},
user.id.clone(),
)
.await
.expect("`Channel`");
let message_id = ulid::Ulid::new().to_string();
let response = harness
.client
.put(format!("/channels/{}/ack/{}", group.id(), message_id))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
let event = harness
.wait_for_event(&format!("{}!", user.id), |event| match event {
EventV1::ChannelAck { id, .. } => id == &group.id(),
_ => false,
})
.await;
match event {
EventV1::ChannelAck {
message_id: m_id, ..
} => assert_eq!(m_id, message_id),
_ => unreachable!(),
};
}
}
@@ -1,34 +1,32 @@
use revolt_quark::{
models::{channel::PartialChannel, Channel, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, PartialChannel, User,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
leave_silently: Option<bool>,
}
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Close Channel
///
/// Deletes a server channel, leaves a group or closes a group.
#[openapi(tag = "Channel Information")]
#[delete("/<target>?<options..>")]
pub async fn req(
db: &Db,
pub async fn delete(
db: &State<Database>,
user: User,
target: Ref,
options: OptionsChannelDelete,
target: Reference,
options: v0::OptionsChannelDelete,
) -> Result<EmptyResponse> {
let mut channel = target.as_channel(db).await?;
let mut perms = perms(&user).channel(&channel);
perms.throw_permission(db, Permission::ViewChannel).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
match &channel {
Channel::SavedMessages { .. } => Err(Error::NoEffect),
Channel::SavedMessages { .. } => Err(create_error!(NoEffect)),
Channel::DirectMessage { .. } => channel
.update(
db,
@@ -41,20 +39,58 @@ pub async fn req(
.await
.map(|_| EmptyResponse),
Channel::Group { .. } => channel
.remove_user_from_group(
db,
&user.id,
None,
options.leave_silently.unwrap_or_default(),
)
.remove_user_from_group(db, &user, None, options.leave_silently.unwrap_or_default())
.await
.map(|_| EmptyResponse),
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
perms
.throw_permission(db, Permission::ManageChannel)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
channel.delete(db).await.map(|_| EmptyResponse)
}
}
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel};
use revolt_models::v0::DataCreateGroup;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_delete_group() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
DataCreateGroup {
..Default::default()
},
user.id.clone(),
)
.await
.expect("`Channel`");
let response = harness
.client
.delete(format!("/channels/{}", group.id()))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelDelete { id, .. } => id == &group.id(),
_ => false,
})
.await;
}
// TEST: member leaves group (no delete)
// TEST: no effect with saved messages
// TEST: DM set to inactive
// TEST: server channel deleted
}
@@ -1,60 +1,36 @@
use revolt_quark::{
models::{
channel::{Channel, FieldsChannel, PartialChannel},
message::SystemMessage,
File, User,
},
perms, Database, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, File, PartialChannel, SystemMessage, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Channel Details
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditChannel {
/// Channel name
#[validate(length(min = 1, max = 32))]
name: Option<String>,
/// Channel description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Group owner
owner: Option<String>,
/// Icon
///
/// Provide an Autumn attachment Id.
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
/// Whether this channel is age-restricted
nsfw: Option<bool>,
/// Whether this channel is archived
archived: Option<bool>,
#[validate(length(min = 1))]
remove: Option<Vec<FieldsChannel>>,
}
/// # Edit Channel
///
/// Edit a channel object by its id.
#[openapi(tag = "Channel Information")]
#[patch("/<target>", data = "<data>")]
pub async fn req(
pub async fn edit(
db: &State<Database>,
user: User,
target: Ref,
data: Json<DataEditChannel>,
) -> Result<Json<Channel>> {
target: Reference,
data: Json<v0::DataEditChannel>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageChannel)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
if data.name.is_none()
&& data.description.is_none()
@@ -63,7 +39,7 @@ pub async fn req(
&& data.owner.is_none()
&& data.remove.is_none()
{
return Ok(Json(channel));
return Ok(Json(channel.into()));
}
let mut partial: PartialChannel = Default::default();
@@ -76,12 +52,12 @@ pub async fn req(
{
// Make sure we are the owner of this group
if owner != &user.id {
return Err(Error::NotOwner);
return Err(create_error!(NotOwner));
}
// Ensure user is part of group
if !recipients.contains(&new_owner) {
return Err(Error::NotInGroup);
return Err(create_error!(NotInGroup));
}
// Transfer ownership
@@ -94,10 +70,10 @@ pub async fn req(
to: new_owner,
}
} else {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
@@ -128,7 +104,7 @@ pub async fn req(
..
} => {
if let Some(fields) = &data.remove {
if fields.contains(&FieldsChannel::Icon) {
if fields.contains(&v0::FieldsChannel::Icon) {
if let Some(icon) = &icon {
db.mark_attachment_as_deleted(&icon.id).await?;
}
@@ -136,10 +112,10 @@ pub async fn req(
for field in fields {
match field {
FieldsChannel::Description => {
v0::FieldsChannel::Description => {
description.take();
}
FieldsChannel::Icon => {
v0::FieldsChannel::Icon => {
icon.take();
}
_ => {}
@@ -175,7 +151,7 @@ pub async fn req(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
@@ -185,26 +161,36 @@ pub async fn req(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
if partial.icon.is_some() {
SystemMessage::ChannelIconChanged { by: user.id }
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
SystemMessage::ChannelIconChanged {
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
}
channel
.update(db, partial, data.remove.unwrap_or_default())
.update(
db,
partial,
data.remove
.unwrap_or_default()
.into_iter()
.map(|f| f.into())
.collect(),
)
.await?;
}
_ => return Err(Error::InvalidOperation),
_ => return Err(create_error!(InvalidOperation)),
};
Ok(Json(channel))
Ok(Json(channel.into()))
}
@@ -13,7 +13,7 @@ use rocket::{serde::json::Json, State};
/// Fetch channel by its id.
#[openapi(tag = "Channel Information")]
#[get("/<target>")]
pub async fn fetch_channel(
pub async fn fetch(
db: &State<Database>,
user: User,
target: Reference,
@@ -13,7 +13,7 @@ use rocket_empty::EmptyResponse;
/// Adds another user to the group.
#[openapi(tag = "Groups")]
#[put("/<group_id>/recipients/<member_id>")]
pub async fn req(
pub async fn add_member(
db: &State<Database>,
user: User,
group_id: Reference,
@@ -31,8 +31,7 @@ pub async fn req(
match &channel {
Channel::Group { .. } => {
// FIXME: use permissions here?
// interesting if users could block new group invites
// 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));
@@ -1,16 +1,23 @@
use revolt_quark::{
models::{Channel, User},
Db, EmptyResponse, Error, Permission, Ref, Result,
};
use revolt_database::{util::reference::Reference, Channel, Database, User};
use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Remove Member from Group
///
/// Removes a user from the group.
#[openapi(tag = "Groups")]
#[delete("/<target>/recipients/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
pub async fn remove_member(
db: &State<Database>,
user: User,
target: Reference,
member: Reference,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?;
@@ -20,24 +27,26 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyR
owner, recipients, ..
} => {
if &user.id != owner {
return Error::from_permission(Permission::ManageChannel);
return Err(create_error!(MissingPermission {
permission: ChannelPermission::ManageChannel.to_string()
}));
}
let member = member.as_user(db).await?;
if user.id == member.id {
return Err(Error::CannotRemoveYourself);
return Err(create_error!(CannotRemoveYourself));
}
if !recipients.iter().any(|x| *x == member.id) {
return Err(Error::NotInGroup);
return Err(create_error!(NotInGroup));
}
channel
.remove_user_from_group(db, &member.id, Some(&user.id), false)
.remove_user_from_group(db, &member, Some(&user.id), false)
.await
.map(|_| EmptyResponse)
}
_ => Err(Error::InvalidOperation),
_ => Err(create_error!(InvalidOperation)),
}
}
@@ -148,7 +157,7 @@ mod test {
.await;
dbg!(response.into_string().await);
// TODO: finish impl
// FIXME: finish impl
// assert_eq!(response.status(), Status::NotFound);
}
@@ -185,6 +194,6 @@ mod test {
.dispatch()
.await;
// TODO: impl assert_eq!(response.status(), Status::Forbidden);
// FIXME: impl assert_eq!(response.status(), Status::Forbidden);
}
}
@@ -1,9 +1,12 @@
use revolt_quark::{
models::{Invite, User},
perms, Db, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Invite, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use rocket::serde::json::Json;
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Create Invite
///
@@ -12,16 +15,23 @@ use rocket::serde::json::Json;
/// Channel must be a `TextChannel`.
#[openapi(tag = "Channel Invites")]
#[post("/<target>/invites")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Invite>> {
pub async fn create_invite(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<v0::Invite>> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::InviteOthers)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
Invite::create(db, &user, &channel).await.map(Json)
Invite::create_channel_invite(db, &user, &channel)
.await
.map(|invite| invite.into())
.map(Json)
}
@@ -1,31 +1,36 @@
use revolt_quark::{
models::{Channel, User},
perms, Db, Error, Permission, Ref, Result,
use futures::future::join_all;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, 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 Group Members
///
/// Retrieves all users who are part of this group.
///
/// This may not return full user information if users are not friends but have mutual connections.
#[openapi(tag = "Groups")]
#[get("/<target>/members")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> {
pub async fn fetch_members(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<Vec<v0::User>>> {
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)?;
if let Channel::Group { recipients, .. } = channel {
Ok(Json(
db.fetch_users(&recipients)
.await?
.into_iter()
.map(|x| x.with_relationship(&user))
.collect::<Vec<User>>(),
User::fetch_many_ids_as_mutuals(db, &user, &recipients).await?,
))
} else {
Err(Error::InvalidOperation)
Err(create_error!(InvalidOperation))
}
}
@@ -1,20 +1,16 @@
use chrono::Utc;
use revolt_quark::{
models::{Message, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Message, 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};
use rocket_empty::EmptyResponse;
use serde::Deserialize;
use validator::Validate;
/// # Search Parameters
#[derive(Validate, Deserialize, JsonSchema)]
pub struct OptionsBulkDelete {
/// Message IDs
#[validate(length(min = 1, max = 100))]
ids: Vec<String>,
}
/// # Bulk Delete Messages
///
/// Delete multiple messages you've sent or one you have permission to delete.
@@ -24,34 +20,37 @@ pub struct OptionsBulkDelete {
/// Messages must have been sent within the past 1 week.
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
pub async fn req(
db: &Db,
pub async fn bulk_delete_messages(
db: &State<Database>,
user: User,
target: Ref,
options: Json<OptionsBulkDelete>,
target: Reference,
options: Json<v0::OptionsBulkDelete>,
) -> Result<EmptyResponse> {
let options = options.into_inner();
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
for id in &options.ids {
if ulid::Ulid::from_string(id)
.map_err(|_| Error::InvalidOperation)?
.map_err(|_| create_error!(InvalidOperation))?
.datetime()
.signed_duration_since(Utc::now())
.num_days()
.abs()
> 7
{
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
}
perms(&user)
.channel(&target.as_channel(db).await?)
.throw_permission(db, Permission::ManageMessages)
.await?;
let channel = target.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::ManageMessages)?;
Message::bulk_delete(db, &target.id, options.ids)
.await
@@ -1,7 +1,11 @@
use revolt_quark::{
models::{message::PartialMessage, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, PartialMessage, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Remove All Reactions from Message
///
@@ -10,15 +14,20 @@ use revolt_quark::{
/// Requires `ManageMessages` permission.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions")]
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
pub async fn clear_reactions(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageMessages)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
// Fetch relevant message
let mut message = msg.as_message_in(db, channel.id()).await?;
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
// Clear reactions
message
@@ -1,21 +1,31 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Delete Message
///
/// Delete a message you've sent or one you have permission to delete.
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/<msg>", rank = 2)]
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let message = msg.as_message(db).await?;
if message.channel != target.id {
return Err(Error::NotFound);
}
pub async fn delete(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<EmptyResponse> {
let message = msg.as_message_in_channel(db, &target.id).await?;
if message.author != user.id {
perms(&user)
.channel(&target.as_channel(db).await?)
.throw_permission(db, Permission::ManageMessages)
.await?;
let channel = target.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::ManageMessages)?;
}
message.delete(db).await.map(|_| EmptyResponse)
@@ -1,60 +1,54 @@
use revolt_quark::{
models::message::{PartialMessage, SendableEmbed},
models::{Message, User},
perms,
types::january::Embed,
Db, Error, Permission, Ref, Result, Timestamp,
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_database::{
tasks,
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Message, PartialMessage, User,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{self, Embed};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Message Details
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditMessage {
/// New message content
#[validate(length(min = 1, max = 2000))]
content: Option<String>,
/// Embeds to include in the message
#[validate(length(min = 0, max = 10))]
embeds: Option<Vec<SendableEmbed>>,
}
/// # Edit Message
///
/// Edits a message that you've previously sent.
#[openapi(tag = "Messaging")]
#[patch("/<target>/messages/<msg>", data = "<edit>")]
pub async fn req(
db: &Db,
pub async fn edit(
db: &State<Database>,
user: User,
target: Ref,
msg: Ref,
edit: Json<DataEditMessage>,
) -> Result<Json<Message>> {
target: Reference,
msg: Reference,
edit: Json<v0::DataEditMessage>,
) -> Result<Json<v0::Message>> {
let edit = edit.into_inner();
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
edit.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let config = config().await;
Message::validate_sum(
&edit.content,
edit.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
)?;
// 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;
let mut message = msg.as_message(db).await?;
if message.channel != channel.id() {
return Err(Error::NotFound);
}
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?;
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
if message.author != user.id {
return Err(Error::CannotEditMessage);
return Err(create_error!(CannotEditMessage));
}
Message::validate_sum(&edit.content, edit.embeds.as_deref().unwrap_or_default())?;
message.edited = Some(Timestamp::now_utc());
let mut partial = PartialMessage {
edited: message.edited,
@@ -79,14 +73,12 @@ 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?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendEmbeds)?;
new_embeds.clear();
for embed in embeds {
new_embeds.push(embed.clone().into_embed(db, &message.id).await?);
new_embeds.push(message.create_embed(db, embed).await?);
}
}
@@ -95,12 +87,9 @@ pub async fn req(
message.update(db, partial).await?;
// Queue up a task for processing embeds if the we have sufficient permissions
if permissions
.has_permission(db, Permission::SendEmbeds)
.await?
{
if permissions.has_channel_permission(ChannelPermission::SendEmbeds) {
if let Some(content) = edit.content {
revolt_quark::tasks::process_embeds::queue(
tasks::process_embeds::queue(
message.channel.to_string(),
message.id.to_string(),
content,
@@ -109,5 +98,5 @@ pub async fn req(
}
}
Ok(Json(message))
Ok(Json(message.into()))
}
@@ -12,14 +12,13 @@ use rocket::{serde::json::Json, State};
/// Retrieves a message by its id.
#[openapi(tag = "Messaging")]
#[get("/<target>/messages/<msg>")]
pub async fn req(
pub async fn fetch(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<Json<v0::Message>> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
@@ -1,69 +1,42 @@
use revolt_quark::{
models::{
message::{
BulkMessageResponse, MessageFilter, MessageQuery, MessageSort, MessageTimePeriod,
},
User,
},
perms, Db, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{self, MessageSort};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsQueryMessages {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
/// Message id before which messages should be fetched
#[validate(length(min = 26, max = 26))]
before: Option<String>,
/// Message id after which messages should be fetched
#[validate(length(min = 26, max = 26))]
after: Option<String>,
/// Message sort direction
sort: Option<MessageSort>,
/// Message id to search around
///
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
/// It will also take half of limit rounded as the limits to each side.
/// It also fetches the message ID specified.
#[validate(length(min = 26, max = 26))]
nearby: Option<String>,
/// Whether to include user (and member, if server channel) objects
include_users: Option<bool>,
}
/// # Fetch Messages
///
/// Fetch multiple messages.
#[openapi(tag = "Messaging")]
#[get("/<target>/messages?<options..>")]
pub async fn req(
db: &Db,
pub async fn query(
db: &State<Database>,
user: User,
target: Ref,
options: OptionsQueryMessages,
) -> Result<Json<BulkMessageResponse>> {
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
target: Reference,
options: v0::OptionsQueryMessages,
) -> Result<Json<v0::BulkMessageResponse>> {
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
if let Some(MessageSort::Relevance) = options.sort {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ReadMessageHistory)
.await?;
let OptionsQueryMessages {
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ReadMessageHistory)?;
let v0::OptionsQueryMessages {
limit,
before,
after,
@@ -72,8 +45,9 @@ pub async fn req(
include_users,
} = options;
let messages = db
.fetch_messages(MessageQuery {
Message::fetch_with_users(
db,
MessageQuery {
filter: MessageFilter {
channel: Some(channel.id().to_string()),
..Default::default()
@@ -88,10 +62,16 @@ pub async fn req(
}
},
limit,
})
.await?;
BulkMessageResponse::transform(db, Some(&channel), messages, include_users)
.await
.map(Json)
},
&user,
include_users,
match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server)
}
_ => None,
},
)
.await
.map(Json)
}
@@ -1,4 +1,11 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, 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;
/// # Add Reaction to Message
///
@@ -6,20 +13,20 @@ use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Resu
#[openapi(tag = "Interactions")]
#[put("/<target>/messages/<msg>/reactions/<emoji>")]
pub async fn react_message(
db: &Db,
db: &State<Database>,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
target: Reference,
msg: Reference,
emoji: Reference,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::React)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::React)?;
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
let message = msg.as_message_in_channel(db, &channel.id()).await?;
// Add the reaction
message
@@ -1,71 +1,43 @@
use revolt_quark::{
models::{
message::{
BulkMessageResponse, MessageFilter, MessageQuery, MessageSort, MessageTimePeriod,
},
User,
},
perms, Db, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Search Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct OptionsMessageSearch {
/// Full-text search query
///
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
#[validate(length(min = 1, max = 64))]
query: String,
/// Maximum number of messages to fetch
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
/// Message id before which messages should be fetched
#[validate(length(min = 26, max = 26))]
before: Option<String>,
/// Message id after which messages should be fetched
#[validate(length(min = 26, max = 26))]
after: Option<String>,
/// Message sort direction
///
/// By default, it will be sorted by latest.
#[serde(default = "MessageSort::default")]
sort: MessageSort,
/// Whether to include user (and member, if server channel) objects
include_users: Option<bool>,
}
/// # Search for Messages
///
/// This route searches for messages within the given parameters.
#[openapi(tag = "Messaging")]
#[post("/<target>/search", data = "<options>")]
pub async fn req(
db: &Db,
pub async fn search(
db: &State<Database>,
user: User,
target: Ref,
options: Json<OptionsMessageSearch>,
) -> Result<Json<BulkMessageResponse>> {
target: Reference,
options: Json<v0::DataMessageSearch>,
) -> Result<Json<v0::BulkMessageResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let options = options.into_inner();
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ReadMessageHistory)
.await?;
let OptionsMessageSearch {
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ReadMessageHistory)?;
let v0::DataMessageSearch {
query,
limit,
before,
@@ -74,8 +46,9 @@ pub async fn req(
include_users,
} = options;
let messages = db
.fetch_messages(MessageQuery {
Message::fetch_with_users(
db,
MessageQuery {
filter: MessageFilter {
channel: Some(channel.id().to_string()),
query: Some(query),
@@ -87,10 +60,16 @@ pub async fn req(
sort: Some(sort),
},
limit,
})
.await?;
BulkMessageResponse::transform(db, Some(&channel), messages, include_users)
.await
.map(Json)
},
&user,
include_users,
match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server)
}
_ => None,
},
)
.await
.map(Json)
}
@@ -1,3 +1,4 @@
use chrono::{Duration, Utc};
use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::{
util::idempotency::IdempotencyKey, util::reference::Reference, Database, User,
@@ -31,7 +32,6 @@ pub async fn message_send(
// Ensure we have permissions to send a message
let channel = target.as_channel(db).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)?;
@@ -61,6 +61,18 @@ pub async fn message_send(
interactions.validate(db, &permissions).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 {
(Utc::now() - ulid::Ulid::from_string(&user.id).unwrap().datetime())
>= Duration::hours(12)
} else {
true
}
} else {
true
};
// Create the message
let author: v0::User = user.clone().into(db, Some(&user)).await;
Ok(Json(
@@ -71,6 +83,7 @@ pub async fn message_send(
v0::MessageAuthor::User(&author),
idempotency,
permissions.has_channel_permission(ChannelPermission::SendEmbeds),
allow_mentions,
)
.await?
.into(),
@@ -1,14 +1,12 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
use serde::{Deserialize, Serialize};
/// # Query Parameters
#[derive(Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsUnreact {
/// Remove a specific user's reaction
user_id: Option<String>,
/// Remove all reactions
remove_all: Option<bool>,
}
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::State;
use rocket_empty::EmptyResponse;
/// # Remove Reaction(s) to Message
///
@@ -18,29 +16,27 @@ pub struct OptionsUnreact {
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions/<emoji>?<options..>")]
pub async fn unreact_message(
db: &Db,
db: &State<Database>,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
options: OptionsUnreact,
target: Reference,
msg: Reference,
emoji: Reference,
options: v0::OptionsUnreact,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::React)
.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::React)?;
// Check if we need to escalate permissions
let remove_all = options.remove_all.unwrap_or_default();
if options.user_id.is_some() || remove_all {
permissions
.throw_permission(db, Permission::ManageMessages)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
let message = msg.as_message_in_channel(db, &channel.id()).await?;
// Check if we should wipe all of this reaction
if remove_all {
+19 -19
View File
@@ -28,29 +28,29 @@ mod webhook_fetch_all;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
channel_ack::req,
channel_fetch::fetch_channel,
members_fetch::req,
channel_delete::req,
channel_edit::req,
invite_create::req,
channel_ack::ack,
channel_fetch::fetch,
members_fetch::fetch_members,
channel_delete::delete,
channel_edit::edit,
invite_create::create_invite,
message_send::message_send,
message_query::req,
message_search::req,
message_fetch::req,
message_edit::req,
message_bulk_delete::req,
message_delete::req,
message_query::query,
message_search::search,
message_fetch::fetch,
message_edit::edit,
message_bulk_delete::bulk_delete_messages,
message_delete::delete,
group_create::create_group,
group_add_member::req,
group_remove_member::req,
voice_join::req,
permissions_set::req,
permissions_set_default::req,
group_add_member::add_member,
group_remove_member::remove_member,
voice_join::call,
permissions_set::set_role_permissions,
permissions_set_default::set_default_permissions,
message_react::react_message,
message_unreact::unreact_message,
message_clear_reactions::clear_reactions,
webhook_create::req,
webhook_fetch_all::req,
webhook_create::create_webhook,
webhook_fetch_all::fetch_webhooks,
]
}
@@ -1,17 +1,12 @@
use rocket::serde::json::Json;
use serde::Deserialize;
use revolt_quark::{
models::{Channel, User},
perms, Db, Error, Override, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User,
};
/// # Permission Value
#[derive(Deserialize, JsonSchema)]
pub struct Data {
/// Allow / deny values to set for this role
permissions: Override,
}
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use serde::Deserialize;
/// # Set Role Permission
///
@@ -20,40 +15,39 @@ pub struct Data {
/// Channel must be a `TextChannel` or `VoiceChannel`.
#[openapi(tag = "Channel Permissions")]
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn req(
db: &Db,
pub async fn set_role_permissions(
db: &State<Database>,
user: User,
target: Ref,
target: Reference,
role_id: String,
data: Json<Data>,
) -> Result<Json<Channel>> {
data: Json<v0::DataSetRolePermissions>,
) -> Result<Json<v0::Channel>> {
let mut channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
permissions
.throw_permission_and_view_channel(db, Permission::ManagePermissions)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?;
if let Some(server) = permissions.server.get() {
if let Some(server) = query.server_ref() {
if let Some(role) = server.roles.get(&role_id) {
if role.rank <= permissions.get_member_rank().unwrap_or(i64::MIN) {
return Err(Error::NotElevated);
if role.rank <= query.get_member_rank().unwrap_or(i64::MIN) {
return Err(create_error!(NotElevated));
}
let current_value: Override = role.permissions.into();
permissions
.throw_permission_override(db, current_value, data.permissions)
.throw_permission_override(current_value, &data.permissions)
.await?;
channel
.set_role_permission(db, &role_id, data.permissions.into())
.set_role_permission(db, &role_id, data.permissions.clone().into())
.await?;
Ok(Json(channel))
Ok(Json(channel.into()))
} else {
Err(Error::NotFound)
Err(create_error!(NotFound))
}
} else {
Err(Error::InvalidOperation)
Err(create_error!(InvalidOperation))
}
}
@@ -1,24 +1,11 @@
use rocket::serde::json::Json;
use serde::Deserialize;
use revolt_quark::{
models::{channel::PartialChannel, Channel, User},
perms, Db, Error, Override, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, PartialChannel, User,
};
/// # Permission Value
#[derive(Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum DataDefaultChannelPermissions {
Value {
/// Permission values to set for members in a `Group`
permissions: u64,
},
Field {
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
permissions: Override,
},
}
use revolt_models::v0::{self, DataDefaultChannelPermissions};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Set Default Permission
///
@@ -27,19 +14,19 @@ pub enum DataDefaultChannelPermissions {
/// Channel must be a `Group`, `TextChannel` or `VoiceChannel`.
#[openapi(tag = "Channel Permissions")]
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn req(
db: &Db,
pub async fn set_default_permissions(
db: &State<Database>,
user: User,
target: Ref,
data: Json<DataDefaultChannelPermissions>,
) -> Result<Json<Channel>> {
target: Reference,
data: Json<v0::DataDefaultChannelPermissions>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();
let mut channel = target.as_channel(db).await?;
let mut perm = perms(&user).channel(&channel);
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
perm.throw_permission_and_view_channel(db, Permission::ManagePermissions)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?;
match &channel {
Channel::Group { .. } => {
@@ -55,7 +42,7 @@ pub async fn req(
)
.await?;
} else {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
}
Channel::TextChannel {
@@ -66,30 +53,27 @@ pub async fn req(
default_permissions,
..
} => {
if let DataDefaultChannelPermissions::Field { permissions } = data {
perm.throw_permission_override(
db,
default_permissions.map(|x| x.into()),
permissions,
)
.await?;
if let DataDefaultChannelPermissions::Field { permissions: field } = data {
permissions
.throw_permission_override(default_permissions.map(|x| x.into()), &field)
.await?;
channel
.update(
db,
PartialChannel {
default_permissions: Some(permissions.into()),
default_permissions: Some(field.into()),
..Default::default()
},
vec![],
)
.await?;
} else {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
}
_ => return Err(Error::InvalidOperation),
_ => return Err(create_error!(InvalidOperation)),
}
Ok(Json(channel))
Ok(Json(channel.into()))
}
+40 -35
View File
@@ -1,40 +1,37 @@
use revolt_quark::{
models::{Channel, User},
perms,
variables::delta::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL},
Db, Error, Permission, Ref, Result,
use revolt_config::config;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
/// # Voice Server Token Response
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct CreateVoiceUserResponse {
/// Token for authenticating with the voice server
token: String,
}
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Join Call
///
/// Asks the voice server for a token to join the call.
#[openapi(tag = "Voice")]
#[post("/<target>/join_call")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUserResponse>> {
pub async fn call(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<v0::LegacyCreateVoiceUserResponse>> {
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
permissions
.throw_permission_and_view_channel(db, Permission::Connect)
.await?;
if !*USE_VOSO {
return Err(Error::VosoUnavailable);
let config = config().await;
if config.api.security.voso_legacy_token.is_empty() {
return Err(create_error!(VosoUnavailable));
}
match channel {
Channel::SavedMessages { .. } | Channel::TextChannel { .. } => {
return Err(Error::CannotJoinCall)
return Err(create_error!(CannotJoinCall))
}
_ => {}
}
@@ -44,33 +41,41 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUse
// - If not, create it.
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/room/{}", *VOSO_URL, channel.id()))
.get(&format!(
"{}/room/{}",
config.hosts.voso_legacy,
channel.id()
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
config.api.security.voso_legacy_token.clone(),
)
.send()
.await;
match result {
Err(_) => return Err(Error::VosoUnavailable),
Err(_) => return Err(create_error!(VosoUnavailable)),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => (),
reqwest::StatusCode::NOT_FOUND => {
if (client
.post(&format!("{}/room/{}", *VOSO_URL, channel.id()))
.post(&format!(
"{}/room/{}",
config.hosts.voso_legacy,
channel.id()
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
config.api.security.voso_legacy_token.clone(),
)
.send()
.await)
.is_err()
{
return Err(Error::VosoUnavailable);
return Err(create_error!(VosoUnavailable));
}
}
_ => return Err(Error::VosoUnavailable),
_ => return Err(create_error!(VosoUnavailable)),
},
}
@@ -78,13 +83,13 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUse
if let Ok(response) = client
.post(&format!(
"{}/room/{}/user/{}",
*VOSO_URL,
config.hosts.voso_legacy,
channel.id(),
user.id
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
config.api.security.voso_legacy_token,
)
.send()
.await
@@ -92,9 +97,9 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUse
response
.json()
.await
.map_err(|_| Error::InvalidOperation)
.map_err(|_| create_error!(InvalidOperation))
.map(Json)
} else {
Err(Error::VosoUnavailable)
Err(create_error!(VosoUnavailable))
}
}
@@ -16,7 +16,7 @@ use validator::Validate;
/// Creates a webhook which 3rd party platforms can use to send messages
#[openapi(tag = "Webhooks")]
#[post("/<target>/webhooks", data = "<data>")]
pub async fn req(
pub async fn create_webhook(
db: &State<Database>,
user: User,
target: Reference,
@@ -12,7 +12,7 @@ use rocket::{serde::json::Json, State};
/// Gets all webhooks inside the channel
#[openapi(tag = "Webhooks")]
#[get("/<channel_id>/webhooks")]
pub async fn req(
pub async fn fetch_webhooks(
db: &State<Database>,
user: User,
channel_id: Reference,
@@ -45,7 +45,7 @@ pub async fn create_emoji(
// Check that we haven't hit the emoji limit
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
if emojis.len() > config.features.limits.default.server_emoji {
if emojis.len() >= config.features.limits.default.server_emoji {
return Err(create_error!(TooManyEmoji {
max: config.features.limits.default.server_emoji,
}));
@@ -1,14 +1,18 @@
use revolt_quark::{
models::{Invite, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Invite, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Delete Invite
///
/// Delete an invite by its id.
#[openapi(tag = "Invites")]
#[delete("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
pub async fn delete(db: &State<Database>, user: User, target: Reference) -> Result<EmptyResponse> {
let invite = target.as_invite(db).await?;
if user.id == invite.creator() {
@@ -17,10 +21,10 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
match invite {
Invite::Server { code, server, .. } => {
let server = db.fetch_server(&server).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageServer)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?;
db.delete_invite(&code).await
}

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