Compare commits

..
110 Commits
Author SHA1 Message Date
Paul Makles 196a856c68 refactor: move and update fixme / todo comments 2023-10-28 14:53:39 +01:00
Paul Makles 9bf60e87e9 fix: validate masquerade on new message models 2023-10-28 14:53:28 +01:00
Paul Makles b90e7a4412 fix: prevent potential double fetching of cached server 2023-10-28 14:53:12 +01:00
Paul Makles cb813e91f0 chore: cut new 0.6.9 release 2023-10-28 14:39:10 +01:00
Paul Makles a6bc9fcbb3 refactor: implement Server::create 2023-10-28 14:25:29 +01:00
Paul Makles cd0737911b refactor: use fixtures to populate db
refactor: method for creating server channels
2023-10-27 16:31:37 +01:00
Paul Makles 7705354d4d ci: use reference db for generating spec 2023-10-26 15:07:03 +01:00
Paul Makles 39ac8b2278 refactor: don't create unnecessary reference 2023-10-26 14:12:28 +01:00
arslee07 c5494aa1ca fix: respect Permission::SendEmbeds when editing a message 2023-10-26 14:12:28 +01:00
Paul Makles 1a96bf5a4b chore: remove redundant file [skip ci] 2023-10-26 14:05:01 +01:00
Paul Makles 37a1244751 test: ignore hello world test because it breaks cargo test? 2023-10-26 14:05:01 +01:00
Paul Makles bf39f18f8d fix: always generate new test database 2023-10-26 14:05:01 +01:00
Paul Makles 95be7dc87a ci: use nextest because cargo test sig sevs 2023-10-26 14:05:01 +01:00
Paul Makles 9be171c7b6 fix: don't copy the old test file 2023-10-26 14:05:01 +01:00
Paul Makles 786fbb8ae4 chore: disable broken tests 2023-10-26 14:05:01 +01:00
Paul Makles 6c0210e8a1 feat: always use test configuration during tests 2023-10-26 14:05:01 +01:00
Paul Makles 456bf7b42a fix: check server ownership for text channel
refactor: minor cleanup of code
2023-10-26 14:05:01 +01:00
Paul Makles 21ffea0f9f fix(core/database): store member during permission query 2023-10-26 14:05:01 +01:00
Paul Makles 96d90215d2 fix(core/models): validator feature flag not properly gatekept 2023-10-26 14:05:01 +01:00
Paul Makles 29dcf714f2 chore: start work on test for permission issue 2023-10-26 14:05:01 +01:00
Paul Makles cb97004d3f feat(core): provide user profile where appropriate 2023-10-16 09:33:36 +01:00
Paul Makles 0b6e5dd9db chore: add nix file to vscode config 2023-10-16 09:23:11 +01:00
Paul Makles 7132877201 feat(core): validation for files in reference db 2023-10-16 09:22:53 +01:00
Paul Makles 4d42fb74e7 fix(core): publish user settings event 2023-10-16 09:22:17 +01:00
Paul Makles 866ace3381 refactor: use config for max bot count 2023-10-16 09:21:48 +01:00
Paul Makles 0798e81862 feat(core): implement ratelimit events for reference db 2023-10-16 09:21:34 +01:00
Paul Makles 2fa5ac41ac refactor: tests for more group routes 2023-09-22 19:04:17 +01:00
Paul Makles 25ae1555a2 chore: add nextest and fmt to nix 2023-09-22 19:03:49 +01:00
Paul Makles 881888bd1c chore: add timeout for nextest runs 2023-09-22 19:03:29 +01:00
Paul Makles 41f20c2239 ci: use custom Revolt.toml for tests 2023-09-10 15:46:00 +01:00
Paul Makles 38f1381fea chore: switch to resolver=2 2023-09-10 15:19:23 +01:00
Paul Makles 6838b0b1f0 refactor: ensure in-memory database consistency
test: update authifier to allow for in-memory tests
ci: explicitly run in-memory and mongo tests
2023-09-10 14:49:27 +01:00
Paul Makles 055deab105 fix: allow bots to authenticate with new routes 2023-09-10 14:48:13 +01:00
Paul Makles 6d2073120d fix: carry over send embed permission when sending [skip ci] 2023-09-10 12:41:23 +01:00
Paul Makles 9789909061 feat(delta): rewrite emoji routes w/o quark
#283
2023-09-05 12:36:18 +01:00
Paul Makles 895de86f1e fix(core/config): use uppercase filename 2023-09-03 19:00:17 +01:00
Paul Makles 98b8c5403b feat(delta): rewrite channel / message fetch 2023-09-03 18:27:21 +01:00
Paul Makles aa7c75c60e chore: ignore Revolt.toml 2023-09-03 18:12:15 +01:00
Paul Makles 4270f0c5d4 feat(core): implement create group 2023-09-03 18:12:08 +01:00
Paul Makles eb1f45d208 fix(core/database): use attachments collection name 2023-09-03 17:43:13 +01:00
Paul Makles 9929f7499a refactor(core): use core crates for sending messages 2023-09-03 16:31:39 +01:00
Paul Makles 279d9ef1b5 refactor(core): remove quark references from webhook routes 2023-09-03 15:52:18 +01:00
Paul Makles 5a9bb9e68d fix: convert index map/set to hash map/set
fix: idempotency token should use correct crate cfg
2023-08-27 15:51:20 +01:00
Paul Makles a8fc9e928a fix: import validator crate directly in derive 2023-08-27 15:45:23 +01:00
Paul Makles da231ae390 fix: remove indexmap from models 2023-08-27 15:43:57 +01:00
Paul Makles 7a31da4189 fix: add indexmap/serde to model depends 2023-08-27 15:40:05 +01:00
Paul Makles 7d0b5392b5 fix: should include crate in build script too 2023-08-27 15:33:28 +01:00
Paul Makles 1e1d44c0e4 fix: add config crate to Dockerfile
chore: bump release version
2023-08-27 15:30:53 +01:00
Paul Makles 1c4588967f merge: branch 'insert/dev-branch' 2023-08-27 15:18:52 +01:00
Paul Makles 853366a297 feat: add support for Firebase Cloud Messaging 2023-08-27 15:18:19 +01:00
Paul Makles 7568f49755 test: add tests to bot routes 2023-08-27 15:01:22 +01:00
Paul Makles 0542788567 feat(delta): test harness for web server 2023-08-27 13:01:21 +01:00
Paul Makles 73f7b8f007 feat: ensure bot invites work correctly 2023-08-27 12:21:48 +01:00
Paul Makles c2412190a8 refactor: solve clippy hints 2023-08-27 11:23:02 +01:00
Paul Makles 5372296dc0 feat(core): implement message tasks
includes:
- acknowledgements
- last_message_id
- embeds
- web push
2023-08-27 11:17:03 +01:00
Paul Makles 41a47bdf8f feat: add revolt_config crate 2023-08-27 11:15:20 +01:00
Paul Makles 51c26e324d refactor: remove quark/web 2023-08-26 16:19:00 +01:00
Paul Makles 3a55d00c6a chore: disable nix environment extension 2023-08-26 16:17:21 +01:00
Paul Makles 004e84ece9 fix: revoke all channel permissions if can't view 2023-08-26 16:17:20 +01:00
Paul Makles 3d0914ce52 chore: add nix shell configuration 2023-08-26 16:17:19 +01:00
Paul Makles bde432cb75 chore: strip bot model from quark 2023-08-26 16:16:48 +01:00
Paul Makles 28d225952a chore: add separate debug Docker build script 2023-08-26 15:34:25 +01:00
Paul Makles 2fb9e7a802 merge: branch 'master' into insert/dev-branch 2023-08-26 13:34:44 +01:00
Paul Makles 431046cb4c chore: cut new release with redis patches and debug profile 2023-08-26 13:33:05 +01:00
Zomatree 92032cd9bc chore: Log errors from failed pub/sub messages 2023-08-26 13:14:24 +01:00
Lea bfdd713d38 fix: allow fetching mutuals for bots 2023-08-26 13:14:15 +01:00
Paul Makles 430df03b20 chore: bump sentry crate 2023-08-10 09:42:44 +01:00
Paul Makles f5f70287e7 feat: add SENTRY_DSN env variable 2023-08-10 09:40:41 +01:00
Paul Makles d5ba4ebf0c feat(core/database): implement permissions backend 2023-08-05 17:42:23 +01:00
Paul Makles 3bfead4ff1 feat(core/database): system message helper 2023-08-05 17:03:16 +01:00
Paul Makles d04621b1d6 feat(core): implement message db methods 2023-08-05 16:59:58 +01:00
Paul Makles a681df04bd feat(core): permissions query, finish bots impl 2023-08-05 16:14:47 +01:00
Paul Makles 9f3c1036d0 chore: refine user and bot creation methods 2023-08-05 12:06:43 +01:00
Paul Makles c9011ac692 chore: migrate bots create route 2023-08-05 11:24:02 +01:00
Paul Makles 42f977f536 feat: start work on migrating routes 2023-08-03 22:11:48 +01:00
Paul Makles a0580c5f5a chore: remove legacy query route 2023-08-03 19:43:05 +01:00
Paul Makles 121a9cd87c feat(core): implement message model
closes #273
2023-08-03 19:32:47 +01:00
Paul Makles d87d608d9e chore(core): flesh out implementations for channels / servers 2023-08-03 18:27:13 +01:00
Paul Makles bd9ecc1519 fix: missing conversion 2023-08-02 00:31:19 +01:00
Paul Makles f5a113da47 feat(core): implement server ban
closes #274
2023-08-01 21:19:07 +01:00
Paul Makles 11fdb0c1dc feat(core): implement emoji model
closes #270
2023-08-01 20:45:58 +01:00
Paul Makles e0033ceb12 feat(core): implement channel unreads model
closes #269
2023-08-01 17:41:43 +01:00
Paul Makles 7318ec6ef6 chore(core/models): add cfg_attr where necessary 2023-08-01 15:54:56 +01:00
Paul Makles a516c7adcf feat(core): implement channel invite model
closes #268
2023-08-01 15:53:43 +01:00
Paul Makles c48109ca66 docs: add note for 'Id' casing 2023-08-01 15:51:57 +01:00
Paul Makles f0ef781cd7 chore: add MSRV to README [skip ci]
closes #254
2023-08-01 15:04:45 +01:00
Paul Makles d9dd040db4 feat(core/models): implement server members model
closes #280
2023-08-01 15:00:07 +01:00
Paul Makles fec947b4c8 feat(core/models): implement server model
closes #279
2023-08-01 14:55:25 +01:00
Paul Makles ec6df36c25 chore: strip legacy admin API 2023-08-01 14:35:36 +01:00
Paul Makles 76150db293 chore: add prometheus metrics 2023-07-04 17:52:40 +01:00
Paul Makles 815c3fe99e chore: cut a new release (0.6.4) 2023-07-02 08:45:39 +01:00
Zomatree 54878e8e8d feat: add webhook permissions 2023-07-02 08:41:38 +01:00
TheBobBobs 6e4798f1d4 fix(bonfire): populate cache for calculating permissions (#259) 2023-07-02 08:40:15 +01:00
TheBobBobs 55bd6fb087 fix: incorrect owner rank 2023-07-02 08:35:49 +01:00
Paul Makles 49035f4817 feat: ratelimit user edit route and discriminator changes 2023-06-15 19:24:53 +01:00
Paul Makles c0ebaa0bd3 chore: include discriminator in ban list response 2023-06-15 19:23:47 +01:00
Paul Makles b98b244fc3 fix: enable staging for API spec generation 2023-06-15 15:21:00 +01:00
Paul Makles c8d5128b0c chore: add additional sanitisation 2023-06-11 12:26:18 +01:00
Paul Makles 0578a05a05 feat: add remove "displayname" field 2023-06-11 12:05:08 +01:00
Paul Makles 9a412b3e08 fix: actually update the display name 2023-06-11 11:50:21 +01:00
Paul Makles 8deec1f80a fix: display name is optional 2023-06-11 10:56:03 +01:00
Paul Makles 26afbeed84 chore: extend discriminator block list 2023-06-11 10:49:51 +01:00
Paul Makles 9975de01bc chore: remove webhooks from production 2023-06-11 10:09:50 +01:00
Paul Makles ac525466b8 chore: cut a new release (0.6.0) 2023-06-11 10:04:15 +01:00
Paul Makles c7a04e4559 fix: conflict resolution in migration
fix: write new username and display name
chore: restrict discriminator search space
2023-06-11 09:53:41 +01:00
Paul Makles 5bbe30edbc feat(core/database): migrate to discriminators 2023-06-11 09:16:24 +01:00
Paul Makles 31c7dc0577 feat: add discriminator and display name fields 2023-06-09 16:34:18 +01:00
Paul Makles aba5c7d8af fix: shouldn't prefix dep: 2023-06-04 19:45:47 +01:00
Paul Makles 2f4ea4cabb refactor(core/models): transitively apply feature flags 2023-06-04 19:45:31 +01:00
Paul Makles 7e801a31bd fix: make sure feature flags are respected 2023-06-04 19:43:56 +01:00
216 changed files with 12001 additions and 2495 deletions
+2
View File
@@ -0,0 +1,2 @@
[profile.default]
slow-timeout = { period = "3s", terminate-after = 2 }
+3
View File
@@ -59,6 +59,9 @@ REVOLT_UNSAFE_NO_EMAIL=1
## Application Settings ## Application Settings
## ##
# Whether to enable staging only features
REVOLT_IS_STAGING=1
# Whether to only allow users to sign up if they have an invite code # Whether to only allow users to sign up if they have an invite code
REVOLT_INVITE_ONLY=0 REVOLT_INVITE_ONLY=0
+20 -13
View File
@@ -20,6 +20,11 @@ jobs:
toolchain: stable toolchain: stable
override: true override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Install cargo-nextest
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-nextest
locked: true
- name: Run cargo build - name: Run cargo build
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -30,25 +35,27 @@ jobs:
run: | run: |
docker-compose -f docker-compose.db.yml up -d docker-compose -f docker-compose.db.yml up -d
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
- name: Run cargo test (with MongoDB)
uses: actions-rs/cargo@v1
env:
MONGODB: mongodb://localhost
with:
command: test
- name: Copy .env.example - name: Copy .env.example
if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: | run: |
cp .env.example .env cp .env.example .env
- name: Run cargo test
env:
TEST_DB: REFERENCE
run: |
cargo nextest run
- name: Run cargo test (with MongoDB)
env:
TEST_DB: MONGODB
MONGODB: mongodb://localhost
run: |
cargo nextest run
- name: Start API in background - name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'master' if: github.event_name != 'pull_request' && github.ref_name == 'master'
env:
TEST_DB: REFERENCE
run: | run: |
cargo run --bin revolt-delta & cargo run --bin revolt-delta &
+3
View File
@@ -1,6 +1,9 @@
Rocket.toml Rocket.toml
Revolt.toml
target target
.data .data
.env .env
.vercel .vercel
.DS_Store
+5 -3
View File
@@ -1,4 +1,6 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy" "rust-analyzer.checkOnSave.command": "clippy",
} "nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
}
Generated
+1013 -77
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -1,6 +1,10 @@
[workspace] [workspace]
resolver = "2"
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"] members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
[patch.crates-io] [patch.crates-io]
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" } # mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
redis = { git = "https://github.com/insertish/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" } redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
+1
View File
@@ -20,6 +20,7 @@ COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/ COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/ COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/quark/Cargo.toml ./crates/quark/ COPY crates/quark/Cargo.toml ./crates/quark/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/ COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/ COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
+29
View File
@@ -0,0 +1,29 @@
# Build Stage
FROM rust:1.70.0-slim
USER 0:0
WORKDIR /home/rust/src
# Install build requirements
RUN apt-get update && \
apt-get install -y \
make \
pkg-config \
libssl-dev
COPY scripts/build-image-layer.sh /tmp/
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/quark/Cargo.toml ./crates/quark/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps
COPY crates ./crates
RUN sh /tmp/build-image-layer.sh apps
+4
View File
@@ -21,6 +21,10 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers) - [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing) - [Contribution Guide](https://developers.revolt.chat/contributing)
## Minimum Supported Rust Version
Rust 1.70 or higher.
## Contributing ## Contributing
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing). The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
+6
View File
@@ -2,6 +2,10 @@
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project. Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
## Writing Style
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
## `core/database` crate ## `core/database` crate
w.r.t. `model.rs` files w.r.t. `model.rs` files
@@ -22,6 +26,7 @@ w.r.t. `model.rs` files
); );
``` ```
- `auto_derived!` macro accepts multiple entries and should be used as such: - `auto_derived!` macro accepts multiple entries and should be used as such:
```rust ```rust
auto_derived!( auto_derived!(
/// Optional fields on server object /// Optional fields on server object
@@ -31,6 +36,7 @@ w.r.t. `model.rs` files
pub enum FieldsRole { .. } pub enum FieldsRole { .. }
); );
``` ```
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root: - If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
```rust ```rust
#[serde(skip_serializing_if = "crate::if_false", default)] #[serde(skip_serializing_if = "crate::if_false", default)]
+16 -3
View File
@@ -1,15 +1,28 @@
disallowed-methods = [ disallowed-methods = [
# Shouldn't need to access these directly # Shouldn't need to access these directly
"revolt_database::models::bots::model::Bot::remove_field", "revolt_database::models::bots::model::Bot::remove_field",
"revolt_database::models::messages::model::Message::attach_sendable_embed",
"revolt_database::models::users::model::User::set_relationship",
"revolt_database::models::users::model::User::apply_relationship",
# Prefer to use Object::create() # Prefer to use Object::create()
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::insert_account_strike", "revolt_database::models::bots::ops::AbstractBots::insert_bot",
"revolt_database::models::channel_invites::ops::AbstractChannelInvites::insert_invite",
"revolt_database::models::channel_unreads::ops::AbstractChannelUnreads::acknowledge_message",
"revolt_database::models::channel_webhooks::ops::AbstractWebhooks::insert_webhook",
"revolt_database::models::channels::ops::AbstractChannels::insert_channel",
"revolt_database::models::emojis::ops::AbstractEmojis::insert_emoji",
"revolt_database::models::files::ops::AbstractAttachments::insert_attachment",
"revolt_database::models::messages::ops::AbstractMessages::insert_message",
"revolt_database::models::ratelimit_events::ops::AbstractRatelimitEvents::insert_ratelimit_event",
"revolt_database::models::server_bans::ops::AbstractServerBans::insert_ban",
"revolt_database::models::server_members::ops::AbstractServerMembers::insert_member",
"revolt_database::models::servers::ops::AbstractServers::insert_server",
"revolt_database::models::users::ops::AbstractUsers::insert_user",
# Prefer to use Object::update(&self) # Prefer to use Object::update(&self)
"revolt_database::models::bots::ops::AbstractBots::update_bot", "revolt_database::models::bots::ops::AbstractBots::update_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
# Prefer to use Object::delete(&self) # Prefer to use Object::delete(&self)
"revolt_database::models::bots::ops::AbstractBots::delete_bot", "revolt_database::models::bots::ops::AbstractBots::delete_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
] ]
+8 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.6.0-rc.2" version = "0.6.9"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -25,7 +25,12 @@ serde = "1.0.136"
# async # async
futures = "0.3.21" futures = "0.3.21"
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] } async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] } async-std = { version = "1.8.0", features = [
"tokio1",
"tokio02",
"attributes",
] }
# core # core
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
sentry = "0.31.5"
+9 -4
View File
@@ -136,13 +136,13 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
);*/ );*/
// Handle incoming events. // Handle incoming events.
match conn.on_message().next().await.map(|item| { match conn.on_message().next().await.map(|res| {
( res.map(|item|(
item.get_channel_name().to_string(), item.get_channel_name().to_string(),
redis_kiss::decode_payload::<EventV1>(&item), redis_kiss::decode_payload::<EventV1>(&item),
) ))
}) { }) {
Some((channel, item)) => { Some(Ok((channel, item))) => {
if let Ok(mut event) = item { if let Ok(mut event) = item {
if state if state
.handle_incoming_event_v1( .handle_incoming_event_v1(
@@ -160,6 +160,11 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
warn!("Failed to deserialise an event for {channel}!"); 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 // No more data, assume we disconnected or otherwise
// something bad occurred, so disconnect user. // something bad occurred, so disconnect user.
None => break, None => break,
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "revolt-config"
version = "0.6.9"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Configuration"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
test = ["async-std"]
default = ["test"]
[dependencies]
# Utility
config = "0.13.3"
cached = "0.44.0"
once_cell = "1.18.0"
# Serde
serde = { version = "1", features = ["derive"] }
# Async
futures-locks = "0.7.1"
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
+3
View File
@@ -0,0 +1,3 @@
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"
+66
View File
@@ -0,0 +1,66 @@
[database]
mongodb = "mongodb://database"
redis = "redis://redis/"
[hosts]
app = "http://local.revolt.chat"
api = "http://local.revolt.chat/api"
events = "ws://local.revolt.chat/ws"
autumn = "http://local.revolt.chat/autumn"
january = "http://local.revolt.chat/january"
voso_legacy = ""
voso_legacy_ws = ""
[api]
staging = false
[api.registration]
invite_only = false
[api.smtp]
host = ""
username = ""
password = ""
from_address = ""
[api.vapid]
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
[api.fcm]
api_key = ""
[api.security]
authifier_shield_key = ""
voso_legacy_token = ""
[api.security.captcha]
hcaptcha_key = ""
hcaptcha_sitekey = ""
[api.workers]
max_concurrent_connections = 50
[features]
[features.limits]
[features.limits.default]
group_size = 100
bots = 5
message_length = 2000
message_embeds = 5
message_replies = 5
message_attachments = 5
message_reactions = 20
servers = 100
server_emoji = 100
server_roles = 200
server_channels = 200
attachment_size = 20000000
avatar_size = 4000000
background_size = 6000000
icon_size = 2500000
banner_size = 6000000
emoji_size = 500000
+169
View File
@@ -0,0 +1,169 @@
use std::collections::HashMap;
use cached::proc_macro::cached;
use config::{Config, File, FileFormat};
use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
RwLock::new({
let mut builder = Config::builder().add_source(File::from_str(
include_str!("../Revolt.toml"),
FileFormat::Toml,
));
if std::env::var("TEST_DB").is_ok() {
builder = builder.add_source(File::from_str(
include_str!("../Revolt.test.toml"),
FileFormat::Toml,
));
} else if std::path::Path::new("Revolt.toml").exists() {
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
}
builder.build().unwrap()
})
});
// https://gifbox.me/view/gT5mqxYKCZv-twilight-meow
#[derive(Deserialize, Debug, Clone)]
pub struct Database {
pub mongodb: String,
pub redis: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Hosts {
pub app: String,
pub api: String,
pub events: String,
pub autumn: String,
pub january: String,
pub voso_legacy: String,
pub voso_legacy_ws: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiRegistration {
pub invite_only: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSmtp {
pub host: String,
pub username: String,
pub password: String,
pub from_address: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiVapid {
pub private_key: String,
pub public_key: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiFcm {
pub api_key: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurityCaptcha {
pub hcaptcha_key: String,
pub hcaptcha_sitekey: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurity {
pub authifier_shield_key: String,
pub voso_legacy_token: String,
pub captcha: ApiSecurityCaptcha,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiWorkers {
pub max_concurrent_connections: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Api {
pub staging: bool,
pub registration: ApiRegistration,
pub smtp: ApiSmtp,
pub vapid: ApiVapid,
pub fcm: ApiFcm,
pub security: ApiSecurity,
pub workers: ApiWorkers,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimits {
pub group_size: usize,
pub bots: usize,
pub message_length: usize,
pub message_replies: usize,
pub message_attachments: usize,
pub message_embeds: usize,
pub message_reactions: usize,
pub servers: usize,
pub server_emoji: usize,
pub server_roles: usize,
pub server_channels: usize,
pub attachment_size: usize,
pub avatar_size: usize,
pub background_size: usize,
pub icon_size: usize,
pub banner_size: usize,
pub emoji_size: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimitsCollection {
pub default: FeaturesLimits,
#[serde(flatten)]
pub roles: HashMap<String, FeaturesLimits>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Features {
pub limits: FeaturesLimitsCollection,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Settings {
pub database: Database,
pub hosts: Hosts,
pub api: Api,
pub features: Features,
}
pub async fn init() {
println!(
":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m",
config().await
);
}
pub async fn read() -> Config {
CONFIG_BUILDER.read().await.clone()
}
#[cached(time = 30)]
pub async fn config() -> Settings {
read().await.try_deserialize::<Settings>().unwrap()
}
#[cfg(feature = "test")]
#[cfg(test)]
mod tests {
use crate::init;
#[async_std::test]
async fn it_works() {
init().await;
}
}
+40 -13
View File
@@ -1,37 +1,51 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.6.0-rc.2" version = "0.6.9"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ] authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Database Implementation" description = "Revolt Backend: Database Implementation"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
# Databases # Databases
mongodb = [ "dep:mongodb", "bson" ] mongodb = ["dep:mongodb", "bson"]
# ... Other # ... Other
async-std-runtime = [ "async-std" ] tasks = ["isahc", "linkify", "url-escape"]
rocket-impl = [ "rocket", "schemars" ] async-std-runtime = ["async-std"]
redis-is-patched = [ "revolt-presence/redis-is-patched" ] rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
# Default Features # Default Features
default = [ "mongodb", "async-std-runtime" ] default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.6.0-rc.2", path = "../result" } revolt-config = { version = "0.6.9", path = "../config" }
revolt-models = { version = "0.6.0-rc.2", path = "../models" } revolt-result = { version = "0.6.9", path = "../result" }
revolt-presence = { version = "0.6.0-rc.2", path = "../presence" } revolt-models = { version = "0.6.9", path = "../models" }
revolt-permissions = { version = "0.6.0-rc.2", path = "../permissions", features = [ "serde", "bson" ] } revolt-presence = { version = "0.6.9", path = "../presence" }
revolt-permissions = { version = "0.6.9", path = "../permissions", features = [
"serde",
"bson",
] }
# Utility # Utility
log = "0.4" log = "0.4"
lru = "0.11.0"
rand = "0.8.5"
ulid = "1.0.0" ulid = "1.0.0"
nanoid = "0.4.0" nanoid = "0.4.0"
base64 = "0.21.3"
once_cell = "1.17" once_cell = "1.17"
indexmap = "1.9.1"
decancer = "1.6.2"
deadqueue = "0.2.4"
linkify = { optional = true, version = "0.8.1" }
url-escape = { optional = true, version = "0.1.1" }
isahc = { optional = true, version = "1.7", features = ["json"] }
# Serialisation # Serialisation
serde_json = "1" serde_json = "1"
@@ -46,8 +60,13 @@ redis-kiss = { version = "0.1.4" }
bson = { optional = true, version = "2.1.0" } bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "2.1.0", default-features = false } mongodb = { optional = true, version = "2.1.0", default-features = false }
# Database Migration
unicode-segmentation = "1.10.1"
regex = "1"
# Async Language Features # Async Language Features
futures = "0.3.19" futures = "0.3.19"
async-lock = "2.8.0"
async-trait = "0.1.51" async-trait = "0.1.51"
async-recursion = "1.0.4" async-recursion = "1.0.4"
@@ -56,7 +75,15 @@ async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Rocket Impl # Rocket Impl
schemars = { version = "0.8.8", optional = true } schemars = { version = "0.8.8", optional = true }
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"], optional = true } rocket = { version = "0.5.0-rc.2", default-features = false, features = [
"json",
], optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.9.1", optional = true }
# Notifications
fcm = "0.9.2"
web-push = "0.10.0"
# Authifier # Authifier
authifier = { version = "1.0" } authifier = { version = "1.0.8" }
@@ -0,0 +1,89 @@
[
{
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Moderator",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "User",
"discriminator": "0001"
},
{
"_object_type": "Channel",
"_id": "__ID:3__",
"channel_type": "TextChannel",
"name": "General",
"server": "__ID:4__",
"default_permissions": {
"a": 0,
"d": 1048576
},
"role_permissions": {
"__ID:5__": {
"a": 1048576,
"d": 0
}
}
},
{
"_object_type": "Server",
"_id": "__ID:4__",
"owner": "__ID:0__",
"name": "Server",
"channels": ["__ID:3__"],
"roles": {
"__ID:5__": {
"name": "Moderator",
"permissions": {
"a": 545270208,
"d": 0
},
"rank": 3
},
"__ID:6__": {
"name": "Owner",
"permissions": {
"a": 0,
"d": 0
},
"rank": 0
}
},
"default_permissions": 4000322560
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:0__",
"server": "__ID:4__"
},
"roles": ["__ID:6__"],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:1__",
"server": "__ID:4__"
},
"roles": ["__ID:5__"],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:2__",
"server": "__ID:4__"
},
"joined_at": 1698318340195
}
]
+32 -10
View File
@@ -1,6 +1,9 @@
mod mongodb; mod mongodb;
mod reference; mod reference;
use rand::Rng;
use revolt_config::config;
pub use self::mongodb::*; pub use self::mongodb::*;
pub use self::reference::*; pub use self::reference::*;
@@ -31,25 +34,44 @@ impl DatabaseInfo {
/// Create a database client from the given database information /// Create a database client from the given database information
#[async_recursion] #[async_recursion]
pub async fn connect(self) -> Result<Database, String> { pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
Ok(match self { Ok(match self {
DatabaseInfo::Auto => { DatabaseInfo::Auto => {
if let Ok(uri) = std::env::var("MONGODB") { if std::env::var("TEST_DB").is_ok() {
return DatabaseInfo::MongoDb { DatabaseInfo::Test(format!(
uri, "revolt_test_{}",
rand::thread_rng().gen_range(1_000_000..10_000_000)
))
.connect()
.await?
} else if !config.database.mongodb.is_empty() {
DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name: "revolt".to_string(), database_name: "revolt".to_string(),
} }
.connect() .connect()
.await; .await?
} else {
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Test(database_name) => { DatabaseInfo::Test(database_name) => {
if let Ok(uri) = std::env::var("MONGODB") { match std::env::var("TEST_DB")
return DatabaseInfo::MongoDb { uri, database_name }.connect().await; .expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
.as_str()
{
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
"MONGODB" => {
DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name,
}
.connect()
.await?
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
} }
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Reference => Database::Reference(Default::default()), DatabaseInfo::Reference => Database::Reference(Default::default()),
DatabaseInfo::MongoDb { uri, database_name } => { DatabaseInfo::MongoDb { uri, database_name } => {
+1 -1
View File
@@ -238,6 +238,6 @@ pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_j
let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap(); let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap();
v.into_iter() v.into_iter()
.filter(|(_k, v)| !v.is_null()) .filter(|(_k, v)| !v.is_null())
.map(|(k, v)| (prefix.to_owned() + &k, v)) .map(|(k, v)| (format!("{}{}", prefix.to_owned(), k), v))
.collect() .collect()
} }
+10 -10
View File
@@ -3,29 +3,29 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex; use futures::lock::Mutex;
use crate::{ use crate::{
AccountStrike, Bot, Channel, File, Member, MemberCompositeKey, Server, User, UserSettings, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
Webhook, MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, User, UserSettings, Webhook,
}; };
database_derived!( database_derived!(
/// Reference implementation /// Reference implementation
#[derive(Default)] #[derive(Default)]
pub struct ReferenceDb { pub struct ReferenceDb {
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>, pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>, pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>, pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>, pub users: Arc<Mutex<HashMap<String, User>>>,
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>, pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>, pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub server_bans: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>, pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>, pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
pub emoji: Arc<Mutex<HashMap<String, ()>>>,
pub messages: Arc<Mutex<HashMap<String, ()>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
} }
); );
+8 -14
View File
@@ -2,7 +2,10 @@ use authifier::AuthifierEvent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revolt_models::v0::{ use revolt_models::v0::{
Channel, FieldsChannel, FieldsWebhook, PartialChannel, PartialWebhook, Webhook, AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, User,
UserSettings, Webhook,
}; };
use revolt_result::Error; use revolt_result::Error;
@@ -55,8 +58,7 @@ pub enum EventV1 {
}, },
/// Ping response /// Ping response
Pong { data: Ping }, Pong { data: Ping }, */
/// New message /// New message
Message(Message), Message(Message),
@@ -153,13 +155,7 @@ pub enum EventV1 {
}, },
/// Relationship with another user changed /// Relationship with another user changed
UserRelationship { UserRelationship { id: String, user: User },
id: String,
user: User,
// ! this field can be deprecated
status: RelationshipStatus,
},
/// Settings updated remotely /// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings }, UserSettingsUpdate { id: String, update: UserSettings },
@@ -173,14 +169,13 @@ pub enum EventV1 {
/// ///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear. /// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 }, UserPlatformWipe { user_id: String, flags: i32 },
/// New emoji /// New emoji
EmojiCreate(Emoji), EmojiCreate(Emoji),
/// Delete emoji /// Delete emoji
EmojiDelete { id: String }, EmojiDelete { id: String },
/// New report /*/// New report
ReportCreate(Report), */ ReportCreate(Report), */
/// New channel /// New channel
ChannelCreate(Channel), ChannelCreate(Channel),
@@ -248,8 +243,7 @@ impl EventV1 {
pub async fn p_user(self, id: String, db: &Database) { pub async fn p_user(self, id: String, db: &Database) {
self.clone().p(id.clone()).await; self.clone().p(id.clone()).await;
// ! FIXME: this should be captured by member list in the future // TODO: this should be captured by member list in the future and not immediately fanned out to users
// ! and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await { if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members { for member in members {
self.clone().p(member.id.server).await; self.clone().p(member.id.server).await;
+2 -1
View File
@@ -43,7 +43,7 @@ macro_rules! auto_derived {
macro_rules! auto_derived_partial { macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => { ( $item:item, $name:expr ) => {
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
#[optional_name = $name] #[optional_name = $name]
#[opt_skip_serializing_none] #[opt_skip_serializing_none]
@@ -81,6 +81,7 @@ pub mod util;
pub use models::*; pub use models::*;
pub mod events; pub mod events;
pub mod tasks;
/// Utility function to check if a boolean value is false /// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool { pub fn if_false(t: &bool) -> bool {
@@ -18,7 +18,7 @@ mod tests {
db.migrate_database().await.unwrap(); db.migrate_database().await.unwrap();
// Migrate the existing database // Migrate the existing database
db.migrate_database().await.unwrap() db.migrate_database().await.unwrap();
}); });
} }
} }
@@ -44,6 +44,10 @@ pub async fn create_database(db: &MongoDb) {
.await .await
.expect("Failed to create channel_unreads collection."); .expect("Failed to create channel_unreads collection.");
db.create_collection("channel_webhooks", None)
.await
.expect("Failed to create channel_webhooks collection.");
db.create_collection("migrations", None) db.create_collection("migrations", None)
.await .await
.expect("Failed to create migrations collection."); .expect("Failed to create migrations collection.");
@@ -72,6 +76,10 @@ pub async fn create_database(db: &MongoDb) {
.await .await
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
db.create_collection("ratelimit_events", None)
.await
.expect("Failed to create ratelimit_events collection.");
db.create_collection( db.create_collection(
"pubsub", "pubsub",
CreateCollectionOptions::builder() CreateCollectionOptions::builder()
@@ -91,6 +99,18 @@ pub async fn create_database(db: &MongoDb) {
"username": 1_i32 "username": 1_i32
}, },
"name": "username", "name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true, "unique": true,
"collation": { "collation": {
"locale": "en", "locale": "en",
@@ -193,5 +213,24 @@ pub async fn create_database(db: &MongoDb) {
.await .await
.expect("Failed to save migration info."); .expect("Failed to save migration info.");
db.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
.await
.expect("Failed to create ratelimit_events index.");
info!("Created database."); info!("Created database.");
} }
@@ -1,14 +1,17 @@
use std::{ops::BitXor, time::Duration}; use std::{collections::HashSet, ops::BitXor, time::Duration};
use crate::{ use crate::{
mongodb::{ mongodb::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document}, bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions, options::FindOptions,
}, },
MongoDb, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
}; };
use futures::StreamExt; use futures::StreamExt;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct MigrationInfo { struct MigrationInfo {
@@ -16,7 +19,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 23; pub const LATEST_REVISION: i32 = 26;
pub async fn migrate_database(db: &MongoDb) { pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations"); let migrations = db.col::<Document>("migrations");
@@ -694,7 +697,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
if revision <= 19 { if revision <= 19 {
info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable."); info!(
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
);
db.db() db.db()
.create_collection("safety_reports", None) .create_collection("safety_reports", None)
@@ -705,19 +710,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.create_collection("safety_snapshots", None) .create_collection("safety_snapshots", None)
.await .await
.unwrap(); .unwrap();
db.col::<Document>("safety_reports")
.update_many(
doc! {},
doc! {
"$set": {
"status": "Created"
}
},
None,
)
.await
.unwrap();
} }
if revision <= 20 { if revision <= 20 {
@@ -768,8 +760,231 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to update server members."); .expect("Failed to update server members.");
} }
if revision <= 23 {
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
db.db()
.run_command(
doc! {
"dropIndexes": "users",
"index": "username"
},
None,
)
.await
.expect("Failed to drop existing username index.");
#[derive(Serialize, Deserialize)]
struct UserInformation {
#[serde(rename = "_id")]
id: String,
username: String,
}
let re_username = regex::Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap();
let users: Vec<UserInformation> = db
.col::<UserInformation>("users")
.find(doc! {}, None)
.await
.unwrap()
.map(|doc| doc.expect("id and username"))
.collect()
.await;
let search_space: Vec<String> = DISCRIMINATOR_SEARCH_SPACE.iter().cloned().collect();
let mut claimed: HashSet<String> = HashSet::new();
for i in 0..users.len() {
let info = &users[i];
let mut discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
if re_username.is_match(&info.username) {
while claimed.contains(&format!("{}#{}", info.username, discriminator)) {
let new_discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
info!(
"Re-rolled {} to {new_discriminator} from {discriminator}",
info.username
);
discriminator = new_discriminator;
}
claimed.insert(format!("{}#{}", info.username, discriminator));
info!(
"({}/{}) Migrating user \"{}\" to #{} - compliant",
i + 1,
users.len(),
info.username,
discriminator
);
db.col::<UserInformation>("users")
.update_one(
doc! {
"_id": &info.id
},
doc! {
"$set": {
"discriminator": discriminator
}
},
None,
)
.await
.unwrap();
} else {
let mut sanitised = info
.username
.graphemes(true)
.filter(|s| re_username.is_match(s))
.collect::<String>();
while sanitised.len() < 2 {
sanitised += "_";
}
while claimed.contains(&format!("{}#{}", sanitised, discriminator)) {
let new_discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
info!("Re-rolled {sanitised} to {new_discriminator} from {discriminator}");
discriminator = new_discriminator;
}
claimed.insert(format!("{}#{}", sanitised, discriminator));
info!(
"({}/{}) Migrating user \"{}\" to #{} - sanitised: \"{}\"",
i + 1,
users.len(),
info.username,
discriminator,
sanitised
);
db.col::<UserInformation>("users")
.update_one(
doc! {
"_id": &info.id
},
doc! {
"$set": {
"username": sanitised,
"discriminator": discriminator,
"display_name": &info.username
}
},
None,
)
.await
.unwrap();
}
}
}
if revision <= 24 {
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
db.db()
.create_collection("channel_webhooks", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
},
None,
)
.await
.expect("Failed to create username index.");
};
if revision <= 25 {
info!("Running migration [revision 25 / 11-06-2023]: Add permissions to webhooks.");
db.col::<Document>("webhooks")
.update_many(
doc! {},
doc! {
"$set": {
"permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64
}
},
None,
)
.await
.expect("Failed to update webhooks.");
}
if revision <= 25 {
info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index.");
db.db()
.create_collection("ratelimit_events", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
.await
.expect("Failed to create ratelimit_events index.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`. // Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION LATEST_REVISION.max(revision)
} }
+87 -26
View File
@@ -1,6 +1,8 @@
use revolt_config::config;
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid;
use crate::Database; use crate::{BotInformation, Database, PartialUser, User};
auto_derived_partial!( auto_derived_partial!(
/// Bot /// Bot
@@ -49,8 +51,70 @@ auto_derived!(
} }
); );
#[allow(clippy::derivable_impls)]
impl Default for Bot {
fn default() -> Self {
Self {
id: Default::default(),
owner: Default::default(),
token: Default::default(),
public: Default::default(),
analytics: Default::default(),
discoverable: Default::default(),
interactions_url: Default::default(),
terms_of_service_url: Default::default(),
privacy_policy_url: Default::default(),
flags: Default::default(),
}
}
}
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
impl Bot { impl Bot {
/// Create a new bot
pub async fn create<D>(db: &Database, username: String, owner: &User, data: D) -> Result<Bot>
where
D: Into<Option<PartialBot>>,
{
if owner.bot.is_some() {
return Err(create_error!(IsBot));
}
let config = config().await;
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
return Err(create_error!(ReachedMaximumBots));
}
let id = Ulid::new().to_string();
User::create(
db,
username,
Some(id.to_string()),
Some(PartialUser {
bot: Some(BotInformation {
owner: id.to_string(),
}),
..Default::default()
}),
)
.await?;
let mut bot = Bot {
id,
owner: owner.id.to_string(),
token: nanoid::nanoid!(64),
..Default::default()
};
if let Some(data) = data.into() {
bot.apply_options(data);
}
db.insert_bot(&bot).await?;
Ok(bot)
}
/// Remove a field from this object /// Remove a field from this object
pub fn remove_field(&mut self, field: &FieldsBot) { pub fn remove_field(&mut self, field: &FieldsBot) {
match field { match field {
@@ -96,27 +160,24 @@ mod tests {
#[async_std::test] #[async_std::test]
async fn crud() { async fn crud() {
database_test!(|db| async move { database_test!(|db| async move {
let bot_id = "bot"; let owner = User::create(&db, "Owner".to_string(), None, None)
let user_id = "user"; .await
let token = "my_token"; .unwrap();
let user = User { let bot = Bot::create(
id: bot_id.to_string(), &db,
username: "Bot Name".to_string(), "Bot Name".to_string(),
..Default::default() &owner,
}; PartialBot {
token: Some("my token".to_string()),
interactions_url: Some("some url".to_string()),
..Default::default()
},
)
.await
.unwrap();
db.insert_user(&user).await.unwrap(); assert!(!bot.interactions_url.is_empty());
let bot = Bot {
id: bot_id.to_string(),
owner: user_id.to_string(),
token: token.to_string(),
interactions_url: "some url".to_string(),
..Default::default()
};
db.insert_bot(&bot).await.unwrap();
let mut updated_bot = bot.clone(); let mut updated_bot = bot.clone();
updated_bot updated_bot
@@ -131,9 +192,9 @@ mod tests {
.await .await
.unwrap(); .unwrap();
let fetched_bot1 = db.fetch_bot(bot_id).await.unwrap(); let fetched_bot1 = db.fetch_bot(&bot.id).await.unwrap();
let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap(); let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap();
let fetched_bots = db.fetch_bots_by_user(user_id).await.unwrap(); let fetched_bots = db.fetch_bots_by_user(&owner.id).await.unwrap();
assert!(!bot.public); assert!(!bot.public);
assert!(fetched_bot1.public); assert!(fetched_bot1.public);
@@ -143,12 +204,12 @@ mod tests {
assert_eq!(updated_bot, fetched_bot1); assert_eq!(updated_bot, fetched_bot1);
assert_eq!(fetched_bot1, fetched_bot2); assert_eq!(fetched_bot1, fetched_bot2);
assert_eq!(fetched_bot1, fetched_bots[0]); assert_eq!(fetched_bot1, fetched_bots[0]);
assert_eq!(1, db.get_number_of_bots_by_user(user_id).await.unwrap()); assert_eq!(1, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
bot.delete(&db).await.unwrap(); bot.delete(&db).await.unwrap();
assert!(db.fetch_bot(bot_id).await.is_err()); assert!(db.fetch_bot(&bot.id).await.is_err());
assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap()); assert_eq!(0, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
assert_eq!(db.fetch_user(bot_id).await.unwrap().flags, Some(2)) assert_eq!(db.fetch_user(&bot.id).await.unwrap().flags, Some(2))
}); });
} }
} }
@@ -0,0 +1,101 @@
use revolt_result::{create_error, Result};
use crate::Database;
/* 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
pub enum Invite {
/// Invite to a specific server channel
Server {
/// Invite code
#[serde(rename = "_id")]
code: String,
/// Id of the server this invite points to
server: String,
/// Id of user who created this invite
creator: String,
/// Id of the server channel this invite points to
channel: String,
},
/// Invite to a group channel
Group {
/// Invite code
#[serde(rename = "_id")]
code: String,
/// Id of user who created this invite
creator: String,
/// Id of the group channel this invite points to
channel: String,
}, /* User {
code: String,
user: String
} */
}
);
#[allow(clippy::disallowed_methods)]
impl Invite {
/// Get the invite code for this invite
pub fn code(&'_ self) -> &'_ str {
match self {
Invite::Server { code, .. } | Invite::Group { code, .. } => code,
}
}
/// Get the ID of the user who created this invite
pub fn creator(&'_ self) -> &'_ str {
match self {
Invite::Server { creator, .. } | Invite::Group { creator, .. } => creator,
}
}
/// Create a new invite from given information
/*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> {
let code = nanoid::nanoid!(8, &ALPHABET);
let invite = match &target {
Channel::Group { id, .. } => Ok(Invite::Group {
code,
creator: creator.id.clone(),
channel: id.clone(),
}),
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
Ok(Invite::Server {
code,
creator: creator.id.clone(),
server: server.clone(),
channel: id.clone(),
})
}
_ => Err(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> {
if let Ok(invite) = db.fetch_invite(code).await {
return Ok(invite);
} else if let Ok(server) = db.fetch_server(code).await {
if server.discoverable {
if let Some(channel) = server.channels.into_iter().next() {
return Ok(Invite::Server {
code: code.to_string(),
server: server.id,
creator: server.owner,
channel,
});
}
}
}
Err(create_error!(NotFound))
}
}
@@ -0,0 +1,21 @@
use revolt_result::Result;
use crate::Invite;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannelInvites: Sync + Send {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
/// Fetch an invite by its id
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>>;
/// Delete an invite by its id
async fn delete_invite(&self, code: &str) -> Result<()>;
}
@@ -0,0 +1,50 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::Invite;
use crate::MongoDb;
use super::AbstractChannelInvites;
static COL: &str = "channel_invites";
#[async_trait]
impl AbstractChannelInvites for MongoDb {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
query!(self, insert_one, COL, &invite).map(|_| ())
}
/// Fetch an invite by the code
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
query!(self, find_one_by_id, COL, code)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
Ok(self
.col::<Invite>(COL)
.find(
doc! {
"server": server_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Delete an invite by its code
async fn delete_invite(&self, code: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, code).map(|_| ())
}
}
@@ -0,0 +1,52 @@
use revolt_result::Result;
use crate::Invite;
use crate::ReferenceDb;
use super::AbstractChannelInvites;
#[async_trait]
impl AbstractChannelInvites for ReferenceDb {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
let mut invites = self.channel_invites.lock().await;
if invites.contains_key(invite.code()) {
Err(create_database_error!("insert", "invite"))
} else {
invites.insert(invite.code().to_string(), invite.clone());
Ok(())
}
}
/// Fetch an invite by the code
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
let invites = self.channel_invites.lock().await;
invites
.get(code)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
let invites = self.channel_invites.lock().await;
Ok(invites
.values()
.filter(|invite| match invite {
Invite::Server { server, .. } => server == server_id,
_ => false,
})
.cloned()
.collect())
}
/// Delete an invite by its code
async fn delete_invite(&self, code: &str) -> Result<()> {
let mut invites = self.channel_invites.lock().await;
if invites.remove(code).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,24 @@
auto_derived!(
/// Channel Unread
pub struct ChannelUnread {
/// Composite key pointing to a user's view of a channel
#[serde(rename = "_id")]
pub id: ChannelCompositeKey,
/// Id of the last message read in this channel by a user
#[serde(skip_serializing_if = "Option::is_none")]
pub last_id: Option<String>,
/// Array of message ids that mention the user
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
}
/// Composite primary key consisting of channel and user id
#[derive(Hash)]
pub struct ChannelCompositeKey {
/// Channel Id
pub channel: String,
/// User Id
pub user: String,
}
);
@@ -0,0 +1,31 @@
use revolt_result::Result;
use crate::ChannelUnread;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannelUnreads: Sync + Send {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()>;
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()>;
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
}
@@ -0,0 +1,119 @@
use bson::Document;
use mongodb::options::UpdateOptions;
use revolt_result::Result;
use ulid::Ulid;
use crate::ChannelUnread;
use crate::MongoDb;
use super::AbstractChannelUnreads;
static COL: &str = "channel_unreads";
#[async_trait]
impl AbstractChannelUnreads for MongoDb {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$unset": {
"mentions": 1_i32
},
"$set": {
"last_id": message_id
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL)
.delete_many(
doc! {
"_id.channel": {
"$in": channel_ids
},
"_id.user": user_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", COL))?;
self.col::<Document>(COL)
.insert_many(
channel_ids
.iter()
.map(|channel_id| {
doc! {
"_id": {
"channel": channel_id,
"user": user_id
},
"last_id": &current_time
}
})
.collect::<Vec<Document>>(),
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
}
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$push": {
"mentions": {
"$each": message_ids
}
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query!(
self,
find,
COL,
doc! {
"_id.user": user_id
}
)
}
}
@@ -0,0 +1,90 @@
use revolt_result::Result;
use ulid::Ulid;
use crate::{ChannelCompositeKey, ChannelUnread, ReferenceDb};
use super::AbstractChannelUnreads;
#[async_trait]
impl AbstractChannelUnreads for ReferenceDb {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(unread) = unreads.get_mut(&key) {
unread.mentions = None;
unread.last_id.replace(message_id.to_string());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: Some(message_id.to_string()),
mentions: None,
},
);
}
Ok(())
}
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
for channel_id in channel_ids {
#[allow(clippy::disallowed_methods)]
self.acknowledge_message(channel_id, user_id, &current_time)
.await?;
}
Ok(())
}
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(unread) = unreads.get_mut(&key) {
unread.mentions.replace(message_ids.to_vec());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: None,
mentions: Some(message_ids.to_vec()),
},
);
}
Ok(())
}
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads
.values()
.filter(|unread| unread.id.user == user_id)
.cloned()
.collect())
}
}
@@ -20,6 +20,9 @@ auto_derived_partial!(
/// The channel this webhook belongs to /// The channel this webhook belongs to
pub channel_id: String, pub channel_id: String,
/// The permissions of the webhook
pub permissions: u64,
/// The private token for the webhook /// The private token for the webhook
pub token: Option<String>, pub token: Option<String>,
}, },
@@ -33,6 +36,20 @@ auto_derived!(
} }
); );
#[allow(clippy::derivable_impls)]
impl Default for Webhook {
fn default() -> Self {
Self {
id: Default::default(),
name: Default::default(),
avatar: None,
channel_id: Default::default(),
permissions: Default::default(),
token: Default::default(),
}
}
}
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
impl Webhook { impl Webhook {
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(&self, db: &Database) -> Result<()> {
@@ -117,7 +134,7 @@ mod tests {
id: webhook_id.to_string(), id: webhook_id.to_string(),
name: "Webhook Name".to_string(), name: "Webhook Name".to_string(),
channel_id: channel_id.to_string(), channel_id: channel_id.to_string(),
avatar: Some(Default::default()), avatar: None,
..Default::default() ..Default::default()
}; };
+229 -35
View File
@@ -1,12 +1,18 @@
use std::collections::HashMap; use std::collections::HashMap;
use revolt_config::config;
use revolt_models::v0::{self, MessageAuthor};
use revolt_permissions::OverrideField; use revolt_permissions::OverrideField;
use revolt_result::Result; use revolt_result::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{events::client::EventV1, Database, File, IntoDocumentPath}; use crate::{
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server,
SystemMessage, User,
};
auto_derived!( auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel { pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages /// Personal "Saved Notes" channel which allows users to save messages
SavedMessages { SavedMessages {
@@ -164,52 +170,177 @@ auto_derived!(
} }
); );
#[allow(clippy::disallowed_methods)]
impl Channel { impl Channel {
/// Create a channel /* /// Create a channel
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_channel(self).await?; db.insert_channel(self).await?;
let event = EventV1::ChannelCreate(self.clone().into());
match self {
Self::SavedMessages { user, .. } => event.private(user.clone()).await,
Self::DirectMessage { recipients, .. } | Self::Group { recipients, .. } => {
for recipient in recipients {
event.clone().private(recipient.clone()).await;
}
}
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => {
event.p(server.clone()).await;
}
}
Ok(()) Ok(())
}*/
/// Create a new server channel
pub async fn create_server_channel(
db: &Database,
server: &mut Server,
data: v0::DataCreateServerChannel,
update_server: bool,
) -> Result<Channel> {
let config = config().await;
if server.channels.len() > config.features.limits.default.server_channels {
return Err(create_error!(TooManyChannels {
max: config.features.limits.default.server_channels,
}));
};
let id = ulid::Ulid::new().to_string();
let channel = match data.channel_type {
v0::LegacyServerChannelType::Text => Channel::TextChannel {
id: id.clone(),
server: server.id.to_owned(),
name: data.name,
description: data.description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
},
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
id: id.clone(),
server: server.id.to_owned(),
name: data.name,
description: data.description,
icon: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
},
};
db.insert_channel(&channel).await?;
if update_server {
server
.update(
db,
PartialServer {
channels: Some([server.channels.clone(), [id].into()].concat()),
..Default::default()
},
vec![],
)
.await?;
EventV1::ChannelCreate(channel.clone().into())
.p(server.id.clone())
.await;
}
Ok(channel)
}
/// Create a group
pub async fn create_group(
db: &Database,
mut data: v0::DataCreateGroup,
owner_id: String,
) -> Result<Channel> {
data.users.insert(owner_id.to_string());
let config = config().await;
if data.users.len() > config.features.limits.default.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size,
}));
}
let recipients = data.users.into_iter().collect::<Vec<String>>();
let channel = Channel::Group {
id: ulid::Ulid::new().to_string(),
name: data.name,
owner: owner_id,
description: data.description,
recipients: recipients.clone(),
icon: None,
last_message_id: None,
permissions: None,
nsfw: data.nsfw.unwrap_or(false),
};
db.insert_channel(&channel).await?;
let event = EventV1::ChannelCreate(channel.clone().into());
for recipient in recipients {
event.clone().private(recipient).await;
}
Ok(channel)
} }
/// Add user to a group /// Add user to a group
pub async fn add_user_to_group( pub async fn add_user_to_group(
&mut self, &mut self,
db: &Database, db: &Database,
user_id: &str, user: &User,
_by_id: &str, by_id: &str,
) -> Result<()> { ) -> Result<()> {
if let Channel::Group { recipients, .. } = self { if let Channel::Group { recipients, .. } = self {
if recipients.contains(&String::from(user_id)) { if recipients.contains(&String::from(&user.id)) {
return Err(create_error!(AlreadyInGroup)); return Err(create_error!(AlreadyInGroup));
} }
recipients.push(String::from(user_id)); recipients.push(String::from(&user.id));
} }
match &self { match &self {
Channel::Group { id, .. } => { Channel::Group { id, .. } => {
db.add_user_to_group(id, user_id).await?; db.add_user_to_group(id, &user.id).await?;
EventV1::ChannelGroupJoin { EventV1::ChannelGroupJoin {
id: id.to_string(), id: id.to_string(),
user: user_id.to_string(), user: user.id.to_string(),
} }
.p(id.to_string()) .p(id.to_string())
.await; .await;
EventV1::ChannelCreate(self.clone().into()) SystemMessage::UserAdded {
.private(user_id.to_string()) id: user.id.to_string(),
.await; by: by_id.to_string(),
/* TODO: SystemMessage::UserAdded {
id: user.to_string(),
by: by.to_string(),
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await .await
.ok(); */ .ok();
EventV1::ChannelCreate(self.clone().into())
.private(user.id.to_string())
.await;
Ok(()) Ok(())
} }
@@ -238,7 +369,7 @@ impl Channel {
} }
} }
/// Get a reference to this channel's id /// Clone this channel's id
pub fn id(&self) -> String { pub fn id(&self) -> String {
match self { match self {
Channel::DirectMessage { id, .. } Channel::DirectMessage { id, .. }
@@ -293,7 +424,7 @@ impl Channel {
} }
/// Update channel data /// Update channel data
pub async fn update<'a>( pub async fn update(
&mut self, &mut self,
db: &Database, db: &Database,
partial: PartialChannel, partial: PartialChannel,
@@ -305,8 +436,19 @@ impl Channel {
self.apply_options(partial.clone()); self.apply_options(partial.clone());
db.update_channel(&self.id(), &partial, remove.clone()) let id = self.id().to_string();
.await?; db.update_channel(&id, &partial, remove.clone()).await?;
EventV1::ChannelUpdate {
id: id.clone(),
data: partial.into(),
clear: remove.into_iter().map(|v| v.into()).collect(),
}
.p(match self {
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
_ => id,
})
.await;
Ok(()) Ok(())
} }
@@ -444,19 +586,20 @@ impl Channel {
pub async fn remove_user_from_group( pub async fn remove_user_from_group(
&self, &self,
db: &Database, db: &Database,
user_id: &str, user: &User,
_by_id: Option<&str>, by_id: Option<&str>,
silent: bool, silent: bool,
) -> Result<()> { ) -> Result<()> {
match &self { match &self {
Channel::Group { Channel::Group {
id, id,
name,
owner, owner,
recipients, recipients,
.. ..
} => { } => {
if user_id == owner { if &user.id == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != user_id) { if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
db.update_channel( db.update_channel(
id, id,
&PartialChannel { &PartialChannel {
@@ -467,14 +610,22 @@ impl Channel {
) )
.await?; .await?;
/* TODO: SystemMessage::ChannelOwnershipChanged { SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(), from: owner.to_string(),
to: new_owner.into(), to: new_owner.to_string(),
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: name,
avatar: None,
},
self,
false,
)
.await .await
.ok(); */ .ok();
} else { } else {
db.delete_channel(self).await?; db.delete_channel(self).await?;
return Ok(()); return Ok(());
@@ -483,26 +634,34 @@ impl Channel {
EventV1::ChannelGroupLeave { EventV1::ChannelGroupLeave {
id: id.to_string(), id: id.to_string(),
user: user_id.to_string(), user: user.id.to_string(),
} }
.p(id.to_string()) .p(id.to_string())
.await; .await;
if !silent { if !silent {
/* TODO: if let Some(_by) = by_id { if let Some(by) = by_id {
SystemMessage::UserRemove { SystemMessage::UserRemove {
id: user_id.to_string(), id: user.id.to_string(),
by: by.to_string(), by: by.to_string(),
} }
} else { } else {
SystemMessage::UserLeft { SystemMessage::UserLeft {
id: user_id.to_string(), id: user.id.to_string(),
} }
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await .await
.ok(); */ .ok();
} }
Ok(()) Ok(())
@@ -514,6 +673,8 @@ impl Channel {
/// Delete a channel /// Delete a channel
pub async fn delete(&self, db: &Database) -> Result<()> { pub async fn delete(&self, db: &Database) -> Result<()> {
let id = self.id().to_string();
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
db.delete_channel(self).await db.delete_channel(self).await
} }
} }
@@ -527,3 +688,36 @@ impl IntoDocumentPath for FieldsChannel {
}) })
} }
} }
#[cfg(test)]
mod tests {
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[async_std::test]
async fn permissions_text_channel() {
database_test!(|db| async move {
fixture!(db, "server_with_roles",
owner user 0
moderator user 1
user user 2
channel channel 3);
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &moderator).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &user).channel(&channel);
assert!(!calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
});
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,87 @@
use std::collections::HashSet;
use std::str::FromStr;
use once_cell::sync::Lazy;
use revolt_result::Result;
use ulid::Ulid;
use crate::events::client::EventV1;
use crate::Database;
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
include_str!("unicode_emoji.txt")
.split('\n')
.map(|x| x.into())
.collect()
});
auto_derived!(
/// Emoji
pub struct Emoji {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// What owns this emoji
pub parent: EmojiParent,
/// Uploader user id
pub creator_id: String,
/// Emoji name
pub name: String,
/// Whether the emoji is animated
#[serde(skip_serializing_if = "crate::if_false", default)]
pub animated: bool,
/// Whether the emoji is marked as nsfw
#[serde(skip_serializing_if = "crate::if_false", default)]
pub nsfw: bool,
}
/// Parent Id of the emoji
#[serde(tag = "type")]
pub enum EmojiParent {
Server { id: String },
Detached,
}
);
#[allow(clippy::disallowed_methods)]
impl Emoji {
/// Get parent id
fn parent(&self) -> &str {
match &self.parent {
EmojiParent::Server { id } => id,
EmojiParent::Detached => "",
}
}
/// Create an emoji
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_emoji(self).await?;
EventV1::EmojiCreate(self.clone().into())
.p(self.parent().to_string())
.await;
Ok(())
}
/// Delete an emoji
pub async fn delete(self, db: &Database) -> Result<()> {
EventV1::EmojiDelete {
id: self.id.to_string(),
}
.p(self.parent().to_string())
.await;
db.detach_emoji(&self).await
}
/// Check whether we can use a given emoji
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
if Ulid::from_str(emoji).is_ok() {
db.fetch_emoji(emoji).await?;
Ok(true)
} else {
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
}
}
}
@@ -0,0 +1,24 @@
use revolt_result::Result;
use crate::Emoji;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractEmojis: Sync + Send {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji>;
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>>;
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
}
@@ -0,0 +1,70 @@
use bson::Document;
use revolt_result::Result;
use crate::Emoji;
use crate::MongoDb;
use super::AbstractEmojis;
static COL: &str = "emojis";
#[async_trait]
impl AbstractEmojis for MongoDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
query!(self, insert_one, COL, &emoji).map(|_| ())
}
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
query!(
self,
find_one,
COL,
doc! {
"parent.id": parent_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
query!(
self,
find,
COL,
doc! {
"parent.id": {
"$in": parent_ids
}
}
)
}
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": &emoji.id
},
doc! {
"$set": {
"parent": {
"type": "Detached"
}
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
}
@@ -0,0 +1,67 @@
use revolt_result::Result;
use crate::Emoji;
use crate::EmojiParent;
use crate::ReferenceDb;
use super::AbstractEmojis;
#[async_trait]
impl AbstractEmojis for ReferenceDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
let mut emojis = self.emojis.lock().await;
if emojis.contains_key(&emoji.id) {
Err(create_database_error!("insert", "emoji"))
} else {
emojis.insert(emoji.id.to_string(), emoji.clone());
Ok(())
}
}
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
let emojis = self.emojis.lock().await;
emojis
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
let emojis = self.emojis.lock().await;
Ok(emojis
.values()
.filter(|emoji| match &emoji.parent {
EmojiParent::Server { id } => id == parent_id,
_ => false,
})
.cloned()
.collect())
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
let emojis = self.emojis.lock().await;
Ok(emojis
.values()
.filter(|emoji| match &emoji.parent {
EmojiParent::Server { id } => parent_ids.contains(id),
_ => false,
})
.cloned()
.collect())
}
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
let mut emojis = self.emojis.lock().await;
if let Some(bot) = emojis.get_mut(&emoji.id) {
bot.parent = EmojiParent::Detached;
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
File diff suppressed because it is too large Load Diff
@@ -6,7 +6,7 @@ use crate::MongoDb;
use super::AbstractAttachments; use super::AbstractAttachments;
static COL: &str = "bots"; static COL: &str = "attachments";
#[async_trait] #[async_trait]
impl AbstractAttachments for MongoDb { impl AbstractAttachments for MongoDb {
@@ -22,15 +22,25 @@ impl AbstractAttachments for ReferenceDb {
async fn find_and_use_attachment( async fn find_and_use_attachment(
&self, &self,
id: &str, id: &str,
_tag: &str, tag: &str,
_parent_type: &str, parent_type: &str,
_parent_id: &str, parent_id: &str,
) -> Result<File> { ) -> Result<File> {
let mut files = self.files.lock().await; let mut files = self.files.lock().await;
if let Some(file) = files.get_mut(id) { if let Some(file) = files.get_mut(id) {
// TODO: check tag if file.tag == tag {
// TODO: set parent ID match parent_type {
Ok(file.clone()) "message" => file.message_id = Some(parent_id.to_owned()),
"user" => file.user_id = Some(parent_id.to_owned()),
"object" => file.object_id = Some(parent_id.to_owned()),
"server" => file.server_id = Some(parent_id.to_owned()),
_ => unreachable!(),
}
Ok(file.clone())
} else {
Err(create_error!(NotFound))
}
} else { } else {
Err(create_error!(NotFound)) Err(create_error!(NotFound))
} }
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,572 @@
use std::collections::HashSet;
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,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
use ulid::Ulid;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::idempotency::IdempotencyKey,
Channel, Database, Emoji, File,
};
auto_derived_partial!(
/// Message
pub struct Message {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Unique value generated by client sending this message
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
/// Id of the channel this message was sent in
pub channel: String,
/// Id of the user or webhook that sent this message
pub author: String,
/// The webhook that sent this message
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook: Option<MessageWebhook>,
/// Message content
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// System message
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemMessage>,
/// Array of attachments
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<File>>,
/// Time at which this message was last edited
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<Timestamp>,
/// Attached embeds to this message
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
/// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
/// Hashmap of emoji IDs to array of user IDs
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub reactions: IndexMap<String, IndexSet<String>>,
/// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,
},
"PartialMessage"
);
auto_derived!(
/// System Event
#[serde(tag = "type")]
pub enum SystemMessage {
#[serde(rename = "text")]
Text { content: String },
#[serde(rename = "user_added")]
UserAdded { id: String, by: String },
#[serde(rename = "user_remove")]
UserRemove { id: String, by: String },
#[serde(rename = "user_joined")]
UserJoined { id: String },
#[serde(rename = "user_left")]
UserLeft { id: String },
#[serde(rename = "user_kicked")]
UserKicked { id: String },
#[serde(rename = "user_banned")]
UserBanned { id: String },
#[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String },
#[serde(rename = "channel_description_changed")]
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
ChannelOwnershipChanged { from: String, to: String },
}
/// Name and / or avatar override information
pub struct Masquerade {
/// Replace the display name shown on this message
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Replace the avatar shown on this message (URL to image file)
#[serde(skip_serializing_if = "Option::is_none")]
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")]
pub colour: Option<String>,
}
/// Information to guide interactions on this message
#[derive(Default)]
pub struct Interactions {
/// Reactions which should always appear and be distinct
#[serde(skip_serializing_if = "Option::is_none", default)]
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
#[serde(skip_serializing_if = "crate::if_false", default)]
pub restrict_reactions: bool,
}
/// Appended Information
pub struct AppendMessage {
/// Additional embeds to include in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
}
/// Message Time Period
///
/// Filter and sort messages by time
#[serde(untagged)]
pub enum MessageTimePeriod {
Relative {
/// 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.
nearby: String,
},
Absolute {
/// Message id before which messages should be fetched
before: Option<String>,
/// Message id after which messages should be fetched
after: Option<String>,
/// Message sort direction
sort: Option<MessageSort>,
},
}
/// Message Filter
pub struct MessageFilter {
/// Parent channel ID
pub channel: Option<String>,
/// Message author ID
pub author: Option<String>,
/// Search query
pub query: Option<String>,
}
/// Message Query
pub struct MessageQuery {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
pub limit: Option<i64>,
/// Filter to apply
#[serde(flatten)]
pub filter: MessageFilter,
/// Time period to fetch
#[serde(flatten)]
pub time_period: MessageTimePeriod,
}
);
#[allow(clippy::derivable_impls)]
impl Default for Message {
fn default() -> Self {
Self {
id: Default::default(),
nonce: None,
channel: Default::default(),
author: Default::default(),
webhook: None,
content: None,
system: None,
attachments: None,
edited: None,
embeds: None,
mentions: None,
replies: None,
reactions: Default::default(),
interactions: Default::default(),
masquerade: None,
}
}
}
#[allow(clippy::disallowed_methods)]
impl Message {
/// Create message from API data
pub async fn create_from_api(
db: &Database,
channel: Channel,
data: DataMessageSend,
author: MessageAuthor<'_>,
mut idempotency: IdempotencyKey,
generate_embeds: bool,
) -> Result<Message> {
let config = config().await;
Message::validate_sum(
&data.content,
data.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
)?;
idempotency
.consume_nonce(data.nonce)
.await
.map_err(|_| create_error!(InvalidOperation))?;
// Check the message is not empty
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
{
return Err(create_error!(EmptyMessage));
}
// Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
let disallowed = if let Some(list) = &interactions.reactions {
list.is_empty()
} else {
true
};
if disallowed {
return Err(create_error!(InvalidProperty));
}
}
}
let (author_id, webhook) = match &author {
MessageAuthor::User(user) => (user.id.clone(), None),
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
MessageAuthor::System { .. } => ("00000000000000000000000000".to_string(), None),
};
// Start constructing the message
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id.clone(),
channel: channel.id(),
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
interactions: data
.interactions
.map(|interactions| interactions.into())
.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
..Default::default()
};
// Parse mentions in message.
let mut mentions = HashSet::new();
if let Some(content) = &data.content {
for capture in RE_MENTION.captures_iter(content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
}
}
// Verify replies are valid.
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > config.features.limits.default.message_replies {
return Err(create_error!(TooManyReplies {
max: config.features.limits.default.message_replies,
}));
}
for ReplyIntent { id, mention } in entries {
let message = db.fetch_message(&id).await?;
if mention {
mentions.insert(message.author.to_owned());
}
replies.insert(message.id);
}
}
if !mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect());
}
if !replies.is_empty() {
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
// Add attachments to message.
let mut attachments = vec![];
if data
.attachments
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
{
return Err(create_error!(TooManyAttachments {
max: config.features.limits.default.message_attachments,
}));
}
if data
.embeds
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
{
return Err(create_error!(TooManyEmbeds {
max: config.features.limits.default.message_embeds,
}));
}
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
attachments.push(
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
.await?,
);
}
if !attachments.is_empty() {
message.attachments.replace(attachments);
}
// Process included embeds.
for sendable_embed in data.embeds.unwrap_or_default() {
message.attach_sendable_embed(db, sendable_embed).await?;
}
// Set content
message.content = data.content;
// Pass-through nonce value for clients
message.nonce = Some(idempotency.into_key());
// Send the message
message.send(db, author, &channel, generate_embeds).await?;
Ok(message)
}
/// Send a message without any notifications
pub async fn send_without_notifications(
&mut self,
db: &Database,
is_dm: bool,
generate_embeds: bool,
) -> Result<()> {
db.insert_message(self).await?;
// Fan out events
EventV1::Message(self.clone().into())
.p(self.channel.to_string())
.await;
// Update last_message_id
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
// Add mentions for affected users
if let Some(mentions) = &self.mentions {
for user in mentions {
tasks::ack::queue(
self.channel.to_string(),
user.to_string(),
AckEvent::AddMention {
ids: vec![self.id.to_string()],
},
)
.await;
}
}
// Generate embeds
if generate_embeds {
if let Some(content) = &self.content {
tasks::process_embeds::queue(
self.channel.to_string(),
self.id.to_string(),
content.clone(),
)
.await;
}
}
Ok(())
}
/// Send a message
pub async fn send(
&mut self,
db: &Database,
author: MessageAuthor<'_>,
channel: &Channel,
generate_embeds: bool,
) -> Result<()> {
self.send_without_notifications(
db,
matches!(channel, Channel::DirectMessage { .. }),
generate_embeds,
)
.await?;
// Push out Web Push notifications
crate::tasks::web_push::queue(
{
match channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => recipients.clone(),
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
_ => vec![],
}
},
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
)
.await;
Ok(())
}
/// Append content to message
pub async fn append(
db: &Database,
id: String,
channel: String,
append: AppendMessage,
) -> Result<()> {
db.append_message(&id, &append).await?;
EventV1::MessageAppend {
id,
channel: channel.to_string(),
append: append.into(),
}
.p(channel)
.await;
Ok(())
}
/// Convert sendable embed to text embed and attach to message
pub async fn attach_sendable_embed(
&mut self,
db: &Database,
embed: v0::SendableEmbed,
) -> Result<()> {
let media: Option<v0::File> = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
.await?
.into(),
)
} else {
None
};
let embed = v0::Embed::Text(v0::Text {
icon_url: embed.icon_url,
url: embed.url,
title: embed.title,
description: embed.description,
media,
colour: embed.colour,
});
if let Some(embeds) = &mut self.embeds {
embeds.push(embed);
} else {
self.embeds = Some(vec![embed]);
}
Ok(())
}
/// Validate the sum of content of a message is under threshold
pub fn validate_sum(
content: &Option<String>,
embeds: &[SendableEmbed],
max_length: usize,
) -> Result<()> {
let mut running_total = 0;
if let Some(content) = content {
running_total += content.len();
}
for embed in embeds {
if let Some(desc) = &embed.description {
running_total += desc.len();
}
}
if running_total <= max_length {
Ok(())
} else {
Err(create_error!(PayloadTooLarge))
}
}
}
impl SystemMessage {
pub fn into_message(self, channel: String) -> Message {
Message {
id: Ulid::new().to_string(),
channel,
author: "00000000000000000000000000".to_string(),
system: Some(self),
..Default::default()
}
}
}
impl Interactions {
/// Validate interactions info is correct
pub async fn validate(&self, db: &Database, permissions: &PermissionValue) -> Result<()> {
let config = config().await;
if let Some(reactions) = &self.reactions {
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
if reactions.len() > config.features.limits.default.message_reactions {
return Err(create_error!(InvalidOperation));
}
for reaction in reactions {
if !Emoji::can_use(db, reaction).await? {
return Err(create_error!(InvalidOperation));
}
}
}
Ok(())
}
/// Check if we can use a given emoji to react
pub fn can_use(&self, emoji: &str) -> bool {
if self.restrict_reactions {
if let Some(reactions) = &self.reactions {
reactions.contains(emoji)
} else {
false
}
} else {
true
}
}
/// Check if default initialisation of fields
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}
@@ -0,0 +1,39 @@
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractMessages: Sync + Send {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()>;
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message>;
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()>;
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>;
}
@@ -0,0 +1,280 @@
use bson::{to_bson, Document};
use futures::try_join;
use mongodb::options::FindOptions;
use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage};
use super::AbstractMessages;
static COL: &str = "messages";
#[async_trait]
impl AbstractMessages for MongoDb {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()> {
query!(self, insert_one, COL, &message).map(|_| ())
}
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
let mut filter = doc! {};
// 1. Apply message filters
if let Some(channel) = query.filter.channel {
filter.insert("channel", channel);
}
if let Some(author) = query.filter.author {
filter.insert("author", author);
}
let is_search_query = if let Some(query) = query.filter.query {
filter.insert(
"$text",
doc! {
"$search": query
},
);
true
} else {
false
};
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
// 3. Apply message time period
match query.time_period {
MessageTimePeriod::Relative { nearby } => {
// 3.1. Prepare filters
let mut older_message_filter = filter.clone();
let mut newer_message_filter = filter;
older_message_filter.insert(
"_id",
doc! {
"$lt": &nearby
},
);
newer_message_filter.insert(
"_id",
doc! {
"$gte": &nearby
},
);
// 3.2. Execute in both directions
let (a, b) = try_join!(
self.find_with_options::<_, Message>(
COL,
newer_message_filter,
FindOptions::builder()
.limit(limit / 2 + 1)
.sort(doc! {
"_id": 1_i32
})
.build(),
),
self.find_with_options::<_, Message>(
COL,
older_message_filter,
FindOptions::builder()
.limit(limit / 2)
.sort(doc! {
"_id": -1_i32
})
.build(),
)
)
.map_err(|_| create_database_error!("find", COL))?;
Ok([a, b].concat())
}
MessageTimePeriod::Absolute {
before,
after,
sort,
} => {
// 3.1. Apply message ID filter
if let Some(doc) = match (before, after) {
(Some(before), Some(after)) => Some(doc! {
"$lt": before,
"$gt": after
}),
(Some(before), _) => Some(doc! {
"$lt": before
}),
(_, Some(after)) => Some(doc! {
"$gt": after
}),
_ => None,
} {
filter.insert("_id", doc);
}
// 3.2. Execute with given message sort
self.find_with_options(
COL,
filter,
FindOptions::builder()
.limit(limit)
.sort(match sort.unwrap_or(MessageSort::Latest) {
// Sort by relevance, fallback to latest
MessageSort::Relevance => {
if is_search_query {
doc! {
"score": {
"$meta": "textScore"
}
}
} else {
doc! {
"_id": -1_i32
}
}
}
// Sort by latest first
MessageSort::Latest => doc! {
"_id": -1_i32
},
// Sort by oldest first
MessageSort::Oldest => doc! {
"_id": 1_i32
},
})
.build(),
)
.await
.map_err(|_| create_database_error!("find", COL))
}
}
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
}
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
let mut query = doc! {};
if let Some(embeds) = &append.embeds {
if !embeds.is_empty() {
query.insert(
"$push",
doc! {
"embeds": {
"$each": to_bson(embeds)
.map_err(|_| create_database_error!("to_bson", "embeds"))?
}
},
);
}
}
if query.is_empty() {
return Ok(());
}
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
query,
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$addToSet": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$pull": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$unset": {
format!("reactions.{emoji}"): 1
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.delete_many(
doc! {
"channel": channel,
"_id": {
"$in": ids
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL))
}
}
@@ -0,0 +1,272 @@
use indexmap::IndexSet;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
use super::AbstractMessages;
#[async_trait]
impl AbstractMessages for ReferenceDb {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()> {
let mut messages = self.messages.lock().await;
if messages.contains_key(&message.id) {
Err(create_database_error!("insert", "message"))
} else {
messages.insert(message.id.to_string(), message.clone());
Ok(())
}
}
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message> {
let messages = self.messages.lock().await;
messages
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
let messages = self.messages.lock().await;
let matched_messages = messages
.values()
.filter(|message| {
if let Some(channel) = &query.filter.channel {
if &message.channel != channel {
return false;
}
}
if let Some(author) = &query.filter.author {
if &message.author != author {
return false;
}
}
if let Some(query) = &query.filter.query {
if let Some(content) = &message.content {
if !content.to_lowercase().contains(query) {
return false;
}
} else {
return false;
}
}
true
})
.cloned()
.collect();
// FIXME: sorting, etc (will be required for tests)
Ok(matched_messages)
/*
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
// 3. Apply message time period
match query.time_period {
MessageTimePeriod::Relative { nearby } => {
// 3.1. Prepare filters
let mut older_message_filter = filter.clone();
let mut newer_message_filter = filter;
older_message_filter.insert(
"_id",
doc! {
"$lt": &nearby
},
);
newer_message_filter.insert(
"_id",
doc! {
"$gte": &nearby
},
);
// 3.2. Execute in both directions
let (a, b) = try_join!(
self.find_with_options::<_, Message>(
COL,
newer_message_filter,
FindOptions::builder()
.limit(limit / 2 + 1)
.sort(doc! {
"_id": 1_i32
})
.build(),
),
self.find_with_options::<_, Message>(
COL,
older_message_filter,
FindOptions::builder()
.limit(limit / 2)
.sort(doc! {
"_id": -1_i32
})
.build(),
)
)
.map_err(|_| create_database_error!("find", COL))?;
Ok([a, b].concat())
}
MessageTimePeriod::Absolute {
before,
after,
sort,
} => {
// 3.1. Apply message ID filter
if let Some(doc) = match (before, after) {
(Some(before), Some(after)) => Some(doc! {
"$lt": before,
"$gt": after
}),
(Some(before), _) => Some(doc! {
"$lt": before
}),
(_, Some(after)) => Some(doc! {
"$gt": after
}),
_ => None,
} {
filter.insert("_id", doc);
}
// 3.2. Execute with given message sort
self.find_with_options(
COL,
filter,
FindOptions::builder()
.limit(limit)
.sort(match sort.unwrap_or(MessageSort::Latest) {
// Sort by relevance, fallback to latest
MessageSort::Relevance => {
if is_search_query {
doc! {
"score": {
"$meta": "textScore"
}
}
} else {
doc! {
"_id": -1_i32
}
}
}
// Sort by latest first
MessageSort::Latest => doc! {
"_id": -1_i32
},
// Sort by oldest first
MessageSort::Oldest => doc! {
"_id": 1_i32
},
})
.build(),
)
.await
.map_err(|_| create_database_error!("find", COL))
}
}*/
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message_data) = messages.get_mut(id) {
message_data.apply_options(message.to_owned());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message_data) = messages.get_mut(id) {
if let Some(embeds) = &append.embeds {
if !embeds.is_empty() {
if let Some(embeds_data) = &mut message_data.embeds {
embeds_data.extend(embeds.clone());
} else {
message_data.embeds = Some(embeds.clone());
}
}
}
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) {
if let Some(users) = message.reactions.get_mut(emoji) {
users.insert(user.to_string());
} else {
message
.reactions
.insert(emoji.to_string(), IndexSet::from([user.to_string()]));
}
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) {
if let Some(users) = message.reactions.get_mut(emoji) {
users.remove(&user.to_string());
}
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) {
message.reactions.remove(emoji);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if messages.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.messages
.lock()
.await
.retain(|id, message| message.channel != channel && !ids.contains(id));
Ok(())
}
}
+18 -3
View File
@@ -1,9 +1,14 @@
mod admin_migrations; mod admin_migrations;
mod bots; mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks; mod channel_webhooks;
mod channels; mod channels;
mod emojis;
mod files; mod files;
mod safety_strikes; mod messages;
mod ratelimit_events;
mod server_bans;
mod server_members; mod server_members;
mod servers; mod servers;
mod user_settings; mod user_settings;
@@ -11,10 +16,15 @@ mod users;
pub use admin_migrations::*; pub use admin_migrations::*;
pub use bots::*; pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
pub use channel_webhooks::*; pub use channel_webhooks::*;
pub use channels::*; pub use channels::*;
pub use emojis::*;
pub use files::*; pub use files::*;
pub use safety_strikes::*; pub use messages::*;
pub use ratelimit_events::*;
pub use server_bans::*;
pub use server_members::*; pub use server_members::*;
pub use servers::*; pub use servers::*;
pub use user_settings::*; pub use user_settings::*;
@@ -28,9 +38,14 @@ pub trait AbstractDatabase:
+ admin_migrations::AbstractMigrations + admin_migrations::AbstractMigrations
+ bots::AbstractBots + bots::AbstractBots
+ channels::AbstractChannels + channels::AbstractChannels
+ channel_invites::AbstractChannelInvites
+ channel_unreads::AbstractChannelUnreads
+ channel_webhooks::AbstractWebhooks + channel_webhooks::AbstractWebhooks
+ emojis::AbstractEmojis
+ files::AbstractAttachments + files::AbstractAttachments
+ safety_strikes::AbstractAccountStrikes + messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents
+ server_bans::AbstractServerBans
+ server_members::AbstractServerMembers + server_members::AbstractServerMembers
+ servers::AbstractServers + servers::AbstractServers
+ user_settings::AbstractUserSettings + user_settings::AbstractUserSettings
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,47 @@
use std::fmt;
use revolt_result::Result;
use ulid::Ulid;
use crate::Database;
auto_derived!(
/// Ratelimit Event
pub struct RatelimitEvent {
/// Id
#[serde(rename = "_id")]
pub id: String,
/// Relevant Object Id
pub target_id: String,
/// Type of event
pub event_type: RatelimitEventType,
}
/// Event type
pub enum RatelimitEventType {
DiscriminatorChange,
}
);
impl fmt::Display for RatelimitEventType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
#[allow(clippy::disallowed_methods)]
impl RatelimitEvent {
/// Create ratelimit event
pub async fn create(
db: &Database,
target_id: String,
event_type: RatelimitEventType,
) -> Result<()> {
db.insert_ratelimit_event(&RatelimitEvent {
id: Ulid::new().to_string(),
target_id,
event_type,
})
.await
}
}
@@ -0,0 +1,20 @@
use std::time::Duration;
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractRatelimitEvents: Sync + Send {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()>;
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool>;
}
@@ -0,0 +1,40 @@
use std::time::{Duration, SystemTime};
use super::AbstractRatelimitEvents;
use crate::{MongoDb, RatelimitEvent, RatelimitEventType};
use revolt_result::Result;
use ulid::Ulid;
static COL: &str = "ratelimit_events";
#[async_trait]
impl AbstractRatelimitEvents for MongoDb {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
query!(self, insert_one, COL, &event).map(|_| ())
}
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool> {
self.col::<RatelimitEvent>(COL)
.count_documents(
doc! {
"_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
"target_id": target_id,
"event_type": event_type.to_string()
},
None,
)
.await
.map(|c| c as usize >= count)
.map_err(|_| create_database_error!("count_documents", COL))
}
}
@@ -0,0 +1,46 @@
use std::cmp::Ordering;
use std::time::Duration;
use std::time::SystemTime;
use super::AbstractRatelimitEvents;
use crate::RatelimitEvent;
use crate::RatelimitEventType;
use crate::ReferenceDb;
use revolt_result::Result;
use ulid::Ulid;
#[async_trait]
impl AbstractRatelimitEvents for ReferenceDb {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
let mut ratelimit_events = self.ratelimit_events.lock().await;
if ratelimit_events.contains_key(&event.id) {
Err(create_database_error!("insert", "message"))
} else {
ratelimit_events.insert(event.id.to_string(), event.clone());
Ok(())
}
}
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool> {
let ratelimit_events = self.ratelimit_events.lock().await;
let gte_cmp_id = Ulid::from_datetime(SystemTime::now() - period).to_string();
Ok(ratelimit_events
.iter()
.filter(|(id, event)| {
id.cmp(&&gte_cmp_id) == Ordering::Greater
&& event.target_id == target_id
&& event.event_type == event_type
})
.count()
>= count)
}
}
@@ -1,124 +0,0 @@
use revolt_result::Result;
use crate::Database;
auto_derived_partial!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[serde(rename = "_id")]
pub id: String,
/// Id of reported user
pub user_id: String,
/// Id of moderator
pub moderator_id: String,
/// Attached reason
pub reason: String,
},
"PartialAccountStrike"
);
#[allow(clippy::disallowed_methods)]
impl AccountStrike {
pub async fn create(
db: &Database,
user_id: String,
reason: String,
moderator_id: String,
) -> Result<AccountStrike> {
let strike = AccountStrike {
id: ulid::Ulid::new().to_string(),
user_id,
moderator_id,
reason,
};
db.insert_account_strike(&strike).await?;
Ok(strike)
}
/// Update this strike
pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> {
db.update_account_strike(&self.id, &partial).await?;
self.apply_options(partial);
Ok(())
}
/// Delete this strike
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_account_strike(&self.id).await
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{AccountStrike, PartialAccountStrike};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let user_id = "user";
let strike = AccountStrike::create(
&db,
user_id.to_string(),
"reason 1".to_string(),
"moderator_id".to_string(),
)
.await
.unwrap();
let mut updated_strike = strike.clone();
updated_strike
.update(
&db,
PartialAccountStrike {
reason: Some("new reason".to_string()),
..Default::default()
},
)
.await
.unwrap();
let strike2 = AccountStrike::create(
&db,
user_id.to_string(),
"reason 2".to_string(),
"moderator_id".to_string(),
)
.await
.unwrap();
let strikes = db.fetch_account_strikes_by_user(user_id).await.unwrap();
let ids = strikes
.iter()
.cloned()
.map(|strike| strike.id)
.collect::<HashSet<String>>();
assert!(ids.contains(&strike.id));
assert!(ids.contains(&strike2.id));
let fetched_strike = strikes
.into_iter()
.find(|entry| entry.id == strike.id)
.unwrap();
assert_eq!(fetched_strike, updated_strike);
assert_ne!(fetched_strike, strike);
strike.delete(&db).await.unwrap();
assert_eq!(
1,
db.fetch_account_strikes_by_user(user_id)
.await
.unwrap()
.len()
)
});
}
}
@@ -1,24 +0,0 @@
use revolt_result::Result;
use crate::{AccountStrike, PartialAccountStrike};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAccountStrikes: Sync + Send {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()>;
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike>;
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>>;
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()>;
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()>;
}
@@ -1,55 +0,0 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::MongoDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
static COL: &str = "safety_strikes";
#[async_trait]
impl AbstractAccountStrikes for MongoDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
query!(self, insert_one, COL, &strike).map(|_| ())
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
Ok(self
.col::<AccountStrike>(COL)
.find(
doc! {
"user_id": user_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
@@ -1,60 +0,0 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
#[async_trait]
impl AbstractAccountStrikes for ReferenceDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.contains_key(&strike.id) {
Err(create_database_error!("insert", "strike"))
} else {
strikes.insert(strike.id.to_string(), strike.clone());
Ok(())
}
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
let strikes = self.account_strikes.lock().await;
strikes
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
let strikes = self.account_strikes.lock().await;
Ok(strikes
.values()
.filter(|strike| strike.user_id == user_id)
.cloned()
.collect())
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if let Some(strike) = strikes.get_mut(id) {
strike.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,12 @@
use crate::MemberCompositeKey;
auto_derived!(
/// Server Ban
pub struct ServerBan {
/// Unique member id
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Reason for ban creation
pub reason: Option<String>,
}
);
@@ -0,0 +1,21 @@
use revolt_result::Result;
use crate::{MemberCompositeKey, ServerBan};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractServerBans: Sync + Send {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()>;
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan>;
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>>;
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()>;
}
@@ -0,0 +1,56 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::{MemberCompositeKey, ServerBan};
use super::AbstractServerBans;
static COL: &str = "server_bans";
#[async_trait]
impl AbstractServerBans for MongoDb {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
query!(self, insert_one, COL, &ban).map(|_| ())
}
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
query!(
self,
find_one,
COL,
doc! {
"_id.server": server_id,
"_id.user": user_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
query!(
self,
find,
COL,
doc! {
"_id.server": server_id
}
)
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
query!(
self,
delete_one,
COL,
doc! {
"_id.server": &id.server,
"_id.user": &id.user
}
)
.map(|_| ())
}
}
@@ -0,0 +1,52 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{MemberCompositeKey, ServerBan};
use super::AbstractServerBans;
#[async_trait]
impl AbstractServerBans for ReferenceDb {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
let mut server_bans = self.server_bans.lock().await;
if server_bans.contains_key(&ban.id) {
Err(create_database_error!("insert", "ban"))
} else {
server_bans.insert(ban.id.clone(), ban.clone());
Ok(())
}
}
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
let server_bans = self.server_bans.lock().await;
server_bans
.get(&MemberCompositeKey {
server: server_id.to_string(),
user: user_id.to_string(),
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
let server_bans = self.server_bans.lock().await;
Ok(server_bans
.values()
.filter(|member| member.id.server == server_id)
.cloned()
.collect())
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_bans = self.server_bans.lock().await;
if server_bans.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -1,7 +1,11 @@
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
use revolt_result::Result; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use crate::{Database, File, Server}; use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
Server, SystemMessage, User,
};
auto_derived_partial!( auto_derived_partial!(
/// Server Member /// Server Member
@@ -11,8 +15,7 @@ auto_derived_partial!(
pub id: MemberCompositeKey, pub id: MemberCompositeKey,
/// Time at which this user joined the server /// Time at which this user joined the server
#[serde(skip_serializing_if = "Option::is_none")] pub joined_at: Timestamp,
pub joined_at: Option<Timestamp>,
/// Member's nickname /// Member's nickname
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -57,7 +60,101 @@ auto_derived!(
} }
); );
impl Default for Member {
fn default() -> Self {
Self {
id: Default::default(),
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
}
}
}
#[allow(clippy::disallowed_methods)]
impl Member { impl Member {
/// Create a new member in a server
pub async fn create(
db: &Database,
server: &Server,
user: &User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
return Err(create_error!(Banned));
}
if db.fetch_member(&server.id, &user.id).await.is_ok() {
return Err(create_error!(AlreadyInServer));
}
let member = Member {
id: MemberCompositeKey {
server: server.id.to_string(),
user: user.id.to_string(),
},
..Default::default()
};
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let query = DatabasePermissionQuery::new(db, user).server(server);
let existing_channels = db.fetch_channels(&server.channels).await?;
for channel in existing_channels {
let mut channel_query = query.clone().channel(&channel);
if calculate_channel_permissions(&mut channel_query)
.await
.has_channel_permission(ChannelPermission::ViewChannel)
{
channels.push(channel);
}
}
}
EventV1::ServerMemberJoin {
id: server.id.clone(),
user: user.id.clone(),
}
.p(server.id.clone())
.await;
EventV1::ServerCreate {
id: server.id.clone(),
server: server.clone().into(),
channels: channels
.clone()
.into_iter()
.map(|channel| channel.into())
.collect(),
}
.private(user.id.clone())
.await;
if let Some(id) = server
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.send_without_notifications(db, false, false)
.await
.ok();
}
Ok(channels)
}
/// Update member data /// Update member data
pub async fn update<'a>( pub async fn update<'a>(
&mut self, &mut self,
@@ -73,13 +170,13 @@ impl Member {
db.update_member(&self.id, &partial, remove.clone()).await?; db.update_member(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::ServerMemberUpdate { EventV1::ServerMemberUpdate {
id: self.id.clone(), id: self.id.clone().into(),
data: partial, data: partial.into(),
clear: remove, clear: remove.into_iter().map(|field| field.into()).collect(),
} }
.p(self.id.server.clone()) .p(self.id.server.clone())
.await; */ .await;
Ok(()) Ok(())
} }
@@ -1,10 +1,11 @@
use std::collections::{HashMap, HashSet}; 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 revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use crate::{Database, File}; use crate::{events::client::EventV1, Channel, Database, File, User};
auto_derived_partial!( auto_derived_partial!(
/// Server /// Server
@@ -22,7 +23,7 @@ auto_derived_partial!(
pub description: Option<String>, pub description: Option<String>,
/// Channels within this server /// Channels within this server
// ! FIXME: this may be redundant // TODO: investigate if this is redundant and can be removed
pub channels: Vec<String>, pub channels: Vec<String>,
/// Categories for this server /// Categories for this server
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -128,10 +129,54 @@ auto_derived!(
} }
); );
#[allow(clippy::disallowed_methods)]
impl Server { impl Server {
/// Create a server /// Create a server
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(
db.insert_server(self).await 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![]
};
db.insert_server(&server).await?;
Ok((server, channels))
} }
/// Update server data /// Update server data
@@ -149,24 +194,24 @@ impl Server {
db.update_server(&self.id, &partial, remove.clone()).await?; db.update_server(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::ServerUpdate { EventV1::ServerUpdate {
id: self.id.clone(), id: self.id.clone(),
data: partial, data: partial.into(),
clear: remove, clear: remove.into_iter().map(|v| v.into()).collect(),
} }
.p(self.id.clone()) .p(self.id.clone())
.await; */ .await;
Ok(()) Ok(())
} }
/// Delete a server /// Delete a server
pub async fn delete(self, db: &Database) -> Result<()> { pub async fn delete(self, db: &Database) -> Result<()> {
/* // TODO: EventV1::ServerDelete { EventV1::ServerDelete {
id: self.id.clone(), id: self.id.clone(),
} }
.p(self.id.clone()) .p(self.id.clone())
.await; */ .await;
db.delete_server(&self.id).await db.delete_server(&self.id).await
} }
@@ -208,80 +253,7 @@ impl Server {
} }
} }
/* /// Create a new member in a server /*
pub async fn create_member(
&self,
db: &Database,
user: User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
return Err(Error::Banned);
}
let member = Member {
id: MemberCompositeKey {
server: self.id.clone(),
user: user.id.clone(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
};
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let perm = perms(&user).server(self).member(&member);
let existing_channels = db.fetch_channels(&self.channels).await?;
for channel in existing_channels {
if perm
.clone()
.channel(&channel)
.has_permission(db, Permission::ViewChannel)
.await?
{
channels.push(channel);
}
}
}
/* // TODO: EventV1::ServerMemberJoin {
id: self.id.clone(),
user: user.id.clone(),
}
.p(self.id.clone())
.await;
EventV1::ServerCreate {
id: self.id.clone(),
server: self.clone(),
channels: channels.clone(),
}
.private(user.id.clone())
.await; */
if let Some(id) = self
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
Ok(channels)
}
/// Remove a member from a server /// Remove a member from a server
pub async fn remove_member( pub async fn remove_member(
@@ -364,14 +336,14 @@ impl Role {
let role_id = Ulid::new().to_string(); let role_id = Ulid::new().to_string();
db.insert_role(server_id, &role_id, self).await?; db.insert_role(server_id, &role_id, self).await?;
/* // TODO: EventV1::ServerRoleUpdate { EventV1::ServerRoleUpdate {
id: server_id.to_string(), id: server_id.to_string(),
role_id: role_id.to_string(), role_id: role_id.to_string(),
data: self.clone().into_optional(), data: self.clone().into_optional().into(),
clear: vec![], clear: vec![],
} }
.p(server_id.to_string()) .p(server_id.to_string())
.await; */ .await;
Ok(role_id) Ok(role_id)
} }
@@ -394,14 +366,14 @@ impl Role {
db.update_role(server_id, role_id, &partial, remove.clone()) db.update_role(server_id, role_id, &partial, remove.clone())
.await?; .await?;
/* // TODO: EventV1::ServerRoleUpdate { EventV1::ServerRoleUpdate {
id: server_id.to_string(), id: server_id.to_string(),
role_id: role_id.to_string(), role_id: role_id.to_string(),
data: partial, data: partial.into(),
clear: vec![], clear: vec![],
} }
.p(server_id.to_string()) .p(server_id.to_string())
.await; */ .await;
Ok(()) Ok(())
} }
@@ -415,12 +387,12 @@ impl Role {
/// Delete a role /// Delete a role
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> { pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
/* // TODO: EventV1::ServerRoleDelete { EventV1::ServerRoleDelete {
id: server_id.to_string(), id: server_id.to_string(),
role_id: role_id.to_string(), role_id: role_id.to_string(),
} }
.p(server_id.to_string()) .p(server_id.to_string())
.await; */ .await;
db.delete_role(server_id, role_id).await db.delete_role(server_id, role_id).await
} }
@@ -449,3 +421,36 @@ impl SystemMessageChannels {
ids ids
} }
} }
#[cfg(test)]
mod tests {
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[async_std::test]
async fn permissions() {
database_test!(|db| async move {
fixture!(db, "server_with_roles",
owner user 0
moderator user 1
user user 2
server server 4);
let mut query = DatabasePermissionQuery::new(&db, &owner).server(&server);
assert!(calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::GrantAllSafe));
let mut query = DatabasePermissionQuery::new(&db, &moderator).server(&server);
assert!(calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::BanMembers));
let mut query = DatabasePermissionQuery::new(&db, &user).server(&server);
assert!(!calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::BanMembers));
});
}
}
@@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::Database; use crate::{events::client::EventV1, Database};
use revolt_result::Result; use revolt_result::Result;
@@ -16,12 +16,12 @@ impl UserSettingsImpl for UserSettings {
async fn set(self, db: &Database, user: &str) -> Result<()> { async fn set(self, db: &Database, user: &str) -> Result<()> {
db.set_user_settings(user, &self).await?; db.set_user_settings(user, &self).await?;
/* // TODO: EventV1::UserSettingsUpdate { EventV1::UserSettingsUpdate {
id: user.to_string(), id: user.to_string(),
update: self, update: self,
} }
.private(user.to_string()) .private(user.to_string())
.await; */ .await;
Ok(()) Ok(())
} }
@@ -2,8 +2,12 @@ mod model;
mod ops; mod ops;
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
mod rocket; mod rocket;
#[cfg(feature = "rocket-impl")]
mod schema;
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
pub use self::rocket::*; pub use self::rocket::*;
#[cfg(feature = "rocket-impl")]
pub use self::schema::*;
pub use model::*; pub use model::*;
pub use ops::*; pub use ops::*;
+415 -15
View File
@@ -1,6 +1,12 @@
use crate::{Database, File}; use std::{collections::HashSet, time::Duration};
use revolt_result::{Error, ErrorType, Result}; use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_config::config;
use revolt_result::{create_error, Error, ErrorType, Result};
use ulid::Ulid;
auto_derived_partial!( auto_derived_partial!(
/// # User /// # User
@@ -10,6 +16,11 @@ auto_derived_partial!(
pub id: String, pub id: String,
/// Username /// Username
pub username: String, pub username: String,
/// Discriminator
pub discriminator: String,
/// Display name
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
/// Avatar attachment /// Avatar attachment
pub avatar: Option<File>, pub avatar: Option<File>,
@@ -41,6 +52,15 @@ auto_derived_partial!(
); );
auto_derived!( auto_derived!(
/// Optional fields on user object
pub enum FieldsUser {
Avatar,
StatusText,
StatusPresence,
ProfileContent,
ProfileBackground,
}
/// User's relationship with another user (or themselves) /// User's relationship with another user (or themselves)
pub enum RelationshipStatus { pub enum RelationshipStatus {
None, None,
@@ -98,18 +118,236 @@ auto_derived!(
/// Id of the owner of this bot /// Id of the owner of this bot
pub owner: String, pub owner: String,
} }
/// Optional fields on user object
pub enum FieldsUser {
Avatar,
StatusText,
StatusPresence,
ProfileContent,
ProfileBackground,
}
); );
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
let mut set = (2..9999)
.map(|v| format!("{:0>4}", v))
.collect::<HashSet<String>>();
for discrim in [
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
] {
set.remove(&format!("{:0>4}", discrim));
}
set.into_iter().collect()
});
#[allow(clippy::derivable_impls)]
impl Default for User {
fn default() -> Self {
Self {
id: Default::default(),
username: Default::default(),
discriminator: Default::default(),
display_name: Default::default(),
avatar: Default::default(),
relations: Default::default(),
badges: Default::default(),
status: Default::default(),
profile: Default::default(),
flags: Default::default(),
privileged: Default::default(),
bot: Default::default(),
}
}
}
#[allow(clippy::disallowed_methods)]
impl User { impl User {
/// Create a new user
pub async fn create<I, D>(
db: &Database,
username: String,
account_id: I,
data: D,
) -> Result<User>
where
I: Into<Option<String>>,
D: Into<Option<PartialUser>>,
{
let username = User::validate_username(username)?;
let mut user = User {
id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()),
discriminator: User::find_discriminator(db, &username, None).await?,
username,
..Default::default()
};
if let Some(data) = data.into() {
user.apply_options(data);
}
db.insert_user(&user).await?;
Ok(user)
}
/// Get the relationship with another user
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
if self.id == user_b {
return RelationshipStatus::User;
}
if let Some(relations) = &self.relations {
if let Some(relationship) = relations.iter().find(|x| x.id == user_b) {
return relationship.status.clone();
}
}
RelationshipStatus::None
}
pub fn is_friends_with(&self, user_b: &str) -> bool {
matches!(
self.relationship_with(user_b),
RelationshipStatus::Friend | RelationshipStatus::User
)
}
/// Check whether two users have a mutual connection
///
/// This will check if user and user_b share a server or a group.
pub async fn has_mutual_connection(&self, db: &Database, user_b: &str) -> Result<bool> {
Ok(!db
.fetch_mutual_server_ids(&self.id, user_b)
.await?
.is_empty()
|| !db
.fetch_mutual_channel_ids(&self.id, user_b)
.await?
.is_empty())
}
/// Check if this user can acquire another server
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
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
let username_lowercase = username.to_lowercase();
// Block homoglyphs
if decancer::cure(&username_lowercase).into_str() != username_lowercase {
return Err(create_error!(InvalidUsername));
}
// Ensure the username itself isn't blocked
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"];
for username in BLOCKED_USERNAMES {
if username_lowercase == *username {
return Err(create_error!(InvalidUsername));
}
}
// Ensure none of the following substrings show up in the username
const BLOCKED_SUBSTRINGS: &[&str] = &["```"];
for substr in BLOCKED_SUBSTRINGS {
if username_lowercase.contains(substr) {
return Err(create_error!(InvalidUsername));
}
}
Ok(username)
}
// Find a free discriminator for a given username
pub async fn find_discriminator(
db: &Database,
username: &str,
preferred: Option<(String, String)>,
) -> Result<String> {
let search_space: &HashSet<String> = &DISCRIMINATOR_SEARCH_SPACE;
let used_discriminators: HashSet<String> = db
.fetch_discriminators_in_use(username)
.await?
.into_iter()
.collect();
let available_discriminators: Vec<&String> =
search_space.difference(&used_discriminators).collect();
if available_discriminators.is_empty() {
return Err(create_error!(UsernameTaken));
}
if let Some((preferred, target_id)) = preferred {
if available_discriminators.contains(&&preferred) {
return Ok(preferred);
} else {
if db
.has_ratelimited(
&target_id,
crate::RatelimitEventType::DiscriminatorChange,
Duration::from_secs(60 * 60 * 24),
1,
)
.await?
{
return Err(create_error!(DiscriminatorChangeRatelimited));
}
RatelimitEvent::create(
db,
target_id,
crate::RatelimitEventType::DiscriminatorChange,
)
.await?;
}
}
let mut rng = rand::thread_rng();
Ok(available_discriminators
.choose(&mut rng)
.expect("we can assert this has an element")
.to_string())
}
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
let username = User::validate_username(username)?;
if self.username.to_lowercase() == username.to_lowercase() {
self.update(
db,
PartialUser {
username: Some(username),
..Default::default()
},
vec![],
)
.await
} else {
self.update(
db,
PartialUser {
discriminator: Some(
User::find_discriminator(
db,
&username,
Some((self.discriminator.to_string(), self.id.clone())),
)
.await?,
),
username: Some(username),
..Default::default()
},
vec![],
)
.await
}
}
/// Check whether a username is already in use by another user /// Check whether a username is already in use by another user
#[allow(dead_code)] #[allow(dead_code)]
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> { async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
@@ -123,6 +361,167 @@ impl User {
} }
} }
/// Set a relationship to another user
pub async fn set_relationship(
&mut self,
db: &Database,
user_b: &User,
status: RelationshipStatus,
) -> Result<()> {
db.set_relationship(&self.id, &user_b.id, &status).await?;
if let RelationshipStatus::None | RelationshipStatus::User = status {
if let Some(relations) = &mut self.relations {
relations.retain(|relation| relation.id != user_b.id);
}
} else {
let relation = Relationship {
id: user_b.id.to_string(),
status,
};
if let Some(relations) = &mut self.relations {
relations.retain(|relation| relation.id != user_b.id);
relations.push(relation);
} else {
self.relations = Some(vec![relation]);
}
}
Ok(())
}
/// Apply a certain relationship between two users
pub async fn apply_relationship(
&mut self,
db: &Database,
target: &mut User,
local: RelationshipStatus,
remote: RelationshipStatus,
) -> Result<()> {
target.set_relationship(db, self, remote).await?;
self.set_relationship(db, target, local).await?;
EventV1::UserRelationship {
id: target.id.clone(),
user: self.clone().into(db, Some(&*target)).await,
}
.private(target.id.clone())
.await;
EventV1::UserRelationship {
id: self.id.clone(),
user: target.clone().into(db, Some(&*self)).await,
}
.private(self.id.clone())
.await;
Ok(())
}
/// Add another user as a friend
pub async fn add_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::User => Err(create_error!(NoEffect)),
RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
RelationshipStatus::Outgoing => Err(create_error!(AlreadySentRequest)),
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
RelationshipStatus::Incoming => {
self.apply_relationship(
db,
target,
RelationshipStatus::Friend,
RelationshipStatus::Friend,
)
.await
}
RelationshipStatus::None => {
self.apply_relationship(
db,
target,
RelationshipStatus::Outgoing,
RelationshipStatus::Incoming,
)
.await
}
}
}
/// Remove another user as a friend
pub async fn remove_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::Friend
| RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => {
self.apply_relationship(
db,
target,
RelationshipStatus::None,
RelationshipStatus::None,
)
.await
}
_ => Err(create_error!(NoEffect)),
}
}
/// Block another user
pub async fn block_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(create_error!(NoEffect)),
RelationshipStatus::BlockedOther => {
self.apply_relationship(
db,
target,
RelationshipStatus::Blocked,
RelationshipStatus::Blocked,
)
.await
}
RelationshipStatus::None
| RelationshipStatus::Friend
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing => {
self.apply_relationship(
db,
target,
RelationshipStatus::Blocked,
RelationshipStatus::BlockedOther,
)
.await
}
}
}
/// Unblock another user
pub async fn unblock_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::Blocked => match target.relationship_with(&self.id) {
RelationshipStatus::Blocked => {
self.apply_relationship(
db,
target,
RelationshipStatus::BlockedOther,
RelationshipStatus::Blocked,
)
.await
}
RelationshipStatus::BlockedOther => {
self.apply_relationship(
db,
target,
RelationshipStatus::None,
RelationshipStatus::None,
)
.await
}
_ => Err(create_error!(InternalError)),
},
_ => Err(create_error!(NoEffect)),
}
}
/// Update user data /// Update user data
pub async fn update<'a>( pub async fn update<'a>(
&mut self, &mut self,
@@ -137,13 +536,14 @@ impl User {
self.apply_options(partial.clone()); self.apply_options(partial.clone());
db.update_user(&self.id, &partial, remove.clone()).await?; db.update_user(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::UserUpdate { EventV1::UserUpdate {
id: self.id.clone(), id: self.id.clone(),
data: partial, data: partial.into(),
clear: remove, clear: remove.into_iter().map(|v| v.into()).collect(),
event_id: Some(Ulid::new().to_string()),
} }
.p_user(self.id.clone(), db) .p_user(self.id.clone(), db)
.await; */ .await;
Ok(()) Ok(())
} }
@@ -22,6 +22,9 @@ pub trait AbstractUsers: Sync + Send {
/// Fetch multiple users by their ids /// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>; async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
/// Fetch all discriminators in use for a username
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>>;
/// Fetch ids of users that both users are friends with /// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>; async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
@@ -87,6 +87,39 @@ impl AbstractUsers for MongoDb {
.await) .await)
} }
/// Fetch all discriminators in use for a username
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
#[derive(Deserialize)]
struct UserDocument {
discriminator: String,
}
Ok(self
.col::<UserDocument>(COL)
.find(
doc! {
"username": username
},
FindOptions::builder()
.collation(
Collation::builder()
.locale("en")
.strength(CollationStrength::Secondary)
.build(),
)
.projection(doc! { "_id": 0, "discriminator": 1 })
.build(),
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { s.ok() })
.collect::<Vec<UserDocument>>()
.await
.into_iter()
.map(|user| user.discriminator)
.collect::<Vec<String>>())
}
/// Fetch ids of users that both users are friends with /// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self Ok(self
@@ -1,7 +1,7 @@
use revolt_result::Result; use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
use crate::{ReferenceDb, Relationship};
use super::AbstractUsers; use super::AbstractUsers;
@@ -56,6 +56,18 @@ impl AbstractUsers for ReferenceDb {
.collect() .collect()
} }
/// Fetch all discriminators in use for a username
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
let users = self.users.lock().await;
let lowercase = username.to_lowercase();
Ok(users
.values()
.filter(|user| user.username.to_lowercase() == lowercase)
.map(|user| &user.discriminator)
.cloned()
.collect())
}
/// Fetch ids of users that both users are friends with /// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
todo!() todo!()
@@ -94,19 +106,49 @@ impl AbstractUsers for ReferenceDb {
/// Set relationship with another user /// Set relationship with another user
/// ///
/// This should use pull_relationship if relationship is None. /// This should use pull_relationship if relationship is None or User.
async fn set_relationship( async fn set_relationship(
&self, &self,
_user_id: &str, user_id: &str,
_target_id: &str, target_id: &str,
_relationship: &RelationshipStatus, relationship: &RelationshipStatus,
) -> Result<()> { ) -> Result<()> {
todo!() if let RelationshipStatus::User | RelationshipStatus::None = &relationship {
self.pull_relationship(user_id, target_id).await
} else {
let mut users = self.users.lock().await;
let user = users
.get_mut(user_id)
.ok_or_else(|| create_error!(NotFound))?;
let relation = Relationship {
id: target_id.to_string(),
status: relationship.clone(),
};
if let Some(relations) = &mut user.relations {
relations.retain(|relation| relation.id != target_id);
relations.push(relation);
} else {
user.relations = Some(vec![relation]);
}
Ok(())
}
} }
/// Remove relationship with another user /// Remove relationship with another user
async fn pull_relationship(&self, _user_id: &str, _target_id: &str) -> Result<()> { async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
todo!() let mut users = self.users.lock().await;
let user = users
.get_mut(user_id)
.ok_or_else(|| create_error!(NotFound))?;
if let Some(relations) = &mut user.relations {
relations.retain(|relation| relation.id != target_id);
}
Ok(())
} }
/// Delete a user by their id /// Delete a user by their id
@@ -13,19 +13,19 @@ impl<'r> FromRequest<'r> for User {
.local_cache_async(async { .local_cache_async(async {
let db = request.rocket().state::<Database>().expect("`Database`"); let db = request.rocket().state::<Database>().expect("`Database`");
let _header_bot_token = request let header_bot_token = request
.headers() .headers()
.get("x-bot-token") .get("x-bot-token")
.next() .next()
.map(|x| x.to_string()); .map(|x| x.to_string());
/* if let Some(bot_token) = header_bot_token { if let Some(bot_token) = header_bot_token {
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await { if let Ok(bot) = db.fetch_bot_by_token(&bot_token).await {
return Some(user); if let Ok(user) = db.fetch_user(&bot.id).await {
return Some(user);
}
} }
} else */ } else if let Outcome::Success(session) = request.guard::<Session>().await {
if let Outcome::Success(session) = request.guard::<Session>().await {
// This uses a guard so can't really easily be refactored into from_token at this stage.
if let Ok(user) = db.fetch_user(&session.user_id).await { if let Ok(user) = db.fetch_user(&session.user_id).await {
return Some(user); return Some(user);
} }
@@ -0,0 +1,31 @@
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
};
use crate::User;
impl<'r> OpenApiFromRequest<'r> for User {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
let mut requirements = schemars::Map::new();
requirements.insert("Session Token".to_owned(), vec![]);
Ok(RequestHeaderInput::Security(
"Session Token".to_owned(),
SecurityScheme {
data: SecuritySchemeData::ApiKey {
name: "x-session-token".to_owned(),
location: "header".to_owned(),
},
description: Some("Used to authenticate as a user.".to_owned()),
extensions: schemars::Map::new(),
},
requirements,
))
}
}
+122
View File
@@ -0,0 +1,122 @@
// Queue Type: Debounced
use crate::Database;
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use std::{collections::HashMap, time::Duration};
use super::DelayedTask;
/// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)]
pub enum AckEvent {
/// Add mentions for a user in a channel
AddMention {
/// Message IDs
ids: Vec<String>,
},
/// Acknowledge message in a channel for a user
AckMessage {
/// Message ID
id: String,
},
}
/// Task information
struct Data {
/// Channel to ack
channel: String,
/// User to ack for
user: String,
/// Event
event: AckEvent,
}
#[derive(Debug)]
struct Task {
event: AckEvent,
}
static Q: Lazy<Queue<Data>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(channel: String, user: String, event: AckEvent) {
Q.try_push(Data {
channel,
user,
event,
})
.ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let mut tasks = HashMap::<(String, String), DelayedTask<Task>>::new();
let mut keys = vec![];
loop {
// Find due tasks.
for (key, task) in &tasks {
if task.should_run() {
keys.push(key.clone());
}
}
// Commit any due tasks to the database.
for key in &keys {
if let Some(task) = tasks.remove(key) {
let Task { event } = task.data;
let (user, channel) = key;
if let Err(err) = match &event {
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
AckEvent::AckMessage { id } => db.acknowledge_message(channel, user, id).await,
AckEvent::AddMention { ids } => {
db.add_mention_to_unread(channel, user, ids).await
}
} {
error!("{err:?} for {event:?}. ({user}, {channel})");
} else {
info!("User {user} ack in {channel} with {event:?}");
}
}
}
// Clear keys
keys.clear();
// Queue incoming tasks.
while let Some(Data {
channel,
user,
mut event,
}) = Q.try_pop()
{
let key = (user, channel);
if let Some(task) = tasks.get_mut(&key) {
task.delay();
match &mut event {
AckEvent::AddMention { ids } => {
if let AckEvent::AddMention { ids: existing } = &mut task.data.event {
existing.append(ids);
} else {
task.data.event = event;
}
}
AckEvent::AckMessage { .. } => {
task.data.event = event;
}
}
} else {
tasks.insert(key, DelayedTask::new(Task { event }));
}
}
// Sleep for an arbitrary amount of time.
async_std::task::sleep(Duration::from_secs(1)).await;
}
}
@@ -0,0 +1,87 @@
// Queue Type: Debounced
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use std::{collections::HashMap, time::Duration};
use crate::{Database, PartialChannel};
use super::DelayedTask;
/// Task information
struct Data {
/// Channel to update
channel: String,
/// Latest message ID
id: String,
/// Whether the channel is a DM
is_dm: bool,
}
/// Task information
#[derive(Debug)]
struct Task {
/// Latest message ID
id: String,
/// Whether the channel is a DM
is_dm: bool,
}
static Q: Lazy<Queue<Data>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(channel: String, id: String, is_dm: bool) {
Q.try_push(Data { channel, id, is_dm }).ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let mut tasks = HashMap::<String, DelayedTask<Task>>::new();
let mut keys = vec![];
loop {
// Find due tasks.
for (key, task) in &tasks {
if task.should_run() {
keys.push(key.clone());
}
}
// Commit any due tasks to the database.
for key in &keys {
if let Some(task) = tasks.remove(key) {
let Task { id, is_dm, .. } = task.data;
let mut channel = PartialChannel {
last_message_id: Some(id.to_string()),
..Default::default()
};
if is_dm {
channel.active = Some(true);
}
match db.update_channel(key, &channel, vec![]).await {
Ok(_) => info!("Updated last_message_id for {key} to {id}."),
Err(err) => error!("Failed to update last_message_id with {err:?}!"),
}
}
}
// Clear keys
keys.clear();
// Queue incoming tasks.
while let Some(Data { channel, id, is_dm }) = Q.try_pop() {
if let Some(task) = tasks.get_mut(&channel) {
task.data.id = id;
task.delay();
} else {
tasks.insert(channel, DelayedTask::new(Task { id, is_dm }));
}
}
// Sleep for an arbitrary amount of time.
async_std::task::sleep(Duration::from_secs(1)).await;
}
}
+58
View File
@@ -0,0 +1,58 @@
//! Semi-important background task management
use crate::Database;
use async_std::task;
use std::time::Instant;
const WORKER_COUNT: usize = 5;
pub mod ack;
pub mod last_message_id;
pub mod process_embeds;
pub mod web_push;
/// Spawn background workers
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
for _ in 0..WORKER_COUNT {
task::spawn(ack::worker(db.clone()));
task::spawn(last_message_id::worker(db.clone()));
task::spawn(process_embeds::worker(db.clone()));
task::spawn(web_push::worker(authifier_db.clone()));
}
}
/// Task with additional information on when it should run
pub struct DelayedTask<T> {
pub data: T,
last_updated: Instant,
first_seen: Instant,
}
/// Commit to database every 30 seconds if the task is particularly active.
static EXPIRE_CONSTANT: u64 = 30;
/// Otherwise, commit to database after 5 seconds.
static SAVE_CONSTANT: u64 = 5;
impl<T> DelayedTask<T> {
/// Create a new delayed task
pub fn new(data: T) -> Self {
DelayedTask {
data,
last_updated: Instant::now(),
first_seen: Instant::now(),
}
}
/// Push a task further back in time
pub fn delay(&mut self) {
self.last_updated = Instant::now()
}
/// Check if a task should run yet
pub fn should_run(&self) -> bool {
self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT
|| self.last_updated.elapsed().as_secs() > SAVE_CONSTANT
}
}
@@ -0,0 +1,170 @@
use crate::{models::Message, AppendMessage, Database};
use futures::future::join_all;
use linkify::{LinkFinder, LinkKind};
use regex::Regex;
use revolt_config::config;
use revolt_result::Result;
use async_lock::Semaphore;
use async_std::task::spawn;
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use revolt_models::v0::Embed;
use std::{collections::HashSet, sync::Arc};
use isahc::prelude::*;
/// Task information
#[derive(Debug)]
struct EmbedTask {
/// Channel we're processing the event in
channel: String,
/// ID of the message we're processing
id: String,
/// Content of the message
content: String,
}
static Q: Lazy<Queue<EmbedTask>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(channel: String, id: String, content: String) {
Q.try_push(EmbedTask {
channel,
id,
content,
})
.ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let semaphore = Arc::new(Semaphore::new(
config().await.api.workers.max_concurrent_connections,
));
loop {
let task = Q.pop().await;
let db = db.clone();
let semaphore = semaphore.clone();
spawn(async move {
let config = config().await;
let embeds = generate(
task.content,
&config.hosts.january,
config.features.limits.default.message_embeds,
semaphore,
)
.await;
if let Ok(embeds) = embeds {
if let Err(err) = Message::append(
&db,
task.id,
task.channel,
AppendMessage {
embeds: Some(embeds),
},
)
.await
{
error!("Encountered an error appending to message: {:?}", err);
}
}
});
}
}
static RE_CODE: Lazy<Regex> = Lazy::new(|| Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap());
static RE_IGNORED: Lazy<Regex> = Lazy::new(|| Regex::new("(<http.+>)").unwrap());
pub async fn generate(
content: String,
host: &str,
max_embeds: usize,
semaphore: Arc<Semaphore>,
) -> Result<Vec<Embed>> {
// Ignore code blocks.
let content = RE_CODE.replace_all(&content, "");
// Ignore all content between angle brackets starting with http.
let content = RE_IGNORED.replace_all(&content, "");
let content = content
// Ignore quoted lines.
.split('\n')
.map(|v| {
if let Some(c) = v.chars().next() {
if c == '>' {
return "";
}
}
v
})
.collect::<Vec<&str>>()
.join("\n");
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
// Process all links, stripping anchors and
// only taking up to `max_embeds` of links.
let links: Vec<String> = finder
.links(&content)
.map(|x| {
x.as_str()
.chars()
.take_while(|&ch| ch != '#')
.collect::<String>()
})
.collect::<HashSet<String>>()
.into_iter()
.take(max_embeds)
.collect();
// If no links, fail out.
if links.is_empty() {
return Err(create_error!(LabelMe));
}
// TODO: batch request to january
let mut tasks = Vec::new();
for link in links {
let semaphore = semaphore.clone();
let host = host.to_string();
tasks.push(spawn(async move {
let guard = semaphore.acquire().await;
if let Ok(mut response) = isahc::get_async(format!(
"{host}/embed?url={}",
url_escape::encode_component(&link)
))
.await
{
drop(guard);
response.json::<Embed>().await.ok()
} else {
None
}
}));
}
let embeds = join_all(tasks)
.await
.into_iter()
.flatten()
.collect::<Vec<Embed>>();
// Prevent database update when no embeds are found.
if !embeds.is_empty() {
Ok(embeds)
} else {
Err(create_error!(LabelMe))
}
}
+162
View File
@@ -0,0 +1,162 @@
use std::collections::HashSet;
use authifier::Database;
use base64::{
engine::{self},
Engine as _,
};
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use revolt_config::config;
use revolt_models::v0::PushNotification;
use revolt_presence::filter_online;
use serde_json::json;
use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushMessageBuilder,
};
/// Task information
#[derive(Debug)]
struct PushTask {
/// User IDs of the targets that are to receive this notification
recipients: Vec<String>,
/// Push Notification
payload: PushNotification,
}
static Q: Lazy<Queue<PushTask>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(recipients: Vec<String>, payload: PushNotification) {
if recipients.is_empty() {
return;
}
let online_ids = filter_online(&recipients).await;
let recipients = (&recipients.into_iter().collect::<HashSet<String>>() - &online_ids)
.into_iter()
.collect::<Vec<String>>();
Q.try_push(PushTask {
recipients,
payload,
})
.ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let config = config().await;
let web_push_client = IsahcWebPushClient::new().unwrap();
let fcm_client = if config.api.fcm.api_key.is_empty() {
None
} else {
Some(fcm::Client::new())
};
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
.decode(config.api.vapid.private_key)
.expect("valid `VAPID_PRIVATE_KEY`");
loop {
let task = Q.pop().await;
if let Ok(sessions) = db.find_sessions_with_subscription(&task.recipients).await {
for session in sessions {
if let Some(sub) = session.subscription {
if sub.endpoint == "fcm" {
// Use Firebase Cloud Messaging
if let Some(client) = &fcm_client {
let PushNotification {
author,
icon,
image: _,
body,
tag,
timestamp: _,
url: _,
} = &task.payload;
let mut notification = fcm::NotificationBuilder::new();
notification.title(author);
notification.icon(icon);
notification.body(body);
notification.tag(tag);
// TODO: expand support for fields
let notification = notification.finalize();
let mut message_builder =
fcm::MessageBuilder::new(&config.api.fcm.api_key, &sub.auth);
message_builder.notification(notification);
if let Err(err) = client.send(message_builder.finalize()).await {
error!("Failed to send FCM notification! {:?}", err);
} else {
info!("Sent FCM notification to {:?}.", session.id);
}
} else {
info!("No FCM token was specified!");
}
} else {
// Use Web Push Standard
let subscription = SubscriptionInfo {
endpoint: sub.endpoint,
keys: SubscriptionKeys {
auth: sub.auth,
p256dh: sub.p256dh,
},
};
match VapidSignatureBuilder::from_pem(
std::io::Cursor::new(&web_push_private_key),
&subscription,
) {
Ok(sig_builder) => match sig_builder.build() {
Ok(signature) => {
let mut builder = WebPushMessageBuilder::new(&subscription);
builder.set_vapid_signature(signature);
let payload = json!(task.payload).to_string();
builder
.set_payload(ContentEncoding::AesGcm, payload.as_bytes());
match builder.build() {
Ok(msg) => match web_push_client.send(msg).await {
Ok(_) => {
info!(
"Sent Web Push notification to {:?}.",
session.id
)
}
Err(err) => {
error!("Hit error sending Web Push! {:?}", err)
}
},
Err(err) => {
error!(
"Failed to build message for {}! {:?}",
session.user_id, err
)
}
}
}
Err(err) => error!(
"Failed to build signature for {}! {:?}",
session.user_id, err
),
},
Err(err) => error!(
"Failed to create signature builder for {}! {:?}",
session.user_id, err
),
}
}
}
}
}
}
}
+546 -30
View File
@@ -1,14 +1,7 @@
use revolt_models::v0::*; use revolt_models::v0::*;
use revolt_permissions::{calculate_user_permissions, UserPermission};
impl From<crate::AccountStrike> for AccountStrike { use crate::{util::permissions::DatabasePermissionQuery, Database};
fn from(value: crate::AccountStrike) -> Self {
AccountStrike {
id: value.id,
user_id: value.user_id,
reason: value.reason,
}
}
}
impl crate::Bot { impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot { pub fn into_public_bot(self, user: crate::User) -> PublicBot {
@@ -44,6 +37,70 @@ impl From<crate::Bot> for Bot {
} }
} }
impl From<FieldsBot> for crate::FieldsBot {
fn from(value: FieldsBot) -> Self {
match value {
FieldsBot::InteractionsURL => crate::FieldsBot::InteractionsURL,
FieldsBot::Token => crate::FieldsBot::Token,
}
}
}
impl From<crate::FieldsBot> for FieldsBot {
fn from(value: crate::FieldsBot) -> Self {
match value {
crate::FieldsBot::InteractionsURL => FieldsBot::InteractionsURL,
crate::FieldsBot::Token => FieldsBot::Token,
}
}
}
impl From<crate::Invite> for Invite {
fn from(value: crate::Invite) -> Self {
match value {
crate::Invite::Group {
code,
creator,
channel,
} => Invite::Group {
code,
creator,
channel,
},
crate::Invite::Server {
code,
server,
creator,
channel,
} => Invite::Server {
code,
server,
creator,
channel,
},
}
}
}
impl From<crate::ChannelUnread> for ChannelUnread {
fn from(value: crate::ChannelUnread) -> Self {
ChannelUnread {
id: value.id.into(),
last_id: value.last_id,
mentions: value.mentions.unwrap_or_default(),
}
}
}
impl From<crate::ChannelCompositeKey> for ChannelCompositeKey {
fn from(value: crate::ChannelCompositeKey) -> Self {
ChannelCompositeKey {
channel: value.channel,
user: value.user,
}
}
}
impl From<crate::Webhook> for Webhook { impl From<crate::Webhook> for Webhook {
fn from(value: crate::Webhook) -> Self { fn from(value: crate::Webhook) -> Self {
Webhook { Webhook {
@@ -52,6 +109,7 @@ impl From<crate::Webhook> for Webhook {
avatar: value.avatar.map(|file| file.into()), avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id, channel_id: value.channel_id,
token: value.token, token: value.token,
permissions: value.permissions,
} }
} }
} }
@@ -64,6 +122,7 @@ impl From<crate::PartialWebhook> for PartialWebhook {
avatar: value.avatar.map(|file| file.into()), avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id, channel_id: value.channel_id,
token: value.token, token: value.token,
permissions: value.permissions,
} }
} }
} }
@@ -197,6 +256,37 @@ impl From<crate::FieldsChannel> for FieldsChannel {
} }
} }
impl From<crate::Emoji> for Emoji {
fn from(value: crate::Emoji) -> Self {
Emoji {
id: value.id,
parent: value.parent.into(),
creator_id: value.creator_id,
name: value.name,
animated: value.animated,
nsfw: value.nsfw,
}
}
}
impl From<crate::EmojiParent> for EmojiParent {
fn from(value: crate::EmojiParent) -> Self {
match value {
crate::EmojiParent::Detached => EmojiParent::Detached,
crate::EmojiParent::Server { id } => EmojiParent::Server { id },
}
}
}
impl From<EmojiParent> for crate::EmojiParent {
fn from(value: EmojiParent) -> Self {
match value {
EmojiParent::Detached => crate::EmojiParent::Detached,
EmojiParent::Server { id } => crate::EmojiParent::Server { id },
}
}
}
impl From<crate::File> for File { impl From<crate::File> for File {
fn from(value: crate::File) -> Self { fn from(value: crate::File) -> Self {
File { File {
@@ -234,34 +324,383 @@ impl From<crate::Metadata> for Metadata {
} }
} }
impl crate::User { impl From<crate::Message> for Message {
pub async fn into<P>(self, perspective: P) -> User fn from(value: crate::Message) -> Self {
where Message {
P: Into<Option<crate::User>>, id: value.id,
{ nonce: value.nonce,
let relationship = if let Some(perspective) = perspective.into() { channel: value.channel,
perspective author: value.author,
.relations webhook: value.webhook,
.unwrap_or_default() content: value.content,
system: value.system.map(|system| system.into()),
attachments: value
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value
.reactions
.into_iter() .into_iter()
.find(|relationship| relationship.id == self.id) .map(|(k, v)| (k, v.into_iter().collect()))
.map(|relationship| relationship.status.into()) .collect(),
.unwrap_or_default() interactions: value.interactions.into(),
} else { masquerade: value.masquerade.map(|masq| masq.into()),
RelationshipStatus::None }
}; }
}
// do permission stuff here impl From<crate::PartialMessage> for PartialMessage {
// TODO: implement permissions =) fn from(value: crate::PartialMessage) -> Self {
let can_see_profile = false; PartialMessage {
id: value.id,
nonce: value.nonce,
channel: value.channel,
author: value.author,
webhook: value.webhook,
content: value.content,
system: value.system.map(|system| system.into()),
attachments: value
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions.map(|reactions| {
reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect()
}),
interactions: value.interactions.map(|interactions| interactions.into()),
masquerade: value.masquerade.map(|masq| masq.into()),
}
}
}
impl From<crate::SystemMessage> for SystemMessage {
fn from(value: crate::SystemMessage) -> Self {
match value {
crate::SystemMessage::ChannelDescriptionChanged { by } => {
Self::ChannelDescriptionChanged { by }
}
crate::SystemMessage::ChannelIconChanged { by } => Self::ChannelIconChanged { by },
crate::SystemMessage::ChannelOwnershipChanged { from, to } => {
Self::ChannelOwnershipChanged { from, to }
}
crate::SystemMessage::ChannelRenamed { name, by } => Self::ChannelRenamed { name, by },
crate::SystemMessage::Text { content } => Self::Text { content },
crate::SystemMessage::UserAdded { id, by } => Self::UserAdded { id, by },
crate::SystemMessage::UserBanned { id } => Self::UserBanned { id },
crate::SystemMessage::UserJoined { id } => Self::UserJoined { id },
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
}
}
}
impl From<crate::Interactions> for Interactions {
fn from(value: crate::Interactions) -> Self {
Interactions {
reactions: value
.reactions
.map(|reactions| reactions.into_iter().collect()),
restrict_reactions: value.restrict_reactions,
}
}
}
impl From<Interactions> for crate::Interactions {
fn from(value: Interactions) -> Self {
crate::Interactions {
reactions: value
.reactions
.map(|reactions| reactions.into_iter().collect()),
restrict_reactions: value.restrict_reactions,
}
}
}
impl From<crate::AppendMessage> for AppendMessage {
fn from(value: crate::AppendMessage) -> Self {
AppendMessage {
embeds: value.embeds,
}
}
}
impl From<crate::Masquerade> for Masquerade {
fn from(value: crate::Masquerade) -> Self {
Masquerade {
name: value.name,
avatar: value.avatar,
colour: value.colour,
}
}
}
impl From<Masquerade> for crate::Masquerade {
fn from(value: Masquerade) -> Self {
crate::Masquerade {
name: value.name,
avatar: value.avatar,
colour: value.colour,
}
}
}
impl From<crate::ServerBan> for ServerBan {
fn from(value: crate::ServerBan) -> Self {
ServerBan {
id: value.id.into(),
reason: value.reason,
}
}
}
impl From<crate::Member> for Member {
fn from(value: crate::Member) -> Self {
Member {
id: value.id.into(),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::PartialMember> for PartialMember {
fn from(value: crate::PartialMember) -> Self {
PartialMember {
id: value.id.map(|id| id.into()),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::MemberCompositeKey> for MemberCompositeKey {
fn from(value: crate::MemberCompositeKey) -> Self {
MemberCompositeKey {
server: value.server,
user: value.user,
}
}
}
impl From<crate::FieldsMember> for FieldsMember {
fn from(value: crate::FieldsMember) -> Self {
match value {
crate::FieldsMember::Avatar => FieldsMember::Avatar,
crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout,
}
}
}
impl From<crate::RemovalIntention> for RemovalIntention {
fn from(value: crate::RemovalIntention) -> Self {
match value {
crate::RemovalIntention::Ban => RemovalIntention::Ban,
crate::RemovalIntention::Kick => RemovalIntention::Kick,
crate::RemovalIntention::Leave => RemovalIntention::Leave,
}
}
}
impl From<crate::Server> for Server {
fn from(value: crate::Server) -> Self {
Server {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.unwrap_or_default() as u32,
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::PartialServer> for PartialServer {
fn from(value: crate::PartialServer) -> Self {
PartialServer {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.map(|roles| roles.into_iter().map(|(k, v)| (k, v.into())).collect()),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.map(|v| v as u32),
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::FieldsServer> for FieldsServer {
fn from(value: crate::FieldsServer) -> Self {
match value {
crate::FieldsServer::Banner => FieldsServer::Banner,
crate::FieldsServer::Categories => FieldsServer::Categories,
crate::FieldsServer::Description => FieldsServer::Description,
crate::FieldsServer::Icon => FieldsServer::Icon,
crate::FieldsServer::SystemMessages => FieldsServer::SystemMessages,
}
}
}
impl From<crate::Category> for Category {
fn from(value: crate::Category) -> Self {
Category {
id: value.id,
title: value.title,
channels: value.channels,
}
}
}
impl From<crate::SystemMessageChannels> for SystemMessageChannels {
fn from(value: crate::SystemMessageChannels) -> Self {
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 {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::PartialRole> for PartialRole {
fn from(value: crate::PartialRole) -> Self {
PartialRole {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::FieldsRole> for FieldsRole {
fn from(value: crate::FieldsRole) -> Self {
match value {
crate::FieldsRole::Colour => FieldsRole::Colour,
}
}
}
impl crate::User {
pub async fn into<'a, P>(self, db: &Database, perspective: P) -> User
where
P: Into<Option<&'a crate::User>>,
{
let perspective = perspective.into();
let (relationship, can_see_profile) = if self.bot.is_some() {
(RelationshipStatus::None, true)
} else if let Some(perspective) = perspective {
let mut query = DatabasePermissionQuery::new(db, perspective).user(&self);
if perspective.id == self.id {
(RelationshipStatus::User, true)
} else {
(
perspective
.relations
.as_ref()
.map(|relations| {
relations
.iter()
.find(|relationship| relationship.id == self.id)
.map(|relationship| relationship.status.clone().into())
.unwrap_or_default()
})
.unwrap_or_default(),
calculate_user_permissions(&mut query)
.await
.has_user_permission(UserPermission::ViewProfile),
)
}
} else {
(RelationshipStatus::None, false)
};
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()), avatar: self.avatar.map(|file| file.into()),
relations: vec![], relations: if let Some(crate::User { id, .. }) = perspective {
if id == &self.id {
self.relations
.unwrap_or_default()
.into_iter()
.map(|relation| relation.into())
.collect()
} else {
vec![]
}
} else {
vec![]
},
badges: self.badges.unwrap_or_default() as u32, badges: self.badges.unwrap_or_default() as u32,
status: None, status: if can_see_profile {
profile: None, 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, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
@@ -270,6 +709,83 @@ impl crate::User {
id: self.id, id: self.id,
} }
} }
pub async fn into_self(self) -> User {
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: self
.relations
.map(|relationships| {
relationships
.into_iter()
.map(|relationship| relationship.into())
.collect()
})
.unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32,
status: self.status.map(|status| status.into()),
profile: self.profile.map(|profile| profile.into()),
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship: RelationshipStatus::User,
online: revolt_presence::is_online(&self.id).await,
id: self.id,
}
}
}
impl From<crate::PartialUser> for PartialUser {
fn from(value: crate::PartialUser) -> Self {
PartialUser {
username: value.username,
discriminator: value.discriminator,
display_name: value.display_name,
avatar: value.avatar.map(|file| file.into()),
relations: value.relations.map(|relationships| {
relationships
.into_iter()
.map(|relationship| relationship.into())
.collect()
}),
badges: value.badges.map(|badges| badges as u32),
status: value.status.map(|status| status.into()),
profile: value.profile.map(|profile| profile.into()),
flags: value.flags.map(|flags| flags as u32),
privileged: value.privileged,
bot: value.bot.map(|bot| bot.into()),
relationship: None,
online: None,
id: value.id,
}
}
}
impl From<FieldsUser> for crate::FieldsUser {
fn from(value: FieldsUser) -> Self {
match value {
FieldsUser::Avatar => crate::FieldsUser::Avatar,
FieldsUser::ProfileBackground => crate::FieldsUser::ProfileBackground,
FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent,
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
FieldsUser::StatusText => crate::FieldsUser::StatusText,
}
}
}
impl From<crate::FieldsUser> for FieldsUser {
fn from(value: crate::FieldsUser) -> Self {
match value {
crate::FieldsUser::Avatar => FieldsUser::Avatar,
crate::FieldsUser::ProfileBackground => FieldsUser::ProfileBackground,
crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent,
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
crate::FieldsUser::StatusText => FieldsUser::StatusText,
}
}
} }
impl From<crate::RelationshipStatus> for RelationshipStatus { impl From<crate::RelationshipStatus> for RelationshipStatus {
@@ -1,23 +1,21 @@
use crate::{Error, Result}; use std::num::NonZeroUsize;
use revolt_result::{create_error, Result};
#[cfg(feature = "rocket-impl")]
use revolt_result::Error;
use async_std::sync::Mutex; use async_std::sync::Mutex;
use revolt_rocket_okapi::gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
use serde::{Deserialize, Serialize};
use validator::Validate;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
#[derive(Validate, Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct IdempotencyKey { pub struct IdempotencyKey {
#[validate(length(min = 1, max = 64))]
key: String, key: String,
} }
static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> = Lazy::new(|| Mutex::new(lru::LruCache::new(100))); static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
impl IdempotencyKey { impl IdempotencyKey {
// Backwards compatibility. // Backwards compatibility.
@@ -26,7 +24,7 @@ impl IdempotencyKey {
if let Some(v) = v { if let Some(v) = v {
let mut cache = TOKEN_CACHE.lock().await; let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&v).is_some() { if cache.get(&v).is_some() {
return Err(Error::DuplicateNonce); return Err(create_error!(DuplicateNonce));
} }
cache.put(v.clone(), ()); cache.put(v.clone(), ());
@@ -41,6 +39,17 @@ impl IdempotencyKey {
} }
} }
#[cfg(feature = "rocket-impl")]
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
revolt_okapi::openapi3::{Parameter, ParameterValue},
};
#[cfg(feature = "rocket-impl")]
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
#[cfg(feature = "rocket-impl")]
impl<'r> OpenApiFromRequest<'r> for IdempotencyKey { impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
fn from_request_input( fn from_request_input(
_gen: &mut OpenApiGenerator, _gen: &mut OpenApiGenerator,
@@ -70,6 +79,13 @@ impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
} }
} }
#[cfg(feature = "rocket-impl")]
use rocket::{
http::Status,
request::{FromRequest, Outcome},
};
#[cfg(feature = "rocket-impl")]
#[async_trait] #[async_trait]
impl<'r> FromRequest<'r> for IdempotencyKey { impl<'r> FromRequest<'r> for IdempotencyKey {
type Error = Error; type Error = Error;
@@ -81,14 +97,19 @@ impl<'r> FromRequest<'r> for IdempotencyKey {
.next() .next()
.map(|k| k.to_string()) .map(|k| k.to_string())
{ {
let idempotency = IdempotencyKey { key }; if key.len() > 64 {
if let Err(error) = idempotency.validate() { return Outcome::Failure((
return Outcome::Failure((Status::BadRequest, Error::FailedValidation { error })); Status::BadRequest,
create_error!(FailedValidation {
error: "idempotency key too long".to_string(),
}),
));
} }
let idempotency = IdempotencyKey { key };
let mut cache = TOKEN_CACHE.lock().await; let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&idempotency.key).is_some() { if cache.get(&idempotency.key).is_some() {
return Outcome::Failure((Status::Conflict, Error::DuplicateNonce)); return Outcome::Failure((Status::Conflict, create_error!(DuplicateNonce)));
} }
cache.put(idempotency.key.clone(), ()); cache.put(idempotency.key.clone(), ());
+4
View File
@@ -1,3 +1,7 @@
pub mod bridge; pub mod bridge;
pub mod idempotency;
pub mod permissions; pub mod permissions;
pub mod reference; pub mod reference;
#[cfg(test)]
pub mod test_fixtures;
+279 -33
View File
@@ -1,29 +1,32 @@
use std::borrow::Cow; use std::borrow::Cow;
use revolt_permissions::{ use revolt_permissions::{
calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus, calculate_user_permissions, ChannelType, Override, PermissionQuery, PermissionValue,
RelationshipStatus,
}; };
use crate::{Database, User}; use crate::{Channel, Database, Member, Server, User};
/// Permissions calculator /// Permissions calculator
pub struct PermissionCalculator<'a> { #[derive(Clone)]
pub struct DatabasePermissionQuery<'a> {
#[allow(dead_code)] #[allow(dead_code)]
database: &'a Database, database: &'a Database,
perspective: &'a User, perspective: &'a User,
user: Option<Cow<'a, User>>, user: Option<Cow<'a, User>>,
// pub channel: Cow<'a, Channel>, channel: Option<Cow<'a, Channel>>,
// pub server: Cow<'a, Server>, server: Option<Cow<'a, Server>>,
// pub member: Cow<'a, Member>, member: Option<Cow<'a, Member>>,
// flag_known_relationship: Option<&'a RelationshipStatus>, // flag_known_relationship: Option<&'a RelationshipStatus>,
cached_user_permission: Option<u32>, cached_user_permission: Option<PermissionValue>,
cached_mutual_connection: Option<bool>,
cached_permission: Option<u64>, cached_permission: Option<u64>,
} }
#[async_trait] #[async_trait]
impl PermissionQuery for PermissionCalculator<'_> { impl PermissionQuery for DatabasePermissionQuery<'_> {
// * For calculating user permission // * For calculating user permission
/// Is our perspective user privileged? /// Is our perspective user privileged?
@@ -48,6 +51,10 @@ impl PermissionQuery for PermissionCalculator<'_> {
/// Get the relationship with have with the currently selected user /// Get the relationship with have with the currently selected user
async fn user_relationship(&mut self) -> RelationshipStatus { async fn user_relationship(&mut self) -> RelationshipStatus {
if let Some(other_user) = &self.user { if let Some(other_user) = &self.user {
if self.perspective.id == other_user.id {
return RelationshipStatus::User;
}
if let Some(relations) = &self.perspective.relations { if let Some(relations) = &self.perspective.relations {
for entry in relations { for entry in relations {
if entry.id == other_user.id { if entry.id == other_user.id {
@@ -81,93 +88,308 @@ impl PermissionQuery for PermissionCalculator<'_> {
/// Do we have a mutual connection with the currently selected user? /// Do we have a mutual connection with the currently selected user?
async fn have_mutual_connection(&mut self) -> bool { async fn have_mutual_connection(&mut self) -> bool {
// TODO: User::has_mutual_connection if let Some(value) = self.cached_mutual_connection {
false value
} else if let Some(user) = &self.user {
let value = self
.perspective
.has_mutual_connection(self.database, &user.id)
.await
.unwrap_or_default();
self.cached_mutual_connection = Some(value);
matches!(value, true)
} else {
false
}
} }
// * For calculating server permission // * For calculating server permission
/// Is our perspective user the server's owner? /// Is our perspective user the server's owner?
async fn are_we_server_owner(&mut self) -> bool { async fn are_we_server_owner(&mut self) -> bool {
todo!() if let Some(server) = &self.server {
server.owner == self.perspective.id
} else {
false
}
} }
/// Is our perspective user a member of the server? /// Is our perspective user a member of the server?
async fn are_we_a_member(&mut self) -> bool { async fn are_we_a_member(&mut self) -> bool {
todo!() if let Some(server) = &self.server {
if self.member.is_some() {
true
} else if let Ok(member) = self
.database
.fetch_member(&server.id, &self.perspective.id)
.await
{
self.member = Some(Cow::Owned(member));
true
} else {
false
}
} else {
false
}
} }
/// Get default server permission /// Get default server permission
async fn get_default_server_permissions(&mut self) -> u64 { async fn get_default_server_permissions(&mut self) -> u64 {
todo!() if let Some(server) = &self.server {
server.default_permissions as u64
} else {
0
}
} }
/// Get the ordered role overrides (from lowest to highest) for this member in this server /// Get the ordered role overrides (from lowest to highest) for this member in this server
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> { async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
todo!() if let Some(server) = &self.server {
let member_roles = self
.member
.as_ref()
.map(|member| member.roles.clone())
.unwrap_or_default();
let mut roles = server
.roles
.iter()
.filter(|(id, _)| member_roles.contains(id))
.map(|(_, role)| {
let v: Override = role.permissions.into();
(role.rank, v)
})
.collect::<Vec<(i64, Override)>>();
roles.sort_by(|a, b| b.0.cmp(&a.0));
roles.into_iter().map(|(_, v)| v).collect()
} else {
vec![]
}
} }
/// Is our perspective user timed out on this server? /// Is our perspective user timed out on this server?
async fn are_we_timed_out(&mut self) -> bool { async fn are_we_timed_out(&mut self) -> bool {
todo!() if let Some(member) = &self.member {
member.in_timeout()
} else {
false
}
} }
// * For calculating channel permission // * For calculating channel permission
/// Get the type of the channel /// Get the type of the channel
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::DirectMessage { .. })
| Cow::Owned(Channel::DirectMessage { .. }) => ChannelType::DirectMessage,
Cow::Borrowed(Channel::Group { .. }) | Cow::Owned(Channel::Group { .. }) => {
ChannelType::Group
}
Cow::Borrowed(Channel::SavedMessages { .. })
| Cow::Owned(Channel::SavedMessages { .. }) => ChannelType::SavedMessages,
Cow::Borrowed(Channel::TextChannel { .. })
| Cow::Owned(Channel::TextChannel { .. })
| Cow::Borrowed(Channel::VoiceChannel { .. })
| Cow::Owned(Channel::VoiceChannel { .. }) => ChannelType::ServerChannel,
}
} else {
ChannelType::Unknown
}
} }
/// Get the default channel permissions /// Get the default channel permissions
/// Group channel defaults should be mapped to an allow-only override /// Group channel defaults should be mapped to an allow-only override
async fn get_default_channel_permissions(&mut self) -> Override { async fn get_default_channel_permissions(&mut self) -> Override {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::Group { permissions, .. })
| Cow::Owned(Channel::Group { permissions, .. }) => Override {
allow: permissions.unwrap_or_default() as u64,
deny: 0,
},
Cow::Borrowed(Channel::TextChannel {
default_permissions,
..
})
| Cow::Owned(Channel::TextChannel {
default_permissions,
..
})
| Cow::Borrowed(Channel::VoiceChannel {
default_permissions,
..
})
| Cow::Owned(Channel::VoiceChannel {
default_permissions,
..
}) => default_permissions.unwrap_or_default().into(),
_ => Default::default(),
}
} else {
Default::default()
}
} }
/// Get the ordered role overrides (from lowest to highest) for this member in this channel /// Get the ordered role overrides (from lowest to highest) for this member in this channel
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> { async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::TextChannel {
role_permissions, ..
})
| Cow::Owned(Channel::TextChannel {
role_permissions, ..
})
| Cow::Borrowed(Channel::VoiceChannel {
role_permissions, ..
})
| Cow::Owned(Channel::VoiceChannel {
role_permissions, ..
}) => {
if let Some(server) = &self.server {
let member_roles = self
.member
.as_ref()
.map(|member| member.roles.clone())
.unwrap_or_default();
let mut roles = role_permissions
.iter()
.filter(|(id, _)| member_roles.contains(id))
.filter_map(|(id, permission)| {
server.roles.get(id).map(|role| {
let v: Override = (*permission).into();
(role.rank, v)
})
})
.collect::<Vec<(i64, Override)>>();
roles.sort_by(|a, b| b.0.cmp(&a.0));
roles.into_iter().map(|(_, v)| v).collect()
} else {
vec![]
}
}
_ => vec![],
}
} else {
vec![]
}
} }
/// Do we own this group or saved messages channel if it is one of those? /// Do we own this group or saved messages channel if it is one of those?
async fn do_we_own_the_channel(&mut self) -> bool { async fn do_we_own_the_channel(&mut self) -> bool {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::Group { owner, .. })
| Cow::Owned(Channel::Group { owner, .. }) => owner == &self.perspective.id,
Cow::Borrowed(Channel::SavedMessages { user, .. })
| Cow::Owned(Channel::SavedMessages { user, .. }) => user == &self.perspective.id,
_ => false,
}
} else {
false
}
} }
/// Are we a recipient of this channel? /// Are we a recipient of this channel?
async fn are_we_part_of_the_channel(&mut self) -> bool { async fn are_we_part_of_the_channel(&mut self) -> bool {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::DirectMessage { recipients, .. })
| Cow::Owned(Channel::DirectMessage { recipients, .. })
| Cow::Borrowed(Channel::Group { recipients, .. })
| Cow::Owned(Channel::Group { recipients, .. }) => {
recipients.contains(&self.perspective.id)
}
_ => false,
}
} else {
false
}
} }
/// Set the current user as the recipient of this channel /// Set the current user as the recipient of this channel
/// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths) /// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths)
async fn set_recipient_as_user(&mut self) { async fn set_recipient_as_user(&mut self) {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::DirectMessage { recipients, .. })
| Cow::Owned(Channel::DirectMessage { recipients, .. }) => {
let recipient_id = recipients
.iter()
.find(|recipient| recipient != &&self.perspective.id)
.expect("Missing recipient for DM");
if let Ok(user) = self.database.fetch_user(recipient_id).await {
self.user.replace(Cow::Owned(user));
}
}
_ => unimplemented!(),
}
}
} }
/// Set the current server as the server owning this channel /// Set the current server as the server owning this channel
/// (this will only ever be called for server channels, use unimplemented!() for other code paths) /// (this will only ever be called for server channels, use unimplemented!() for other code paths)
async fn set_server_from_channel(&mut self) { async fn set_server_from_channel(&mut self) {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::TextChannel { server, .. })
| Cow::Owned(Channel::TextChannel { server, .. })
| Cow::Borrowed(Channel::VoiceChannel { server, .. })
| Cow::Owned(Channel::VoiceChannel { server, .. }) => {
if let Some(known_server) =
// I'm not sure why I can't just pattern match both at once here?
// It throws some weird error and the provided fix doesn't work :/
if let Some(Cow::Borrowed(known_server)) = self.server {
Some(known_server)
} else if let Some(Cow::Owned(ref known_server)) = self.server {
Some(known_server)
} else {
None
}
{
if server == &known_server.id {
// Already cached, return early.
return;
}
}
if let Ok(server) = self.database.fetch_server(server).await {
self.server.replace(Cow::Owned(server));
}
}
_ => unimplemented!(),
}
}
} }
} }
impl<'a> PermissionCalculator<'a> { impl<'a> DatabasePermissionQuery<'a> {
/// Create a new permission calculator /// Create a new permission calculator
pub fn new(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> { pub fn new(database: &'a Database, perspective: &'a User) -> DatabasePermissionQuery<'a> {
PermissionCalculator { DatabasePermissionQuery {
database, database,
perspective, perspective,
user: None, user: None,
channel: None,
server: None,
member: None,
cached_mutual_connection: None,
cached_user_permission: None, cached_user_permission: None,
cached_permission: None, cached_permission: None,
} }
} }
/// Calculate the user permission value /// Calculate the user permission value
pub async fn calc_user(mut self) -> PermissionCalculator<'a> { pub async fn calc_user(mut self) -> DatabasePermissionQuery<'a> {
if self.cached_user_permission.is_some() { if self.cached_user_permission.is_some() {
return self; return self;
} }
@@ -176,14 +398,14 @@ impl<'a> PermissionCalculator<'a> {
panic!("Expected `PermissionCalculator.user to exist."); panic!("Expected `PermissionCalculator.user to exist.");
} }
PermissionCalculator { DatabasePermissionQuery {
cached_user_permission: Some(calculate_user_permissions(&mut self).await), cached_user_permission: Some(calculate_user_permissions(&mut self).await),
..self ..self
} }
} }
/// Calculate the permission value /// Calculate the permission value
pub async fn calc(self) -> PermissionCalculator<'a> { pub async fn calc(self) -> DatabasePermissionQuery<'a> {
if self.cached_permission.is_some() { if self.cached_permission.is_some() {
return self; return self;
} }
@@ -192,15 +414,39 @@ impl<'a> PermissionCalculator<'a> {
} }
/// Use user /// Use user
pub fn user(self, user: Cow<'a, User>) -> PermissionCalculator { pub fn user(self, user: &'a User) -> DatabasePermissionQuery {
PermissionCalculator { DatabasePermissionQuery {
user: Some(user), user: Some(Cow::Borrowed(user)),
..self
}
}
/// Use channel
pub fn channel(self, channel: &'a Channel) -> DatabasePermissionQuery {
DatabasePermissionQuery {
channel: Some(Cow::Borrowed(channel)),
..self
}
}
/// Use server
pub fn server(self, server: &'a Server) -> DatabasePermissionQuery {
DatabasePermissionQuery {
server: Some(Cow::Borrowed(server)),
..self
}
}
/// Use member
pub fn member(self, member: &'a Member) -> DatabasePermissionQuery {
DatabasePermissionQuery {
member: Some(Cow::Borrowed(member)),
..self ..self
} }
} }
} }
/// Short-hand for creating a permission calculator /// Short-hand for creating a permission calculator
pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> { pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> DatabasePermissionQuery<'a> {
PermissionCalculator::new(database, perspective) DatabasePermissionQuery::new(database, perspective)
} }
+26 -1
View File
@@ -7,7 +7,7 @@ use schemars::{
JsonSchema, JsonSchema,
}; };
use crate::{Bot, Database, Webhook}; use crate::{Bot, Channel, Database, Emoji, Message, Server, User, Webhook};
/// Reference to some object in the database /// Reference to some object in the database
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -27,6 +27,31 @@ impl Reference {
db.fetch_bot(&self.id).await db.fetch_bot(&self.id).await
} }
/// Fetch emoji from Ref
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
db.fetch_emoji(&self.id).await
}
/// Fetch channel from Ref
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
db.fetch_channel(&self.id).await
}
/// Fetch message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(&self.id).await
}
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await
}
/// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> {
db.fetch_user(&self.id).await
}
/// Fetch webhook from Ref /// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> { pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await db.fetch_webhook(&self.id).await
@@ -0,0 +1,103 @@
use std::collections::HashMap;
use once_cell::sync::Lazy;
use regex::{Captures, Regex};
use serde_json::from_str;
use crate::{Channel, Database, Member, Server, User};
static RE_ID: Lazy<Regex> = Lazy::new(|| Regex::new("__ID:(\\d+)__").unwrap());
#[derive(Debug, Deserialize)]
#[serde(tag = "_object_type")]
enum LoadedFixture {
User(User),
Channel(Channel),
Server(Server),
ServerMember(Member),
}
pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String> {
let mut ids = HashMap::<String, String>::new();
let input = RE_ID.replace_all(input, |cap: &Captures| {
let d = cap.get(1).unwrap().as_str();
if !ids.contains_key(d) {
ids.insert(d.to_string(), ulid::Ulid::new().to_string());
}
ids.get(d).unwrap().clone()
});
// Deserialise the fixtures
let items: Vec<LoadedFixture> = from_str(&input).expect("Failed to deserialise fixture");
// Load all of the items within
for item in items {
#[allow(clippy::disallowed_methods)]
match item {
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
}
}
// Return IDs for ease of use
ids
}
#[async_trait]
pub trait FetchFixture {
async fn user(&self, db: &Database, d: usize) -> User;
async fn channel(&self, db: &Database, d: usize) -> Channel;
async fn server(&self, db: &Database, d: usize) -> Server;
async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member;
}
#[async_trait]
impl FetchFixture for HashMap<String, String> {
async fn user(&self, db: &Database, d: usize) -> User {
db.fetch_user(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn channel(&self, db: &Database, d: usize) -> Channel {
db.fetch_channel(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn server(&self, db: &Database, d: usize) -> Server {
db.fetch_server(self.get(&d.to_string()).unwrap())
.await
.unwrap()
}
async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member {
db.fetch_member(
self.get(&d_server.to_string()).unwrap(),
self.get(&d_user.to_string()).unwrap(),
)
.await
.unwrap()
}
}
#[macro_export]
macro_rules! fixture {
( $database:expr, $name:expr, $( $variable:ident $type:ident $id: expr )+ ) => {
use $crate::util::test_fixtures::FetchFixture;
let fixtures = $crate::util::test_fixtures::load_fixture(
&$database,
include_str!(concat!("../../../fixtures/", $name, ".json")),
)
.await;
$(
let $variable = fixtures.$type(&$database, $id).await;
)+
};
}
+15 -8
View File
@@ -1,28 +1,35 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.6.0-rc.2" version = "0.6.9"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ] authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: API Models" description = "Revolt Backend: API Models"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
serde = [ "dep:serde" ] serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
schemas = [ "dep:schemars" ] schemas = ["dep:schemars", "revolt-permissions/schemas"]
validator = [ "dep:validator" ] validator = ["dep:validator"]
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ] partials = ["dep:revolt_optional_struct", "serde", "schemas"]
default = [ "serde", "partials" ] default = ["serde", "partials"]
[dependencies] [dependencies]
# Core # Core
revolt-permissions = { version = "0.6.0-rc.2", path = "../permissions", features = [ "serde" ] } revolt-config = { version = "0.6.9", path = "../config" }
revolt-permissions = { version = "0.6.9", path = "../permissions" }
# Utility
regex = "1"
indexmap = "1.9.3"
once_cell = "1.17.1"
# Serialisation # Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true } revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
# Spec Generation # Spec Generation
schemars = { version = "0.8.8", optional = true } schemars = { version = "0.8.8", optional = true }
@@ -1,28 +0,0 @@
auto_derived!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// New strike information
pub struct DataCreateStrike {
/// Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// New strike information
pub struct DataEditAccountStrike {
/// New attached reason
pub reason: String,
}
);
+80 -3
View File
@@ -2,6 +2,7 @@ use super::User;
auto_derived!( auto_derived!(
/// Bot /// Bot
#[derive(Default)]
pub struct Bot { pub struct Bot {
/// Bot Id /// Bot Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))] #[cfg_attr(feature = "serde", serde(rename = "_id"))]
@@ -55,6 +56,12 @@ auto_derived!(
pub flags: u32, pub flags: u32,
} }
/// Optional fields on bot object
pub enum FieldsBot {
Token,
InteractionsURL,
}
/// Flags that may be attributed to a bot /// Flags that may be attributed to a bot
#[repr(u32)] #[repr(u32)]
pub enum BotFlags { pub enum BotFlags {
@@ -65,16 +72,22 @@ auto_derived!(
/// Public Bot /// Public Bot
pub struct PublicBot { pub struct PublicBot {
/// Bot Id /// Bot Id
#[serde(rename = "_id")] #[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String, pub id: String,
/// Bot Username /// Bot Username
pub username: String, pub username: String,
/// Profile Avatar /// Profile Avatar
#[serde(skip_serializing_if = "String::is_empty")] #[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub avatar: String, pub avatar: String,
/// Profile Description /// Profile Description
#[serde(skip_serializing_if = "String::is_empty")] #[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub description: String, pub description: String,
} }
@@ -85,4 +98,68 @@ auto_derived!(
/// User object /// User object
pub user: User, pub user: User,
} }
/// Bot Details
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateBot {
/// Bot username
#[cfg_attr(
feature = "validator",
validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")
)]
pub name: String,
}
/// New Bot Details
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataEditBot {
/// Bot username
#[cfg_attr(
feature = "validator",
validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Whether the bot can be added by anyone
pub public: Option<bool>,
/// Whether analytics should be gathered for this bot
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
pub analytics: Option<bool>,
/// Interactions URL
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
pub interactions_url: Option<String>,
/// Fields to remove from bot object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsBot>>,
}
/// Where we are inviting a bot to
#[serde(untagged)]
pub enum InviteBotDestination {
/// Invite to a server
Server {
/// Server Id
server: String,
},
/// Invite to a group
Group {
/// Group Id
group: String,
},
}
/// Owned Bots Response
///
/// Both lists are sorted by their IDs.
///
/// TODO: user should be in bot object
pub struct OwnedBotsResponse {
/// Bot objects
pub bots: Vec<Bot>,
/// User objects
pub users: Vec<User>,
}
); );
@@ -0,0 +1,27 @@
auto_derived!(
/// Invite
pub enum Invite {
/// Invite to a specific server channel
Server {
/// Invite code
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
code: String,
/// Id of the server this invite points to
server: String,
/// Id of user who created this invite
creator: String,
/// Id of the server channel this invite points to
channel: String,
},
/// Invite to a group channel
Group {
/// Invite code
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
code: String,
/// Id of user who created this invite
creator: String,
/// Id of the group channel this invite points to
channel: String,
},
}
);
@@ -0,0 +1,27 @@
auto_derived!(
/// Channel Unread
pub struct ChannelUnread {
/// Composite key pointing to a user's view of a channel
#[serde(rename = "_id")]
pub id: ChannelCompositeKey,
/// Id of the last message read in this channel by a user
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub last_id: Option<String>,
/// Array of message ids that mention the user
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Vec::is_empty", default)
)]
pub mentions: Vec<String>,
}
/// Composite primary key consisting of channel and user id
#[derive(Hash)]
pub struct ChannelCompositeKey {
/// Channel Id
pub channel: String,
/// User Id
pub user: String,
}
);
@@ -1,3 +1,6 @@
#[cfg(feature = "validator")]
use validator::Validate;
use super::File; use super::File;
auto_derived_partial!( auto_derived_partial!(
@@ -16,6 +19,9 @@ auto_derived_partial!(
/// The channel this webhook belongs to /// The channel this webhook belongs to
pub channel_id: String, pub channel_id: String,
/// The permissions for the webhook
pub permissions: u64,
/// The private token for the webhook /// The private token for the webhook
pub token: Option<String>, pub token: Option<String>,
}, },
@@ -43,6 +49,9 @@ auto_derived!(
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub avatar: Option<String>, pub avatar: Option<String>,
/// Webhook permissions
pub permissions: Option<u64>,
/// Fields to remove from webhook /// Fields to remove from webhook
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsWebhook>, pub remove: Vec<FieldsWebhook>,
@@ -61,12 +70,25 @@ auto_derived!(
/// The channel this webhook belongs to /// The channel this webhook belongs to
pub channel_id: String, pub channel_id: String,
/// The permissions for the webhook
pub permissions: u64,
} }
/// Optional fields on webhook object /// Optional fields on webhook object
pub enum FieldsWebhook { pub enum FieldsWebhook {
Avatar, Avatar,
} }
/// Information for the webhook
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct CreateWebhookBody {
#[validate(length(min = 1, max = 32))]
pub name: String,
#[validate(length(min = 1, max = 128))]
pub avatar: Option<String>,
}
); );
impl From<Webhook> for MessageWebhook { impl From<Webhook> for MessageWebhook {
@@ -85,6 +107,7 @@ impl From<Webhook> for ResponseWebhook {
name: value.name, name: value.name,
avatar: value.avatar.map(|file| file.id), avatar: value.avatar.map(|file| file.id),
channel_id: value.channel_id, channel_id: value.channel_id,
permissions: value.permissions,
} }
} }
} }
+67 -1
View File
@@ -1,10 +1,11 @@
use super::File; use super::File;
use revolt_permissions::OverrideField; use revolt_permissions::OverrideField;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
auto_derived!( auto_derived!(
/// Channel /// Channel
#[serde(tag = "channel_type")]
pub enum Channel { pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages /// Personal "Saved Notes" channel which allows users to save messages
SavedMessages { SavedMessages {
@@ -205,4 +206,69 @@ auto_derived!(
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub remove: Option<Vec<FieldsChannel>>, pub remove: Option<Vec<FieldsChannel>>,
} }
/// Create new group
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateGroup {
/// Group name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Group description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Group icon
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub icon: Option<String>,
/// Array of user IDs to add to the group
///
/// Must be friends with these users.
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 49)))]
#[serde(default)]
pub users: HashSet<String>,
/// Whether this group is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
}
/// Server Channel Type
#[derive(Default)]
pub enum LegacyServerChannelType {
/// Text Channel
#[default]
Text,
/// Voice Channel
Voice,
}
/// Create new server channel
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateServerChannel {
/// Channel type
#[serde(rename = "type", default = "LegacyServerChannelType::default")]
pub channel_type: LegacyServerChannelType,
/// Channel name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Channel description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
}
); );
impl Channel {
/// Get a reference to this channel's id
pub fn id(&self) -> &str {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
}
}
}
+160
View File
@@ -0,0 +1,160 @@
use super::File;
auto_derived!(
/// Image positioning and size
pub enum ImageSize {
/// Show large preview at the bottom of the embed
Large,
/// Show small preview to the side of the embed
Preview,
}
/// Image
pub struct Image {
/// URL to the original image
pub url: String,
/// Width of the image
pub width: isize,
/// Height of the image
pub height: isize,
/// Positioning and size
pub size: ImageSize,
}
/// Video
pub struct Video {
/// URL to the original video
pub url: String,
/// Width of the video
pub width: isize,
/// Height of the video
pub height: isize,
}
/// Type of remote Twitch content
pub enum TwitchType {
Channel,
Video,
Clip,
}
/// Type of remote Lightspeed.tv content
pub enum LightspeedType {
Channel,
}
/// Type of remote Bandcamp content
pub enum BandcampType {
Album,
Track,
}
/// Information about special remote content
#[serde(tag = "type")]
pub enum Special {
/// No remote content
None,
/// Content hint that this contains a GIF
///
/// Use metadata to find video or image to play
GIF,
/// YouTube video
YouTube {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
timestamp: Option<String>,
},
/// Lightspeed.tv stream
Lightspeed {
content_type: LightspeedType,
id: String,
},
/// Twitch stream or clip
Twitch {
content_type: TwitchType,
id: String,
},
/// Spotify track
Spotify { content_type: String, id: String },
/// Soundcloud track
Soundcloud,
/// Bandcamp track
Bandcamp {
content_type: BandcampType,
id: String,
},
/// Streamable Video
Streamable { id: String },
}
/// Website metadata
pub struct WebsiteMetadata {
/// Direct URL to web page
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
/// Original direct URL
#[serde(skip_serializing_if = "Option::is_none")]
original_url: Option<String>,
/// Remote content
#[serde(skip_serializing_if = "Option::is_none")]
special: Option<Special>,
/// Title of website
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
/// Description of website
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Embedded image
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<Image>,
/// Embedded video
#[serde(skip_serializing_if = "Option::is_none")]
video: Option<Video>,
// #[serde(skip_serializing_if = "Option::is_none")]
// opengraph_type: Option<String>,
/// Site name
#[serde(skip_serializing_if = "Option::is_none")]
site_name: Option<String>,
/// URL to site icon
#[serde(skip_serializing_if = "Option::is_none")]
icon_url: Option<String>,
/// CSS Colour
#[serde(skip_serializing_if = "Option::is_none")]
colour: Option<String>,
}
/// Text Embed
pub struct Text {
/// URL to icon
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
/// URL for title
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// Title of text embed
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// Description of text embed
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// ID of uploaded autumn file
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<File>,
/// CSS Colour
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
}
/// Embed
#[serde(tag = "type")]
pub enum Embed {
Website(WebsiteMetadata),
Image(Image),
Video(Video),
Text(Text),
None,
}
);
+57
View File
@@ -0,0 +1,57 @@
use once_cell::sync::Lazy;
use regex::Regex;
#[cfg(feature = "validator")]
use validator::Validate;
/// Regex for valid emoji names
///
/// Alphanumeric and underscores
pub static RE_EMOJI: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap());
auto_derived!(
/// Emoji
pub struct Emoji {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// What owns this emoji
pub parent: EmojiParent,
/// Uploader user id
pub creator_id: String,
/// Emoji name
pub name: String,
/// Whether the emoji is animated
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub animated: bool,
/// Whether the emoji is marked as nsfw
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub nsfw: bool,
}
/// Parent Id of the emoji
#[serde(tag = "type")]
pub enum EmojiParent {
Server { id: String },
Detached,
}
/// Create a new emoji
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateEmoji {
/// Server name
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
pub name: String,
/// Parent information
pub parent: EmojiParent,
/// Whether the emoji is mature
#[serde(default)]
pub nsfw: bool,
}
);
+8 -8
View File
@@ -2,7 +2,7 @@ auto_derived!(
/// File /// File
pub struct File { pub struct File {
/// Unique Id /// Unique Id
#[serde(rename = "_id")] #[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String, pub id: String,
/// Tag / bucket this file was uploaded to /// Tag / bucket this file was uploaded to
pub tag: String, pub tag: String,
@@ -16,29 +16,29 @@ auto_derived!(
pub size: isize, pub size: isize,
/// Whether this file was deleted /// Whether this file was deleted
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub deleted: Option<bool>, pub deleted: Option<bool>,
/// Whether this file was reported /// Whether this file was reported
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub reported: Option<bool>, pub reported: Option<bool>,
// TODO: migrate this mess to having: // TODO: migrate this mess to having:
// - author_id // - author_id
// - parent: Parent { Message(id), User(id), etc } // - parent: Parent { Message(id), User(id), etc }
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub message_id: Option<String>, pub message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_id: Option<String>, pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub server_id: Option<String>, pub server_id: Option<String>,
/// Id of the object this file is associated with /// Id of the object this file is associated with
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub object_id: Option<String>, pub object_id: Option<String>,
} }
/// Metadata associated with a file /// Metadata associated with a file
#[serde(tag = "type")] #[cfg_attr(feature = "serde", serde(tag = "type"))]
#[derive(Default)] #[derive(Default)]
pub enum Metadata { pub enum Metadata {
/// File is just a generic uncategorised file /// File is just a generic uncategorised file

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