Compare commits

...
Author SHA1 Message Date
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
Paul Makles eb4870c9d5 fix: explicitly target Rust 1.70.0 for images 2023-06-04 19:42:42 +01:00
Paul Makles b1fdd7fc74 merge: pull request #251 from revoltchat/fix/zomatree/send-message-null 2023-06-04 19:29:07 +01:00
Paul MaklesandToastXC bf3e027067 feat: channel model implementation (#250)
Co-authored-by: ToastXC <100072983+toastxc@users.noreply.github.com>
2023-06-04 18:52:49 +01:00
Zomatree a7a7e49e18 fix: send message route not accepting null 2023-06-04 18:46:49 +01:00
Paul Makles 3c834d06d4 fix: must not restrict lowerbound for embeds 2023-06-03 17:49:31 +01:00
Paul Makles f5e6086d2a fix(core/database): test matches incorrect entry
[skip ci]
2023-06-03 17:15:43 +01:00
Paul Makles 3365a8e482 chore: force bind to 0.0.0.0 because it's just ignoring all environment variables??? 2023-06-03 17:05:25 +01:00
Paul Makles 268c2d1355 fix: re-order crate publishing 2023-06-03 14:11:44 +01:00
Paul Makles e9d6b963df chore: bump version to 0.6.0-rc.1 2023-06-03 14:06:04 +01:00
Paul Makles f9d9447203 feat: add additional validation for github webhook 2023-06-03 14:02:16 +01:00
Paul Makles a0002d0b43 feat: reintroduce permission checks for send 2023-06-03 14:00:17 +01:00
Paul Makles 23188032ca refactor(delta): port routes to core webhook models 2023-06-03 13:01:43 +01:00
Paul Makles f9f5a30e2c refactor(quark): strip webhook code 2023-06-03 13:01:28 +01:00
Paul Makles e393e17b59 feat(core/result): add rocket and okapi support 2023-06-03 13:01:12 +01:00
Paul Makles a29d8f6aab refactor(core/models): swap dependency order with db
feat(core/models): implement webhook model
2023-06-03 13:01:01 +01:00
Paul Makles 1dbbc3ed8d feat(core/database): implement webhook model 2023-06-03 13:00:05 +01:00
Paul Makles 410acb5b5c merge: branch 'master' into webhooks 2023-06-02 18:55:32 +01:00
Zomatree 175866ce00 Merge remote-tracking branch 'revoltchat/master' into webhooks 2023-04-25 20:38:09 +01:00
Zomatree 8c5de18991 restrict webhooks to text and group dm channels 2023-04-25 20:30:16 +01:00
Zomatree 57f00a283b make request function names the same as file name 2023-04-20 17:52:10 +01:00
Zomatree 312e1dd41f update code to be inline with reviews 2023-04-20 17:45:48 +01:00
Zomatree 426d836a09 Merge remote-tracking branch 'revoltchat/master' into webhooks 2023-04-18 19:50:04 +01:00
Zomatree 2fad66d24a only send a subsect of the webhook in a message 2023-04-18 19:39:34 +01:00
Zomatree 83429f9f65 fix broken merge conflict 2023-04-01 23:09:29 +01:00
Angelo Kontaxis a60ae78176 Merge branch 'master' into webhooks 2023-04-01 22:52:11 +01:00
Zomatree 7b39f4e9d9 change how webhook information is in the webhook 2023-04-01 22:49:34 +01:00
Zomatree 01577fd798 avoid leaking the token in events 2023-03-11 23:24:10 +00:00
Zomatree fca91e1560 update upstream code 2023-03-01 11:49:28 +00:00
Angelo Kontaxis 3c881a40a6 Merge branch 'master' into webhooks 2023-02-23 17:26:59 +00:00
Zomatree c7849a82bf Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-27 17:27:32 +00:00
Zomatree 781d51df6f add seperate non token webhook routes 2023-01-27 17:26:32 +00:00
Zomatree 758b1e43e7 no longer make mesage_send.rs public 2023-01-24 22:54:09 +00:00
Zomatree 5cc8231c36 deduplicate message sending 2023-01-24 22:52:18 +00:00
Zomatree 7b447faefa add pull request events 2023-01-21 22:57:12 +00:00
Zomatree a017256e95 add more events 2023-01-20 20:15:12 +00:00
Zomatree 1050891d87 Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-20 20:04:18 +00:00
Zomatree a246cff7f0 Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-18 21:03:15 +00:00
Zomatree c218db5047 add push event 2022-12-30 22:44:36 +00:00
Zomatree c7f2f3614f star event 2022-12-30 18:08:43 +00:00
Zomatree c826d8cf07 add github webhook support 2022-11-27 06:52:44 +00:00
Zomatree 5cb2320760 inital webhook support 2022-11-26 23:07:22 +00:00
209 changed files with 13973 additions and 2102 deletions
+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
+1
View File
@@ -4,3 +4,4 @@ target
.env .env
.vercel .vercel
.DS_Store
+4 -3
View File
@@ -1,4 +1,5 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy" "rust-analyzer.checkOnSave.command": "clippy",
} "nixEnvSelector.suggestion": false
}
Generated
+1038 -93
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -3,4 +3,5 @@ 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" }
+2 -1
View File
@@ -1,5 +1,5 @@
# Build Stage # Build Stage
FROM --platform="${BUILDPLATFORM}" rust:slim FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
USER 0:0 USER 0:0
WORKDIR /home/rust/src WORKDIR /home/rust/src
@@ -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)]
+13 -3
View File
@@ -3,13 +3,23 @@ disallowed-methods = [
"revolt_database::models::bots::model::Bot::remove_field", "revolt_database::models::bots::model::Bot::remove_field",
# 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.5.21" version = "0.6.8"
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.8"
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 }
+64
View File
@@ -0,0 +1,64 @@
[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_replies = 5
message_attachments = 5
message_embeds = 5
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
+162
View File
@@ -0,0 +1,162 @@
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::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_replies: usize,
pub message_attachments: usize,
pub message_embeds: 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;
}
}
+42 -9
View File
@@ -1,34 +1,51 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.0.2" version = "0.6.8"
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"]
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.0.2", path = "../result" } revolt-config = { version = "0.6.8", path = "../config" }
revolt-permissions = { version = "0.0.2", path = "../permissions", features = [ "serde" ] } revolt-result = { version = "0.6.8", path = "../result" }
revolt-models = { version = "0.6.8", path = "../models" }
revolt-presence = { version = "0.6.8", path = "../presence" }
revolt-permissions = { version = "0.6.8", path = "../permissions", features = [
"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"
@@ -36,12 +53,20 @@ revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] } iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
# Events
redis-kiss = { version = "0.1.4" }
# Database # Database
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"
@@ -50,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" }
+11
View File
@@ -65,3 +65,14 @@ impl DatabaseInfo {
}) })
} }
} }
impl From<Database> for authifier::Database {
fn from(value: Database) -> Self {
match value {
Database::Reference(_) => Default::default(),
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
}
}
}
+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()
} }
+12 -10
View File
@@ -2,27 +2,29 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex; use futures::lock::Mutex;
use crate::{AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings}; use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
MemberCompositeKey, Message, 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 emojis: Arc<Mutex<HashMap<String, Emoji>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,
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, ()>>>,
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
} }
); );
+269
View File
@@ -0,0 +1,269 @@
use authifier::AuthifierEvent;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, UserSettings,
Webhook,
};
use revolt_result::Error;
use crate::Database;
/// WebSocket Client Errors
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "error")]
pub enum WebSocketError {
LabelMe,
InternalError { at: String },
InvalidSession,
OnboardingNotFinished,
AlreadyAuthenticated,
MalformedData { msg: String },
}
/// Ping Packet
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Ping {
Binary(Vec<u8>),
Number(usize),
}
/// Untagged Error
#[derive(Serialize)]
#[serde(untagged)]
pub enum ErrorEvent {
Error(WebSocketError),
APIError(Error),
}
/// Protocol Events
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum EventV1 {
/// Multiple events
Bulk { v: Vec<EventV1> },
/// Successfully authenticated
Authenticated,
/* /// Basic data to cache
Ready {
users: Vec<User>,
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
emojis: Option<Vec<Emoji>>,
},
/// Ping response
Pong { data: Ping }, */
/// New message
Message(Message),
/// Update existing message
MessageUpdate {
id: String,
channel: String,
data: PartialMessage,
},
/// Append information to existing message
MessageAppend {
id: String,
channel: String,
append: AppendMessage,
},
/// Delete message
MessageDelete { id: String, channel: String },
/// New reaction to a message
MessageReact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove user's reaction from message
MessageUnreact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove a reaction from message
MessageRemoveReaction {
id: String,
channel_id: String,
emoji_id: String,
},
/// Bulk delete messages
BulkMessageDelete { channel: String, ids: Vec<String> },
/// New server
ServerCreate {
id: String,
server: Server,
channels: Vec<Channel>,
},
/// Update existing server
ServerUpdate {
id: String,
data: PartialServer,
clear: Vec<FieldsServer>,
},
/// Delete server
ServerDelete { id: String },
/// Update existing server member
ServerMemberUpdate {
id: MemberCompositeKey,
data: PartialMember,
clear: Vec<FieldsMember>,
},
/// User joins server
ServerMemberJoin { id: String, user: String },
/// User left server
ServerMemberLeave { id: String, user: String },
/// Server role created or updated
ServerRoleUpdate {
id: String,
role_id: String,
data: PartialRole,
clear: Vec<FieldsRole>,
},
/// Server role deleted
ServerRoleDelete { id: String, role_id: String },
/// Update existing user
UserUpdate {
id: String,
data: PartialUser,
clear: Vec<FieldsUser>,
event_id: Option<String>,
},
/*/// Relationship with another user changed
UserRelationship {
id: String,
user: User,
// ! this field can be deprecated
status: RelationshipStatus,
},*/
/// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings },
/*/// User has been platform banned or deleted their account
///
/// Clients should remove the following associated data:
/// - Messages
/// - DM Channels
/// - Relationships
/// - Server Memberships
///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 }, */
/// New emoji
EmojiCreate(Emoji),
/// Delete emoji
EmojiDelete { id: String },
/*/// New report
ReportCreate(Report), */
/// New channel
ChannelCreate(Channel),
/// Update existing channel
ChannelUpdate {
id: String,
data: PartialChannel,
clear: Vec<FieldsChannel>,
},
/// Delete channel
ChannelDelete { id: String },
/// User joins a group
ChannelGroupJoin { id: String, user: String },
/// User leaves a group
ChannelGroupLeave { id: String, user: String },
/// User started typing in a channel
ChannelStartTyping { id: String, user: String },
/// User stopped typing in a channel
ChannelStopTyping { id: String, user: String },
/// User acknowledged message in channel
ChannelAck {
id: String,
user: String,
message_id: String,
},
/// New webhook
WebhookCreate(Webhook),
/// Update existing webhook
WebhookUpdate {
id: String,
data: PartialWebhook,
remove: Vec<FieldsWebhook>,
},
/// Delete webhook
WebhookDelete { id: String },
/// Auth events
Auth(AuthifierEvent),
}
impl EventV1 {
/// Publish helper wrapper
pub async fn p(self, channel: String) {
#[cfg(not(debug_assertions))]
redis_kiss::p(channel, self).await;
#[cfg(debug_assertions)]
info!("Publishing event to {channel}: {self:?}");
#[cfg(debug_assertions)]
redis_kiss::publish(channel, self).await.unwrap();
}
/// Publish user event
pub async fn p_user(self, id: String, db: &Database) {
self.clone().p(id.clone()).await;
// ! FIXME: this should be captured by member list in the future
// ! and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members {
self.clone().p(member.id.server).await;
}
}
}
/// Publish private event
pub async fn private(self, id: String) {
self.p(format!("{id}!")).await;
}
/// Publish internal global event
pub async fn global(self) {
self.p("global".to_string()).await;
}
}
+1
View File
@@ -0,0 +1 @@
pub mod client;
+4 -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]
@@ -80,6 +80,9 @@ mod models;
pub mod util; pub mod util;
pub use models::*; pub use models::*;
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 {
!t !t
@@ -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,7 @@
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 +50,71 @@ 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));
}
// TODO: config
let max_bot_count = 5;
if db.get_number_of_bots_by_user(&owner.id).await? >= max_bot_count {
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())
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,175 @@
use revolt_result::Result;
use crate::events::client::EventV1;
use crate::{Database, File};
auto_derived_partial!(
/// Webhook
pub struct Webhook {
/// Webhook Id
#[serde(rename = "_id")]
pub id: String,
/// The name of the webhook
pub name: String,
/// The avatar of the webhook
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
/// The channel this webhook belongs to
pub channel_id: String,
/// The permissions of the webhook
pub permissions: u64,
/// The private token for the webhook
pub token: Option<String>,
},
"PartialWebhook"
);
auto_derived!(
/// Optional fields on webhook object
pub enum FieldsWebhook {
Avatar,
}
);
#[allow(clippy::derivable_impls)]
impl Default for Webhook {
fn default() -> Self {
Self {
id: Default::default(),
name: Default::default(),
avatar: None,
channel_id: Default::default(),
permissions: Default::default(),
token: Default::default(),
}
}
}
#[allow(clippy::disallowed_methods)]
impl Webhook {
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_webhook(self).await?;
// Avoid leaking the token to people who receive the event
let mut webhook = self.clone();
webhook.token = None;
EventV1::WebhookCreate(webhook.into())
.p(self.channel_id.clone())
.await;
Ok(())
}
pub fn assert_token(&self, token: &str) -> Result<()> {
if self.token.as_deref() == Some(token) {
Ok(())
} else {
Err(create_error!(InvalidCredentials))
}
}
pub async fn update(
&mut self,
db: &Database,
mut partial: PartialWebhook,
remove: Vec<FieldsWebhook>,
) -> Result<()> {
for field in &remove {
self.remove_field(field)
}
self.apply_options(partial.clone());
db.update_webhook(&self.id, &partial, &remove).await?;
partial.token = None; // Avoid leaking the token to people who receive the event
EventV1::WebhookUpdate {
id: self.id.clone(),
data: partial.into(),
remove: remove.into_iter().map(|v| v.into()).collect(),
}
.p(self.channel_id.clone())
.await;
Ok(())
}
pub fn remove_field(&mut self, field: &FieldsWebhook) {
match field {
FieldsWebhook::Avatar => self.avatar = None,
}
}
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_webhook(&self.id).await?;
EventV1::WebhookDelete {
id: self.id.clone(),
}
.p(self.channel_id.clone())
.await;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{FieldsWebhook, PartialWebhook, Webhook};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let webhook_id = "webhook";
let channel_id = "channel";
let webhook = Webhook {
id: webhook_id.to_string(),
name: "Webhook Name".to_string(),
channel_id: channel_id.to_string(),
avatar: None,
..Default::default()
};
db.insert_webhook(&webhook).await.unwrap();
let mut updated_webhook = webhook.clone();
updated_webhook
.update(
&db,
PartialWebhook {
name: Some("New Name".to_string()),
..Default::default()
},
vec![FieldsWebhook::Avatar],
)
.await
.unwrap();
let fetched_webhook = db.fetch_webhook(webhook_id).await.unwrap();
let fetched_webhooks = db.fetch_webhooks_for_channel(channel_id).await.unwrap();
assert_eq!(updated_webhook, fetched_webhook);
assert_ne!(webhook, fetched_webhook);
assert_eq!(1, fetched_webhooks.len());
assert_eq!(fetched_webhook, fetched_webhooks[0]);
webhook.delete(&db).await.unwrap();
assert!(db.fetch_webhook(webhook_id).await.is_err());
assert_eq!(
0,
db.fetch_webhooks_for_channel(channel_id)
.await
.unwrap()
.len()
)
});
}
}
@@ -0,0 +1,29 @@
use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractWebhooks: Sync + Send {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>;
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook>;
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>>;
/// Update webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()>;
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()>;
}
@@ -0,0 +1,77 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractWebhooks;
static COL: &str = "channel_webhooks";
#[async_trait]
impl AbstractWebhooks for MongoDb {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
query!(self, insert_one, COL, &webhook).map(|_| ())
}
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
query!(self, find_one_by_id, COL, webhook_id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
Ok(self
.col::<Webhook>(COL)
.find(
doc! {
"channel_id": channel_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 webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
webhook_id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, webhook_id).map(|_| ())
}
}
impl IntoDocumentPath for FieldsWebhook {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsWebhook::Avatar => "avatar",
})
}
}
@@ -0,0 +1,70 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
use super::AbstractWebhooks;
#[async_trait]
impl AbstractWebhooks for ReferenceDb {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if webhooks.contains_key(&webhook.id) {
Err(create_database_error!("insert", "webhook"))
} else {
webhooks.insert(webhook.id.to_string(), webhook.clone());
Ok(())
}
}
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
let webhooks = self.channel_webhooks.lock().await;
webhooks
.get(webhook_id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
let webhooks = self.channel_webhooks.lock().await;
Ok(webhooks
.values()
.filter(|webhook| webhook.channel_id == channel_id)
.cloned()
.collect())
}
/// Update webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if let Some(webhook) = webhooks.get_mut(webhook_id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
webhook.remove_field(field);
}
webhook.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if webhooks.remove(webhook_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,583 @@
use std::collections::HashMap;
use revolt_models::v0::MessageAuthor;
use revolt_permissions::OverrideField;
use revolt_result::Result;
use serde::{Deserialize, Serialize};
use crate::{events::client::EventV1, Database, File, IntoDocumentPath, SystemMessage, User};
auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages
SavedMessages {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the user this channel belongs to
user: String,
},
/// Direct message channel between two users
DirectMessage {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Whether this direct message channel is currently open on both sides
active: bool,
/// 2-tuple of user ids participating in direct message
recipients: Vec<String>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
},
/// Group channel between 1 or more participants
Group {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Display name of the channel
name: String,
/// User id of the owner of the group
owner: String,
/// Channel description
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Array of user ids participating in channel
recipients: Vec<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
/// Permissions assigned to members of this group
/// (does not apply to the owner of the group)
#[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<i64>,
/// Whether this group is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
/// Text channel belonging to a server
TextChannel {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Display name of the channel
name: String,
/// Channel description
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
/// Default permissions assigned to users in this channel
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
/// Voice channel belonging to a server
VoiceChannel {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Display name of the channel
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
/// Channel description
description: Option<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Default permissions assigned to users in this channel
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
}
);
auto_derived!(
#[derive(Default)]
pub struct PartialChannel {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role_permissions: Option<HashMap<String, OverrideField>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_permissions: Option<OverrideField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_message_id: Option<String>,
}
/// Optional fields on channel object
pub enum FieldsChannel {
Description,
Icon,
DefaultPermissions,
}
);
#[allow(clippy::disallowed_methods)]
impl Channel {
/// Create a channel
pub async fn create(&self, db: &Database) -> Result<()> {
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(())
}
/// Add user to a group
pub async fn add_user_to_group(
&mut self,
db: &Database,
user: &User,
by_id: &str,
) -> Result<()> {
if let Channel::Group { recipients, .. } = self {
if recipients.contains(&String::from(&user.id)) {
return Err(create_error!(AlreadyInGroup));
}
recipients.push(String::from(&user.id));
}
match &self {
Channel::Group { id, .. } => {
db.add_user_to_group(id, &user.id).await?;
EventV1::ChannelGroupJoin {
id: id.to_string(),
user: user.id.to_string(),
}
.p(id.to_string())
.await;
EventV1::ChannelCreate(self.clone().into())
.private(user.id.to_string())
.await;
SystemMessage::UserAdded {
id: user.id.to_string(),
by: by_id.to_string(),
}
.into_message(id.to_string())
.send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await
.ok();
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Map out whether it is a direct DM
pub fn is_direct_dm(&self) -> bool {
matches!(self, Channel::DirectMessage { .. })
}
/// Check whether has a user as a recipient
pub fn contains_user(&self, user_id: &str) -> bool {
match self {
Channel::Group { recipients, .. } => recipients.contains(&String::from(user_id)),
_ => false,
}
}
/// Get list of recipients
pub fn users(&self) -> Result<Vec<String>> {
match self {
Channel::Group { recipients, .. } => Ok(recipients.to_owned()),
_ => Err(create_error!(NotFound)),
}
}
/// Get a reference to this channel's id
pub fn id(&self) -> String {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id.clone(),
}
}
/// Set role permission on a channel
pub async fn set_role_permission(
&mut self,
db: &Database,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
match self {
Channel::TextChannel {
id,
server,
role_permissions,
..
}
| Channel::VoiceChannel {
id,
server,
role_permissions,
..
} => {
db.set_channel_role_permission(id, role_id, permissions)
.await?;
role_permissions.insert(role_id.to_string(), permissions);
EventV1::ChannelUpdate {
id: id.clone(),
data: PartialChannel {
role_permissions: Some(role_permissions.clone()),
..Default::default()
}
.into(),
clear: vec![],
}
.p(server.clone())
.await;
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Update channel data
pub async fn update(
&mut self,
db: &Database,
partial: PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
let id = self.id().to_string();
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(())
}
/// Remove a field from Channel object
pub fn remove_field(&mut self, field: &FieldsChannel) {
match field {
FieldsChannel::Description => match self {
Self::Group { description, .. }
| Self::TextChannel { description, .. }
| Self::VoiceChannel { description, .. } => {
description.take();
}
_ => {}
},
FieldsChannel::Icon => match self {
Self::Group { icon, .. }
| Self::TextChannel { icon, .. }
| Self::VoiceChannel { icon, .. } => {
icon.take();
}
_ => {}
},
FieldsChannel::DefaultPermissions => match self {
Self::TextChannel {
default_permissions,
..
}
| Self::VoiceChannel {
default_permissions,
..
} => {
default_permissions.take();
}
_ => {}
},
}
}
/// Remove multiple fields from Channel object
pub fn remove_fields(&mut self, partial: Vec<FieldsChannel>) {
for field in partial {
self.remove_field(&field)
}
}
/// Apply partial channel to channel
pub fn apply_options(&mut self, partial: PartialChannel) {
match self {
Self::SavedMessages { .. } => {}
Self::DirectMessage { active, .. } => {
if let Some(v) = partial.active {
*active = v;
}
}
Self::Group {
name,
owner,
description,
icon,
nsfw,
permissions,
..
} => {
if let Some(v) = partial.name {
*name = v;
}
if let Some(v) = partial.owner {
*owner = v;
}
if let Some(v) = partial.description {
description.replace(v);
}
if let Some(v) = partial.icon {
icon.replace(v);
}
if let Some(v) = partial.nsfw {
*nsfw = v;
}
if let Some(v) = partial.permissions {
permissions.replace(v);
}
}
Self::TextChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
..
}
| Self::VoiceChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
..
} => {
if let Some(v) = partial.name {
*name = v;
}
if let Some(v) = partial.description {
description.replace(v);
}
if let Some(v) = partial.icon {
icon.replace(v);
}
if let Some(v) = partial.nsfw {
*nsfw = v;
}
if let Some(v) = partial.role_permissions {
*role_permissions = v;
}
if let Some(v) = partial.default_permissions {
default_permissions.replace(v);
}
}
}
}
/// Remove user from a group
pub async fn remove_user_from_group(
&self,
db: &Database,
user: &User,
by_id: Option<&str>,
silent: bool,
) -> Result<()> {
match &self {
Channel::Group {
id,
name,
owner,
recipients,
..
} => {
if &user.id == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
db.update_channel(
id,
&PartialChannel {
owner: Some(new_owner.into()),
..Default::default()
},
vec![],
)
.await?;
SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(),
to: new_owner.to_string(),
}
.into_message(id.to_string())
.send(
db,
MessageAuthor::System {
username: name,
avatar: None,
},
self,
false,
)
.await
.ok();
} else {
db.delete_channel(self).await?;
return Ok(());
}
}
EventV1::ChannelGroupLeave {
id: id.to_string(),
user: user.id.to_string(),
}
.p(id.to_string())
.await;
if !silent {
if let Some(by) = by_id {
SystemMessage::UserRemove {
id: user.id.to_string(),
by: by.to_string(),
}
} else {
SystemMessage::UserLeft {
id: user.id.to_string(),
}
}
.into_message(id.to_string())
.send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await
.ok();
}
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Delete a channel
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
}
}
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsChannel::Description => "description",
FieldsChannel::Icon => "icon",
FieldsChannel::DefaultPermissions => "default_permissions",
})
}
}
@@ -0,0 +1,50 @@
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannels: Sync + Send {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()>;
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel>;
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>>;
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>>;
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel>;
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel>;
/// Insert a user to a group
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel_id: &str,
role_id: &str,
permissions: OverrideField,
) -> Result<()>;
// Update channel
async fn update_channel(
&self,
id: &str,
channel_id: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()>;
// Remove a user from a group
async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
// Delete a channel
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
}
@@ -0,0 +1,193 @@
use super::AbstractChannels;
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::Document;
use futures::StreamExt;
use revolt_permissions::OverrideField;
use revolt_result::Result;
static COL: &str = "channels";
#[async_trait]
impl AbstractChannels for MongoDb {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
query!(self, insert_one, COL, &channel).map(|_| ())
}
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
query!(self, find_one_by_id, COL, channel_id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
Ok(self
.col::<Channel>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("fetch", "channels"))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
query!(
self,
find,
COL,
doc! {
"$or": [
{
"$or": [
{
"channel_type": "DirectMessage"
},
{
"channel_type": "Group"
}
],
"recipients": user_id
},
{
"channel_type": "SavedMessages",
"user": user_id
}
]
}
)
}
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
query!(
self,
find_one,
COL,
doc! {
"channel_type": "SavedMessages",
"user": user_id
}
)?
.ok_or_else(|| create_error!(InternalError))
}
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
let doc = match (user_a, user_b) {
self_user if self_user.0 == self_user.1 => {
doc! {
"channel_type": "SavedMessages",
"user": self_user.0
}
}
users => {
doc! {
"channel_type": "DirectMessage",
"recipients": {
"$all": [ users.0, users.1 ]
}
}
}
};
query!(self, find_one, COL, doc)?.ok_or_else(|| create_error!(NotFound))
}
/// Insert a user to a group
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$push": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channel"))
}
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel: &str,
role: &str,
permissions: OverrideField,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! { "_id": channel },
doc! {
"$set": {
"role_permissions.".to_owned() + role: permissions
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channel"))
}
// Update channel
async fn update_channel(
&self,
id: &str,
channel: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
channel,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
// Remove a user from a group
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$pull": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channels"))
}
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
}
}
@@ -0,0 +1,157 @@
use std::collections::hash_map::Entry;
use super::AbstractChannels;
use crate::ReferenceDb;
use crate::{Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
use revolt_result::Result;
#[async_trait]
impl AbstractChannels for ReferenceDb {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
entry.insert(channel.clone());
Ok(())
} else {
Err(create_database_error!("insert", "channel"))
}
}
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
channels
.get(channel_id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
let channels = self.channels.lock().await;
ids.iter()
.map(|id| {
channels
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
}
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
let channels = self.channels.lock().await;
Ok(channels
.values()
.filter(|channel| channel.contains_user(user_id))
.cloned()
.collect())
}
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
channels
.get(user_id)
.cloned()
.ok_or_else(|| create_database_error!("fetch", "channel"))
}
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
for (_, data) in channels.iter() {
if data.contains_user(user_a) && data.contains_user(user_b) {
return Ok(data.to_owned());
}
}
Err(create_error!(NotFound))
}
/// Insert a user to a group
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(channel_id) {
recipients.push(String::from(user_id));
Ok(())
} else {
Err(create_error!(InvalidOperation))
}
}
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel_id: &str,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(mut channel) = channels.get_mut(channel_id) {
match &mut channel {
Channel::TextChannel {
role_permissions, ..
}
| Channel::VoiceChannel {
role_permissions, ..
} => {
if role_permissions.get(role_id).is_some() {
role_permissions.remove(role_id);
role_permissions.insert(String::from(role_id), permissions);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
_ => Err(create_error!(NotFound)),
}
} else {
Err(create_error!(NotFound))
}
}
// Update channel
async fn update_channel(
&self,
id: &str,
channel: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(channel_data) = channels.get_mut(id) {
channel_data.apply_options(channel.to_owned());
channel_data.remove_fields(remove);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
// Remove a user from a group
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(channel_data) = channels.get_mut(channel) {
if channel_data.users()?.contains(&String::from(user)) {
channel_data.users()?.retain(|x| x != user);
return Ok(());
} else {
return Err(create_error!(NotFound));
}
}
Err(create_error!(NotFound))
}
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if channels.remove(&channel.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,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
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,329 @@
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_models::v0::{Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification};
use revolt_result::Result;
use ulid::Ulid;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
Channel, Database, 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 {
/// 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(())
}
}
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 {
/// 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();
// TODO: sorting, etc
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(())
}
}
+24 -3
View File
@@ -1,7 +1,14 @@
mod admin_migrations; mod admin_migrations;
mod bots; mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks;
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;
@@ -9,8 +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 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::*;
@@ -23,8 +37,15 @@ pub trait AbstractDatabase:
+ Send + Send
+ admin_migrations::AbstractMigrations + admin_migrations::AbstractMigrations
+ bots::AbstractBots + bots::AbstractBots
+ channels::AbstractChannels
+ channel_invites::AbstractChannelInvites
+ channel_unreads::AbstractChannelUnreads
+ 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,28 @@
use std::time::Duration;
use super::AbstractRatelimitEvents;
use crate::RatelimitEvent;
use crate::RatelimitEventType;
use crate::ReferenceDb;
use revolt_result::Result;
#[async_trait]
impl AbstractRatelimitEvents for ReferenceDb {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, _event: &RatelimitEvent) -> Result<()> {
// TODO: implement
unimplemented!()
}
/// 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> {
// TODO: implement
unimplemented!()
}
}
@@ -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(|strike| strike.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, 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,102 @@ 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>> {
) -> Result<()> {
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 mut channels = vec![];
if true {
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)
Ok(())
}
/// Update member data /// Update member data
pub async fn update<'a>( pub async fn update<'a>(
&mut self, &mut self,
@@ -73,13 +171,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(())
} }
@@ -4,7 +4,7 @@ use revolt_permissions::OverrideField;
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use crate::{Database, File}; use crate::{events::client::EventV1, Database, File};
auto_derived_partial!( auto_derived_partial!(
/// Server /// Server
@@ -128,6 +128,7 @@ 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(&self, db: &Database) -> Result<()> {
@@ -149,24 +150,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
} }
@@ -364,14 +365,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 +395,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 +416,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
} }
@@ -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::*;
+219 -15
View File
@@ -1,6 +1,11 @@
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_result::{create_error, Error, ErrorType, Result};
use ulid::Ulid;
auto_derived_partial!( auto_derived_partial!(
/// # User /// # User
@@ -10,6 +15,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 +51,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 +117,202 @@ 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)
}
/// 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())
}
/// 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> {
@@ -137,13 +340,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
@@ -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!()
@@ -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) {
for _ in 0..WORKER_COUNT {
task::spawn(ack::worker(db.clone()));
task::spawn(last_message_id::worker(db.clone()));
task::spawn(process_embeds::worker(db.clone()));
task::spawn(web_push::worker(db.clone().into()));
}
}
/// Task 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));
}
// ! FIXME: 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
),
}
}
}
}
}
}
}
@@ -0,0 +1 @@
pub mod v0;
+772
View File
@@ -0,0 +1,772 @@
use revolt_models::v0::*;
impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot {
#[cfg(debug_assertions)]
assert_eq!(self.id, user.id);
PublicBot {
id: self.id,
username: user.username,
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
description: user
.profile
.map(|profile| profile.content)
.unwrap_or_default(),
}
}
}
impl From<crate::Bot> for Bot {
fn from(value: crate::Bot) -> Self {
Bot {
id: value.id,
owner_id: value.owner,
token: value.token,
public: value.public,
analytics: value.analytics,
discoverable: value.discoverable,
interactions_url: value.interactions_url,
terms_of_service_url: value.terms_of_service_url,
privacy_policy_url: value.privacy_policy_url,
flags: value.flags.unwrap_or_default() as u32,
}
}
}
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 {
fn from(value: crate::Webhook) -> Self {
Webhook {
id: value.id,
name: value.name,
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
permissions: value.permissions,
}
}
}
impl From<crate::PartialWebhook> for PartialWebhook {
fn from(value: crate::PartialWebhook) -> Self {
PartialWebhook {
id: value.id,
name: value.name,
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
permissions: value.permissions,
}
}
}
impl From<FieldsWebhook> for crate::FieldsWebhook {
fn from(_value: FieldsWebhook) -> Self {
Self::Avatar
}
}
impl From<crate::FieldsWebhook> for FieldsWebhook {
fn from(_value: crate::FieldsWebhook) -> Self {
Self::Avatar
}
}
impl From<crate::Channel> for Channel {
fn from(value: crate::Channel) -> Self {
match value {
crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user },
crate::Channel::DirectMessage {
id,
active,
recipients,
last_message_id,
} => Channel::DirectMessage {
id,
active,
recipients,
last_message_id,
},
crate::Channel::Group {
id,
name,
owner,
description,
recipients,
icon,
last_message_id,
permissions,
nsfw,
} => Channel::Group {
id,
name,
owner,
description,
recipients,
icon: icon.map(|file| file.into()),
last_message_id,
permissions,
nsfw,
},
crate::Channel::TextChannel {
id,
server,
name,
description,
icon,
last_message_id,
default_permissions,
role_permissions,
nsfw,
} => Channel::TextChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
last_message_id,
default_permissions,
role_permissions,
nsfw,
},
crate::Channel::VoiceChannel {
id,
server,
name,
description,
icon,
default_permissions,
role_permissions,
nsfw,
} => Channel::VoiceChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
nsfw,
},
}
}
}
impl From<crate::PartialChannel> for PartialChannel {
fn from(value: crate::PartialChannel) -> Self {
PartialChannel {
name: value.name,
owner: value.owner,
description: value.description,
icon: value.icon.map(|file| file.into()),
nsfw: value.nsfw,
active: value.active,
permissions: value.permissions,
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
}
}
}
impl From<FieldsChannel> for crate::FieldsChannel {
fn from(value: FieldsChannel) -> Self {
match value {
FieldsChannel::Description => crate::FieldsChannel::Description,
FieldsChannel::Icon => crate::FieldsChannel::Icon,
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
}
}
}
impl From<crate::FieldsChannel> for FieldsChannel {
fn from(value: crate::FieldsChannel) -> Self {
match value {
crate::FieldsChannel::Description => FieldsChannel::Description,
crate::FieldsChannel::Icon => FieldsChannel::Icon,
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
}
}
}
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<crate::File> for File {
fn from(value: crate::File) -> Self {
File {
id: value.id,
tag: value.tag,
filename: value.filename,
metadata: value.metadata.into(),
content_type: value.content_type,
size: value.size,
deleted: value.deleted,
reported: value.reported,
message_id: value.message_id,
user_id: value.user_id,
server_id: value.server_id,
object_id: value.object_id,
}
}
}
impl From<crate::Metadata> for Metadata {
fn from(value: crate::Metadata) -> Self {
match value {
crate::Metadata::File => Metadata::File,
crate::Metadata::Text => Metadata::Text,
crate::Metadata::Image { width, height } => Metadata::Image {
width: width as usize,
height: height as usize,
},
crate::Metadata::Video { width, height } => Metadata::Video {
width: width as usize,
height: height as usize,
},
crate::Metadata::Audio => Metadata::Audio,
}
}
}
impl From<crate::Message> for Message {
fn from(value: crate::Message) -> Self {
Message {
id: value.id,
nonce: value.nonce,
channel: value.channel,
author: value.author,
webhook: value.webhook,
content: value.content,
system: value.system.map(|system| system.into()),
attachments: value
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions,
interactions: value.interactions.into(),
masquerade: value.masquerade.map(|masq| masq.into()),
}
}
}
impl From<crate::PartialMessage> for PartialMessage {
fn from(value: crate::PartialMessage) -> Self {
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,
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,
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<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<P>(self, perspective: P) -> User
where
P: Into<Option<crate::User>>,
{
let relationship = if let Some(perspective) = perspective.into() {
perspective
.relations
.unwrap_or_default()
.into_iter()
.find(|relationship| relationship.id == self.id)
.map(|relationship| relationship.status.into())
.unwrap_or_default()
} else {
RelationshipStatus::None
};
// do permission stuff here
// TODO: implement permissions =)
let can_see_profile = false;
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
status: None,
profile: None,
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship,
online: can_see_profile && revolt_presence::is_online(&self.id).await,
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 {
fn from(value: crate::RelationshipStatus) -> Self {
match value {
crate::RelationshipStatus::None => RelationshipStatus::None,
crate::RelationshipStatus::User => RelationshipStatus::User,
crate::RelationshipStatus::Friend => RelationshipStatus::Friend,
crate::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
crate::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
crate::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
crate::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther,
}
}
}
impl From<crate::Relationship> for Relationship {
fn from(value: crate::Relationship) -> Self {
Self {
user_id: value.id,
status: value.status.into(),
}
}
}
impl From<crate::Presence> for Presence {
fn from(value: crate::Presence) -> Self {
match value {
crate::Presence::Online => Presence::Online,
crate::Presence::Idle => Presence::Idle,
crate::Presence::Focus => Presence::Focus,
crate::Presence::Busy => Presence::Busy,
crate::Presence::Invisible => Presence::Invisible,
}
}
}
impl From<crate::UserStatus> for UserStatus {
fn from(value: crate::UserStatus) -> Self {
UserStatus {
text: value.text,
presence: value.presence.map(|presence| presence.into()),
}
}
}
impl From<crate::UserProfile> for UserProfile {
fn from(value: crate::UserProfile) -> Self {
UserProfile {
content: value.content,
background: value.background.map(|file| file.into()),
}
}
}
impl From<crate::BotInformation> for BotInformation {
fn from(value: crate::BotInformation) -> Self {
BotInformation {
owner_id: value.owner,
}
}
}
@@ -1,6 +1,9 @@
use crate::{Error, Result}; use std::num::NonZeroUsize;
use revolt_result::{create_error, Error, Result};
use async_std::sync::Mutex; use async_std::sync::Mutex;
use once_cell::sync::Lazy;
use revolt_rocket_okapi::gen::OpenApiGenerator; use revolt_rocket_okapi::gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue}; use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue};
@@ -8,16 +11,14 @@ use rocket::http::Status;
use rocket::request::{FromRequest, Outcome}; use rocket::request::{FromRequest, Outcome};
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec}; use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate;
use once_cell::sync::Lazy;
#[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 +27,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(), ());
@@ -81,14 +82,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(), ());
+2
View File
@@ -1,2 +1,4 @@
pub mod bridge;
pub mod idempotency;
pub mod permissions; pub mod permissions;
pub mod reference; pub mod reference;
+246 -31
View File
@@ -4,18 +4,19 @@ use revolt_permissions::{
calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus, calculate_user_permissions, ChannelType, Override, PermissionQuery, 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<u32>,
@@ -23,7 +24,7 @@ pub struct PermissionCalculator<'a> {
} }
#[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?
@@ -81,85 +82,275 @@ 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(user) = &self.user {
false // TODO: cache result?
matches!(
self.perspective
.has_mutual_connection(self.database, &user.id)
.await,
Ok(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 {
self.database
.fetch_member(&server.id, &self.perspective.id)
.await
.is_ok()
}
} 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 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_user_permission: None, cached_user_permission: None,
cached_permission: None, cached_permission: None,
@@ -167,7 +358,7 @@ impl<'a> PermissionCalculator<'a> {
} }
/// 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 +367,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 +383,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)
} }
+6 -1
View File
@@ -7,7 +7,7 @@ use schemars::{
JsonSchema, JsonSchema,
}; };
use crate::{Bot, Database}; use crate::{Bot, Database, Webhook};
/// Reference to some object in the database /// Reference to some object in the database
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -26,6 +26,11 @@ impl Reference {
pub async fn as_bot(&self, db: &Database) -> Result<Bot> { pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await db.fetch_bot(&self.id).await
} }
/// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await
}
} }
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
+20 -11
View File
@@ -1,29 +1,38 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.0.2" version = "0.6.8"
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"]
schemas = [ "dep:schemars" ] schemas = ["dep:schemars", "revolt-permissions/schemas"]
from_database = [ "revolt-database", "revolt-presence" ] validator = ["dep:validator"]
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
redis-is-patched = [ "revolt-presence/redis-is-patched" ] default = ["serde", "partials"]
default = [ "serde", "from_database" ]
[dependencies] [dependencies]
# Repo # Core
revolt-database = { version = "0.0.2", path = "../database", optional = true } revolt-config = { version = "0.6.8", path = "../config" }
revolt-presence = { version = "0.0.2", path = "../presence", optional = true } revolt-permissions = { version = "0.6.8", 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 }
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 }
# Validation
validator = { version = "0.16.0", optional = true, features = ["derive"] }
+29
View File
@@ -6,6 +6,13 @@ extern crate serde;
#[macro_use] #[macro_use]
extern crate schemars; extern crate schemars;
#[cfg(feature = "partials")]
#[macro_use]
extern crate revolt_optional_struct;
#[cfg(feature = "validator")]
pub use validator;
macro_rules! auto_derived { macro_rules! auto_derived {
( $( $item:item )+ ) => { ( $( $item:item )+ ) => {
$( $(
@@ -17,6 +24,28 @@ macro_rules! auto_derived {
}; };
} }
#[cfg(feature = "partials")]
macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => {
#[derive(
OptionalStruct, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema,
)]
#[optional_derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[optional_name = $name]
#[opt_skip_serializing_none]
#[opt_some_priority]
$item
};
}
#[cfg(not(feature = "partials"))]
macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => {
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
$item
};
}
pub mod v0; pub mod v0;
/// Utility function to check if a boolean value is false /// Utility function to check if a boolean value is false
@@ -1,39 +0,0 @@
auto_derived!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[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,
}
);
#[cfg(feature = "from_database")]
impl From<revolt_database::AccountStrike> for AccountStrike {
fn from(value: revolt_database::AccountStrike) -> Self {
AccountStrike {
id: value.id,
user_id: value.user_id,
reason: value.reason,
}
}
}
+88 -45
View File
@@ -1,14 +1,17 @@
use super::User; use super::User;
use validator::Validate;
auto_derived!( auto_derived!(
/// Bot /// Bot
#[derive(Default)]
pub struct Bot { pub struct Bot {
/// Bot Id /// Bot Id
#[serde(rename = "_id")] #[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String, pub id: String,
/// User Id of the bot owner /// User Id of the bot owner
#[serde(rename = "owner")] #[cfg_attr(feature = "serde", serde(rename = "owner"))]
pub owner_id: String, pub owner_id: String,
/// Token used to authenticate requests for this bot /// Token used to authenticate requests for this bot
pub token: String, pub token: String,
@@ -55,6 +58,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,17 +74,23 @@ 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"))]
id: String, pub id: String,
/// Bot Username /// Bot Username
username: String, pub username: String,
/// Profile Avatar /// Profile Avatar
#[serde(skip_serializing_if = "String::is_empty")] #[cfg_attr(
avatar: String, feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub avatar: String,
/// Profile Description /// Profile Description
#[serde(skip_serializing_if = "String::is_empty")] #[cfg_attr(
description: String, feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub description: String,
} }
/// Bot Response /// Bot Response
@@ -85,40 +100,68 @@ auto_derived!(
/// User object /// User object
pub user: User, pub user: User,
} }
/// Bot Details
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(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(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>,
}
); );
#[cfg(feature = "from_database")]
impl PublicBot {
pub fn from(bot: revolt_database::Bot, user: revolt_database::User) -> Self {
#[cfg(debug_assertions)]
assert_eq!(bot.id, user.id);
PublicBot {
id: bot.id,
username: user.username,
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
description: user
.profile
.map(|profile| profile.content)
.unwrap_or_default(),
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::Bot> for Bot {
fn from(value: revolt_database::Bot) -> Self {
Bot {
id: value.id,
owner_id: value.owner,
token: value.token,
public: value.public,
analytics: value.analytics,
discoverable: value.discoverable,
interactions_url: value.interactions_url,
terms_of_service_url: value.terms_of_service_url,
privacy_policy_url: value.privacy_policy_url,
flags: value.flags.unwrap_or_default() as u32,
}
}
}
@@ -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,
}
);
@@ -0,0 +1,100 @@
use super::File;
auto_derived_partial!(
/// Webhook
pub struct Webhook {
/// Webhook Id
pub id: String,
/// The name of the webhook
pub name: String,
/// The avatar of the webhook
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub avatar: Option<File>,
/// The channel this webhook belongs to
pub channel_id: String,
/// The permissions for the webhook
pub permissions: u64,
/// The private token for the webhook
pub token: Option<String>,
},
"PartialWebhook"
);
auto_derived!(
/// Information about the webhook bundled with Message
pub struct MessageWebhook {
// The name of the webhook - 1 to 32 chars
pub name: String,
// The id of the avatar of the webhook, if it has one
pub avatar: Option<String>,
}
/// New webhook information
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataEditWebhook {
/// Webhook name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: Option<String>,
/// Avatar ID
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub avatar: Option<String>,
/// Webhook permissions
pub permissions: Option<u64>,
/// Fields to remove from webhook
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsWebhook>,
}
/// Webhook information
pub struct ResponseWebhook {
/// Webhook Id
pub id: String,
/// Webhook name
pub name: String,
/// Avatar ID
pub avatar: Option<String>,
/// The channel this webhook belongs to
pub channel_id: String,
/// The permissions for the webhook
pub permissions: u64
}
/// Optional fields on webhook object
pub enum FieldsWebhook {
Avatar,
}
);
impl From<Webhook> for MessageWebhook {
fn from(value: Webhook) -> Self {
MessageWebhook {
name: value.name,
avatar: value.avatar.map(|file| file.id),
}
}
}
impl From<Webhook> for ResponseWebhook {
fn from(value: Webhook) -> Self {
ResponseWebhook {
id: value.id,
name: value.name,
avatar: value.avatar.map(|file| file.id),
channel_id: value.channel_id,
permissions: value.permissions
}
}
}
+208
View File
@@ -0,0 +1,208 @@
use super::File;
use revolt_permissions::OverrideField;
use std::collections::HashMap;
auto_derived!(
/// Channel
pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages
SavedMessages {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
id: String,
/// Id of the user this channel belongs to
user: String,
},
/// Direct message channel between two users
DirectMessage {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
id: String,
/// Whether this direct message channel is currently open on both sides
active: bool,
/// 2-tuple of user ids participating in direct message
recipients: Vec<String>,
/// Id of the last message sent in this channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
last_message_id: Option<String>,
},
/// Group channel between 1 or more participants
Group {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
id: String,
/// Display name of the channel
name: String,
/// User id of the owner of the group
owner: String,
/// Channel description
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
description: Option<String>,
/// Array of user ids participating in channel
recipients: Vec<String>,
/// Custom icon attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
icon: Option<File>,
/// Id of the last message sent in this channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
last_message_id: Option<String>,
/// Permissions assigned to members of this group
/// (does not apply to the owner of the group)
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
permissions: Option<i64>,
/// Whether this group is marked as not safe for work
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
nsfw: bool,
},
/// Text channel belonging to a server
TextChannel {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Display name of the channel
name: String,
/// Channel description
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
description: Option<String>,
/// Custom icon attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
icon: Option<File>,
/// Id of the last message sent in this channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
last_message_id: Option<String>,
/// Default permissions assigned to users in this channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[cfg_attr(
feature = "serde",
serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
nsfw: bool,
},
/// Voice channel belonging to a server
VoiceChannel {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Display name of the channel
name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
/// Channel description
description: Option<String>,
/// Custom icon attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
icon: Option<File>,
/// Default permissions assigned to users in this channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[cfg_attr(
feature = "serde",
serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
nsfw: bool,
},
}
/// Partial representation of a channel
#[derive(Default)]
pub struct PartialChannel {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub name: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub owner: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub icon: Option<File>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub nsfw: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub active: Option<bool>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub permissions: Option<i64>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub role_permissions: Option<HashMap<String, OverrideField>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub default_permissions: Option<OverrideField>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub last_message_id: Option<String>,
}
/// Optional fields on channel object
pub enum FieldsChannel {
Description,
Icon,
DefaultPermissions,
}
/// New webhook information
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataEditChannel {
/// Channel name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: Option<String>,
/// Channel description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Group owner
pub owner: Option<String>,
/// Icon
///
/// Provide an Autumn attachment Id.
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub icon: Option<String>,
/// Whether this channel is age-restricted
pub nsfw: Option<bool>,
/// Whether this channel is archived
pub archived: Option<bool>,
/// Fields to remove from channel
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Option<Vec<FieldsChannel>>,
}
);
+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,
}
);
+33
View File
@@ -0,0 +1,33 @@
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,
}
);

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