Compare commits

...
Author SHA1 Message Date
Paul Makles 9929f7499a refactor(core): use core crates for sending messages 2023-09-03 16:31:39 +01:00
Paul Makles 279d9ef1b5 refactor(core): remove quark references from webhook routes 2023-09-03 15:52:18 +01:00
Paul Makles 5a9bb9e68d fix: convert index map/set to hash map/set
fix: idempotency token should use correct crate cfg
2023-08-27 15:51:20 +01:00
Paul Makles a8fc9e928a fix: import validator crate directly in derive 2023-08-27 15:45:23 +01:00
Paul Makles da231ae390 fix: remove indexmap from models 2023-08-27 15:43:57 +01:00
Paul Makles 7a31da4189 fix: add indexmap/serde to model depends 2023-08-27 15:40:05 +01:00
Paul Makles 7d0b5392b5 fix: should include crate in build script too 2023-08-27 15:33:28 +01:00
Paul Makles 1e1d44c0e4 fix: add config crate to Dockerfile
chore: bump release version
2023-08-27 15:30:53 +01:00
Paul Makles 1c4588967f merge: branch 'insert/dev-branch' 2023-08-27 15:18:52 +01:00
Paul Makles 853366a297 feat: add support for Firebase Cloud Messaging 2023-08-27 15:18:19 +01:00
Paul Makles 7568f49755 test: add tests to bot routes 2023-08-27 15:01:22 +01:00
Paul Makles 0542788567 feat(delta): test harness for web server 2023-08-27 13:01:21 +01:00
Paul Makles 73f7b8f007 feat: ensure bot invites work correctly 2023-08-27 12:21:48 +01:00
Paul Makles c2412190a8 refactor: solve clippy hints 2023-08-27 11:23:02 +01:00
Paul Makles 5372296dc0 feat(core): implement message tasks
includes:
- acknowledgements
- last_message_id
- embeds
- web push
2023-08-27 11:17:03 +01:00
Paul Makles 41a47bdf8f feat: add revolt_config crate 2023-08-27 11:15:20 +01:00
Paul Makles 51c26e324d refactor: remove quark/web 2023-08-26 16:19:00 +01:00
Paul Makles 3a55d00c6a chore: disable nix environment extension 2023-08-26 16:17:21 +01:00
Paul Makles 004e84ece9 fix: revoke all channel permissions if can't view 2023-08-26 16:17:20 +01:00
Paul Makles 3d0914ce52 chore: add nix shell configuration 2023-08-26 16:17:19 +01:00
Paul Makles bde432cb75 chore: strip bot model from quark 2023-08-26 16:16:48 +01:00
Paul Makles 28d225952a chore: add separate debug Docker build script 2023-08-26 15:34:25 +01:00
Paul Makles 2fb9e7a802 merge: branch 'master' into insert/dev-branch 2023-08-26 13:34:44 +01:00
Paul Makles 431046cb4c chore: cut new release with redis patches and debug profile 2023-08-26 13:33:05 +01:00
Zomatree 92032cd9bc chore: Log errors from failed pub/sub messages 2023-08-26 13:14:24 +01:00
Lea bfdd713d38 fix: allow fetching mutuals for bots 2023-08-26 13:14:15 +01:00
Paul Makles 430df03b20 chore: bump sentry crate 2023-08-10 09:42:44 +01:00
Paul Makles f5f70287e7 feat: add SENTRY_DSN env variable 2023-08-10 09:40:41 +01:00
Paul Makles d5ba4ebf0c feat(core/database): implement permissions backend 2023-08-05 17:42:23 +01:00
Paul Makles 3bfead4ff1 feat(core/database): system message helper 2023-08-05 17:03:16 +01:00
Paul Makles d04621b1d6 feat(core): implement message db methods 2023-08-05 16:59:58 +01:00
Paul Makles a681df04bd feat(core): permissions query, finish bots impl 2023-08-05 16:14:47 +01:00
Paul Makles 9f3c1036d0 chore: refine user and bot creation methods 2023-08-05 12:06:43 +01:00
Paul Makles c9011ac692 chore: migrate bots create route 2023-08-05 11:24:02 +01:00
Paul Makles 42f977f536 feat: start work on migrating routes 2023-08-03 22:11:48 +01:00
Paul Makles a0580c5f5a chore: remove legacy query route 2023-08-03 19:43:05 +01:00
Paul Makles 121a9cd87c feat(core): implement message model
closes #273
2023-08-03 19:32:47 +01:00
Paul Makles d87d608d9e chore(core): flesh out implementations for channels / servers 2023-08-03 18:27:13 +01:00
Paul Makles bd9ecc1519 fix: missing conversion 2023-08-02 00:31:19 +01:00
Paul Makles f5a113da47 feat(core): implement server ban
closes #274
2023-08-01 21:19:07 +01:00
Paul Makles 11fdb0c1dc feat(core): implement emoji model
closes #270
2023-08-01 20:45:58 +01:00
Paul Makles e0033ceb12 feat(core): implement channel unreads model
closes #269
2023-08-01 17:41:43 +01:00
Paul Makles 7318ec6ef6 chore(core/models): add cfg_attr where necessary 2023-08-01 15:54:56 +01:00
Paul Makles a516c7adcf feat(core): implement channel invite model
closes #268
2023-08-01 15:53:43 +01:00
Paul Makles c48109ca66 docs: add note for 'Id' casing 2023-08-01 15:51:57 +01:00
Paul Makles f0ef781cd7 chore: add MSRV to README [skip ci]
closes #254
2023-08-01 15:04:45 +01:00
Paul Makles d9dd040db4 feat(core/models): implement server members model
closes #280
2023-08-01 15:00:07 +01:00
Paul Makles fec947b4c8 feat(core/models): implement server model
closes #279
2023-08-01 14:55:25 +01:00
Paul Makles ec6df36c25 chore: strip legacy admin API 2023-08-01 14:35:36 +01:00
Paul Makles 76150db293 chore: add prometheus metrics 2023-07-04 17:52:40 +01:00
Paul Makles 815c3fe99e chore: cut a new release (0.6.4) 2023-07-02 08:45:39 +01:00
Zomatree 54878e8e8d feat: add webhook permissions 2023-07-02 08:41:38 +01:00
TheBobBobs 6e4798f1d4 fix(bonfire): populate cache for calculating permissions (#259) 2023-07-02 08:40:15 +01:00
TheBobBobs 55bd6fb087 fix: incorrect owner rank 2023-07-02 08:35:49 +01:00
Paul Makles 49035f4817 feat: ratelimit user edit route and discriminator changes 2023-06-15 19:24:53 +01:00
Paul Makles c0ebaa0bd3 chore: include discriminator in ban list response 2023-06-15 19:23:47 +01:00
Paul Makles b98b244fc3 fix: enable staging for API spec generation 2023-06-15 15:21:00 +01:00
179 changed files with 9784 additions and 1816 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
+991 -73
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" }
+1
View File
@@ -20,6 +20,7 @@ COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/ COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/ COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/quark/Cargo.toml ./crates/quark/ COPY crates/quark/Cargo.toml ./crates/quark/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/ COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/ COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
+29
View File
@@ -0,0 +1,29 @@
# Build Stage
FROM rust:1.70.0-slim
USER 0:0
WORKDIR /home/rust/src
# Install build requirements
RUN apt-get update && \
apt-get install -y \
make \
pkg-config \
libssl-dev
COPY scripts/build-image-layer.sh /tmp/
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/quark/Cargo.toml ./crates/quark/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps
COPY crates ./crates
RUN sh /tmp/build-image-layer.sh apps
+4
View File
@@ -21,6 +21,10 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers) - [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing) - [Contribution Guide](https://developers.revolt.chat/contributing)
## Minimum Supported Rust Version
Rust 1.70 or higher.
## Contributing ## Contributing
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing). The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
+6
View File
@@ -2,6 +2,10 @@
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project. Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
## Writing Style
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
## `core/database` crate ## `core/database` crate
w.r.t. `model.rs` files w.r.t. `model.rs` files
@@ -22,6 +26,7 @@ w.r.t. `model.rs` files
); );
``` ```
- `auto_derived!` macro accepts multiple entries and should be used as such: - `auto_derived!` macro accepts multiple entries and should be used as such:
```rust ```rust
auto_derived!( auto_derived!(
/// Optional fields on server object /// Optional fields on server object
@@ -31,6 +36,7 @@ w.r.t. `model.rs` files
pub enum FieldsRole { .. } pub enum FieldsRole { .. }
); );
``` ```
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root: - If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
```rust ```rust
#[serde(skip_serializing_if = "crate::if_false", default)] #[serde(skip_serializing_if = "crate::if_false", default)]
+14 -3
View File
@@ -1,15 +1,26 @@
disallowed-methods = [ disallowed-methods = [
# Shouldn't need to access these directly # Shouldn't need to access these directly
"revolt_database::models::bots::model::Bot::remove_field", "revolt_database::models::bots::model::Bot::remove_field",
"revolt_database::models::messages::model::Message::attach_sendable_embed",
# Prefer to use Object::create() # Prefer to use Object::create()
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::insert_account_strike", "revolt_database::models::bots::ops::AbstractBots::insert_bot",
"revolt_database::models::channel_invites::ops::AbstractChannelInvites::insert_invite",
"revolt_database::models::channel_unreads::ops::AbstractChannelUnreads::acknowledge_message",
"revolt_database::models::channel_webhooks::ops::AbstractWebhooks::insert_webhook",
"revolt_database::models::channels::ops::AbstractChannels::insert_channel",
"revolt_database::models::emojis::ops::AbstractEmojis::insert_emoji",
"revolt_database::models::files::ops::AbstractAttachments::insert_attachment",
"revolt_database::models::messages::ops::AbstractMessages::insert_message",
"revolt_database::models::ratelimit_events::ops::AbstractRatelimitEvents::insert_ratelimit_event",
"revolt_database::models::server_bans::ops::AbstractServerBans::insert_ban",
"revolt_database::models::server_members::ops::AbstractServerMembers::insert_member",
"revolt_database::models::servers::ops::AbstractServers::insert_server",
"revolt_database::models::users::ops::AbstractUsers::insert_user",
# Prefer to use Object::update(&self) # Prefer to use Object::update(&self)
"revolt_database::models::bots::ops::AbstractBots::update_bot", "revolt_database::models::bots::ops::AbstractBots::update_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
# Prefer to use Object::delete(&self) # Prefer to use Object::delete(&self)
"revolt_database::models::bots::ops::AbstractBots::delete_bot", "revolt_database::models::bots::ops::AbstractBots::delete_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
] ]
+8 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.6.1" 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 }
+66
View File
@@ -0,0 +1,66 @@
[database]
mongodb = "mongodb://database"
redis = "redis://redis/"
[hosts]
app = "http://local.revolt.chat"
api = "http://local.revolt.chat/api"
events = "ws://local.revolt.chat/ws"
autumn = "http://local.revolt.chat/autumn"
january = "http://local.revolt.chat/january"
voso_legacy = ""
voso_legacy_ws = ""
[api]
staging = false
[api.registration]
invite_only = false
[api.smtp]
host = ""
username = ""
password = ""
from_address = ""
[api.vapid]
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
[api.fcm]
api_key = ""
[api.security]
authifier_shield_key = ""
voso_legacy_token = ""
[api.security.captcha]
hcaptcha_key = ""
hcaptcha_sitekey = ""
[api.workers]
max_concurrent_connections = 50
[features]
[features.limits]
[features.limits.default]
group_size = 100
bots = 5
message_length = 2000
message_embeds = 5
message_replies = 5
message_attachments = 5
message_reactions = 20
servers = 100
server_emoji = 100
server_roles = 200
server_channels = 200
attachment_size = 20000000
avatar_size = 4000000
background_size = 6000000
icon_size = 2500000
banner_size = 6000000
emoji_size = 500000
+164
View File
@@ -0,0 +1,164 @@
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_length: usize,
pub message_replies: usize,
pub message_attachments: usize,
pub message_embeds: usize,
pub message_reactions: usize,
pub servers: usize,
pub server_emoji: usize,
pub server_roles: usize,
pub server_channels: usize,
pub attachment_size: usize,
pub avatar_size: usize,
pub background_size: usize,
pub icon_size: usize,
pub banner_size: usize,
pub emoji_size: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimitsCollection {
pub default: FeaturesLimits,
#[serde(flatten)]
pub roles: HashMap<String, FeaturesLimits>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Features {
pub limits: FeaturesLimitsCollection,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Settings {
pub database: Database,
pub hosts: Hosts,
pub api: Api,
pub features: Features,
}
pub async fn init() {
println!(
":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m",
config().await
);
}
pub async fn read() -> Config {
CONFIG_BUILDER.read().await.clone()
}
#[cached(time = 30)]
pub async fn config() -> Settings {
read().await.try_deserialize::<Settings>().unwrap()
}
#[cfg(feature = "test")]
#[cfg(test)]
mod tests {
use crate::init;
#[async_std::test]
async fn it_works() {
init().await;
}
}
+34 -12
View File
@@ -1,38 +1,51 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.6.1" 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"]
redis-is-patched = [ "revolt-presence/redis-is-patched" ] rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
# Default Features # Default Features
default = [ "mongodb", "async-std-runtime" ] default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.6.1", path = "../result" } revolt-config = { version = "0.6.8", path = "../config" }
revolt-models = { version = "0.6.1", path = "../models" } revolt-result = { version = "0.6.8", path = "../result" }
revolt-presence = { version = "0.6.1", path = "../presence" } revolt-models = { version = "0.6.8", path = "../models" }
revolt-permissions = { version = "0.6.1", path = "../permissions", features = [ "serde", "bson" ] } 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" 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"
@@ -53,6 +66,7 @@ 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"
@@ -61,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()
} }
+9 -10
View File
@@ -3,29 +3,28 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex; use futures::lock::Mutex;
use crate::{ use crate::{
AccountStrike, Bot, Channel, File, Member, MemberCompositeKey, Server, User, UserSettings, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
Webhook, MemberCompositeKey, Message, Server, ServerBan, User, UserSettings, Webhook,
}; };
database_derived!( database_derived!(
/// Reference implementation /// Reference implementation
#[derive(Default)] #[derive(Default)]
pub struct ReferenceDb { pub struct ReferenceDb {
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>, pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>, pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>, pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>, pub users: Arc<Mutex<HashMap<String, User>>>,
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>, pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>, pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub server_bans: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>, pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>, pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
pub emoji: Arc<Mutex<HashMap<String, ()>>>,
pub messages: Arc<Mutex<HashMap<String, ()>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
} }
); );
+10 -10
View File
@@ -2,7 +2,10 @@ use authifier::AuthifierEvent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revolt_models::v0::{ use revolt_models::v0::{
Channel, FieldsChannel, FieldsWebhook, PartialChannel, PartialWebhook, Webhook, AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, UserSettings,
Webhook,
}; };
use revolt_result::Error; use revolt_result::Error;
@@ -55,8 +58,7 @@ pub enum EventV1 {
}, },
/// Ping response /// Ping response
Pong { data: Ping }, Pong { data: Ping }, */
/// New message /// New message
Message(Message), Message(Message),
@@ -152,18 +154,17 @@ pub enum EventV1 {
event_id: Option<String>, event_id: Option<String>,
}, },
/// Relationship with another user changed /*/// Relationship with another user changed
UserRelationship { UserRelationship {
id: String, id: String,
user: User, user: User,
// ! this field can be deprecated // ! this field can be deprecated
status: RelationshipStatus, status: RelationshipStatus,
}, },*/
/// Settings updated remotely /// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings }, UserSettingsUpdate { id: String, update: UserSettings },
/// User has been platform banned or deleted their account /*/// User has been platform banned or deleted their account
/// ///
/// Clients should remove the following associated data: /// Clients should remove the following associated data:
/// - Messages /// - Messages
@@ -172,15 +173,14 @@ pub enum EventV1 {
/// - Server Memberships /// - Server Memberships
/// ///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear. /// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 }, UserPlatformWipe { user_id: String, flags: i32 }, */
/// New emoji /// New emoji
EmojiCreate(Emoji), EmojiCreate(Emoji),
/// Delete emoji /// Delete emoji
EmojiDelete { id: String }, EmojiDelete { id: String },
/// New report /*/// New report
ReportCreate(Report), */ ReportCreate(Report), */
/// New channel /// New channel
ChannelCreate(Channel), ChannelCreate(Channel),
+2 -1
View File
@@ -43,7 +43,7 @@ macro_rules! auto_derived {
macro_rules! auto_derived_partial { macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => { ( $item:item, $name:expr ) => {
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
#[optional_name = $name] #[optional_name = $name]
#[opt_skip_serializing_none] #[opt_skip_serializing_none]
@@ -81,6 +81,7 @@ pub mod util;
pub use models::*; pub use models::*;
pub mod events; pub mod events;
pub mod tasks;
/// Utility function to check if a boolean value is false /// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool { pub fn if_false(t: &bool) -> bool {
@@ -76,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()
@@ -209,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.");
} }
@@ -9,6 +9,7 @@ use crate::{
}; };
use futures::StreamExt; use futures::StreamExt;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@@ -18,7 +19,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 25; 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");
@@ -696,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)
@@ -707,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 {
@@ -945,10 +935,56 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
) )
.await .await
.expect("Failed to create username index."); .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())
}
}
@@ -20,6 +20,9 @@ auto_derived_partial!(
/// The channel this webhook belongs to /// The channel this webhook belongs to
pub channel_id: String, pub channel_id: String,
/// The permissions of the webhook
pub permissions: u64,
/// The private token for the webhook /// The private token for the webhook
pub token: Option<String>, pub token: Option<String>,
}, },
@@ -33,6 +36,20 @@ auto_derived!(
} }
); );
#[allow(clippy::derivable_impls)]
impl Default for Webhook {
fn default() -> Self {
Self {
id: Default::default(),
name: Default::default(),
avatar: None,
channel_id: Default::default(),
permissions: Default::default(),
token: Default::default(),
}
}
}
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
impl Webhook { impl Webhook {
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(&self, db: &Database) -> Result<()> {
@@ -117,7 +134,7 @@ mod tests {
id: webhook_id.to_string(), id: webhook_id.to_string(),
name: "Webhook Name".to_string(), name: "Webhook Name".to_string(),
channel_id: channel_id.to_string(), channel_id: channel_id.to_string(),
avatar: Some(Default::default()), avatar: None,
..Default::default() ..Default::default()
}; };
@@ -1,12 +1,14 @@
use std::collections::HashMap; use std::collections::HashMap;
use revolt_models::v0::MessageAuthor;
use revolt_permissions::OverrideField; use revolt_permissions::OverrideField;
use revolt_result::Result; use revolt_result::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{events::client::EventV1, Database, File, IntoDocumentPath}; use crate::{events::client::EventV1, Database, File, IntoDocumentPath, SystemMessage, User};
auto_derived!( auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel { pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages /// Personal "Saved Notes" channel which allows users to save messages
SavedMessages { SavedMessages {
@@ -164,11 +166,25 @@ auto_derived!(
} }
); );
#[allow(clippy::disallowed_methods)]
impl Channel { impl Channel {
/// Create a channel /// Create a channel
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_channel(self).await?; db.insert_channel(self).await?;
let event = EventV1::ChannelCreate(self.clone().into());
match self {
Self::SavedMessages { user, .. } => event.private(user.clone()).await,
Self::DirectMessage { recipients, .. } | Self::Group { recipients, .. } => {
for recipient in recipients {
event.clone().private(recipient.clone()).await;
}
}
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => {
event.p(server.clone()).await;
}
}
Ok(()) Ok(())
} }
@@ -176,40 +192,48 @@ impl Channel {
pub async fn add_user_to_group( pub async fn add_user_to_group(
&mut self, &mut self,
db: &Database, db: &Database,
user_id: &str, user: &User,
_by_id: &str, by_id: &str,
) -> Result<()> { ) -> Result<()> {
if let Channel::Group { recipients, .. } = self { if let Channel::Group { recipients, .. } = self {
if recipients.contains(&String::from(user_id)) { if recipients.contains(&String::from(&user.id)) {
return Err(create_error!(AlreadyInGroup)); return Err(create_error!(AlreadyInGroup));
} }
recipients.push(String::from(user_id)); recipients.push(String::from(&user.id));
} }
match &self { match &self {
Channel::Group { id, .. } => { Channel::Group { id, .. } => {
db.add_user_to_group(id, user_id).await?; db.add_user_to_group(id, &user.id).await?;
EventV1::ChannelGroupJoin { EventV1::ChannelGroupJoin {
id: id.to_string(), id: id.to_string(),
user: user_id.to_string(), user: user.id.to_string(),
} }
.p(id.to_string()) .p(id.to_string())
.await; .await;
EventV1::ChannelCreate(self.clone().into()) EventV1::ChannelCreate(self.clone().into())
.private(user_id.to_string()) .private(user.id.to_string())
.await; .await;
/* TODO: SystemMessage::UserAdded { SystemMessage::UserAdded {
id: user.to_string(), id: user.id.to_string(),
by: by.to_string(), by: by_id.to_string(),
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await .await
.ok(); */ .ok();
Ok(()) Ok(())
} }
@@ -293,7 +317,7 @@ impl Channel {
} }
/// Update channel data /// Update channel data
pub async fn update<'a>( pub async fn update(
&mut self, &mut self,
db: &Database, db: &Database,
partial: PartialChannel, partial: PartialChannel,
@@ -305,8 +329,19 @@ impl Channel {
self.apply_options(partial.clone()); self.apply_options(partial.clone());
db.update_channel(&self.id(), &partial, remove.clone()) let id = self.id().to_string();
.await?; db.update_channel(&id, &partial, remove.clone()).await?;
EventV1::ChannelUpdate {
id: id.clone(),
data: partial.into(),
clear: remove.into_iter().map(|v| v.into()).collect(),
}
.p(match self {
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
_ => id,
})
.await;
Ok(()) Ok(())
} }
@@ -444,19 +479,20 @@ impl Channel {
pub async fn remove_user_from_group( pub async fn remove_user_from_group(
&self, &self,
db: &Database, db: &Database,
user_id: &str, user: &User,
_by_id: Option<&str>, by_id: Option<&str>,
silent: bool, silent: bool,
) -> Result<()> { ) -> Result<()> {
match &self { match &self {
Channel::Group { Channel::Group {
id, id,
name,
owner, owner,
recipients, recipients,
.. ..
} => { } => {
if user_id == owner { if &user.id == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != user_id) { if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
db.update_channel( db.update_channel(
id, id,
&PartialChannel { &PartialChannel {
@@ -467,14 +503,22 @@ impl Channel {
) )
.await?; .await?;
/* TODO: SystemMessage::ChannelOwnershipChanged { SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(), from: owner.to_string(),
to: new_owner.into(), to: new_owner.to_string(),
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: name,
avatar: None,
},
self,
false,
)
.await .await
.ok(); */ .ok();
} else { } else {
db.delete_channel(self).await?; db.delete_channel(self).await?;
return Ok(()); return Ok(());
@@ -483,26 +527,34 @@ impl Channel {
EventV1::ChannelGroupLeave { EventV1::ChannelGroupLeave {
id: id.to_string(), id: id.to_string(),
user: user_id.to_string(), user: user.id.to_string(),
} }
.p(id.to_string()) .p(id.to_string())
.await; .await;
if !silent { if !silent {
/* TODO: if let Some(_by) = by_id { if let Some(by) = by_id {
SystemMessage::UserRemove { SystemMessage::UserRemove {
id: user_id.to_string(), id: user.id.to_string(),
by: by.to_string(), by: by.to_string(),
} }
} else { } else {
SystemMessage::UserLeft { SystemMessage::UserLeft {
id: user_id.to_string(), id: user.id.to_string(),
} }
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await .await
.ok(); */ .ok();
} }
Ok(()) Ok(())
@@ -514,6 +566,8 @@ impl Channel {
/// Delete a channel /// Delete a channel
pub async fn delete(&self, db: &Database) -> Result<()> { pub async fn delete(&self, db: &Database) -> Result<()> {
let id = self.id().to_string();
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
db.delete_channel(self).await db.delete_channel(self).await
} }
} }
@@ -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,572 @@
use std::collections::HashSet;
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_models::v0::{
self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification,
ReplyIntent, SendableEmbed, RE_MENTION,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
use ulid::Ulid;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::idempotency::IdempotencyKey,
Channel, Database, Emoji, File,
};
auto_derived_partial!(
/// Message
pub struct Message {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Unique value generated by client sending this message
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
/// Id of the channel this message was sent in
pub channel: String,
/// Id of the user or webhook that sent this message
pub author: String,
/// The webhook that sent this message
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook: Option<MessageWebhook>,
/// Message content
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// System message
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemMessage>,
/// Array of attachments
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<File>>,
/// Time at which this message was last edited
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<Timestamp>,
/// Attached embeds to this message
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
/// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
/// Hashmap of emoji IDs to array of user IDs
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub reactions: IndexMap<String, IndexSet<String>>,
/// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,
},
"PartialMessage"
);
auto_derived!(
/// System Event
#[serde(tag = "type")]
pub enum SystemMessage {
#[serde(rename = "text")]
Text { content: String },
#[serde(rename = "user_added")]
UserAdded { id: String, by: String },
#[serde(rename = "user_remove")]
UserRemove { id: String, by: String },
#[serde(rename = "user_joined")]
UserJoined { id: String },
#[serde(rename = "user_left")]
UserLeft { id: String },
#[serde(rename = "user_kicked")]
UserKicked { id: String },
#[serde(rename = "user_banned")]
UserBanned { id: String },
#[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String },
#[serde(rename = "channel_description_changed")]
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
ChannelOwnershipChanged { from: String, to: String },
}
/// Name and / or avatar override information
pub struct Masquerade {
/// Replace the display name shown on this message
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Replace the avatar shown on this message (URL to image file)
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>,
/// Replace the display role colour shown on this message
///
/// Must have `ManageRole` permission to use
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
}
/// Information to guide interactions on this message
#[derive(Default)]
pub struct Interactions {
/// Reactions which should always appear and be distinct
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reactions: Option<IndexSet<String>>,
/// Whether reactions should be restricted to the given list
///
/// Can only be set to true if reactions list is of at least length 1
#[serde(skip_serializing_if = "crate::if_false", default)]
pub restrict_reactions: bool,
}
/// Appended Information
pub struct AppendMessage {
/// Additional embeds to include in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
}
/// Message Time Period
///
/// Filter and sort messages by time
#[serde(untagged)]
pub enum MessageTimePeriod {
Relative {
/// Message id to search around
///
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
/// It will also take half of limit rounded as the limits to each side.
/// It also fetches the message ID specified.
nearby: String,
},
Absolute {
/// Message id before which messages should be fetched
before: Option<String>,
/// Message id after which messages should be fetched
after: Option<String>,
/// Message sort direction
sort: Option<MessageSort>,
},
}
/// Message Filter
pub struct MessageFilter {
/// Parent channel ID
pub channel: Option<String>,
/// Message author ID
pub author: Option<String>,
/// Search query
pub query: Option<String>,
}
/// Message Query
pub struct MessageQuery {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
pub limit: Option<i64>,
/// Filter to apply
#[serde(flatten)]
pub filter: MessageFilter,
/// Time period to fetch
#[serde(flatten)]
pub time_period: MessageTimePeriod,
}
);
#[allow(clippy::derivable_impls)]
impl Default for Message {
fn default() -> Self {
Self {
id: Default::default(),
nonce: None,
channel: Default::default(),
author: Default::default(),
webhook: None,
content: None,
system: None,
attachments: None,
edited: None,
embeds: None,
mentions: None,
replies: None,
reactions: Default::default(),
interactions: Default::default(),
masquerade: None,
}
}
}
#[allow(clippy::disallowed_methods)]
impl Message {
/// Create message from API data
pub async fn create_from_api(
db: &Database,
channel: Channel,
data: DataMessageSend,
author: MessageAuthor<'_>,
mut idempotency: IdempotencyKey,
generate_embeds: bool,
) -> Result<Message> {
let config = config().await;
Message::validate_sum(
&data.content,
data.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
)?;
idempotency
.consume_nonce(data.nonce)
.await
.map_err(|_| create_error!(InvalidOperation))?;
// Check the message is not empty
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
{
return Err(create_error!(EmptyMessage));
}
// Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
let disallowed = if let Some(list) = &interactions.reactions {
list.is_empty()
} else {
true
};
if disallowed {
return Err(create_error!(InvalidProperty));
}
}
}
let (author_id, webhook) = match &author {
MessageAuthor::User(user) => (user.id.clone(), None),
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
MessageAuthor::System { .. } => ("00000000000000000000000000".to_string(), None),
};
// Start constructing the message
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id.clone(),
channel: channel.id(),
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
interactions: data
.interactions
.map(|interactions| interactions.into())
.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
..Default::default()
};
// Parse mentions in message.
let mut mentions = HashSet::new();
if let Some(content) = &data.content {
for capture in RE_MENTION.captures_iter(content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
}
}
// Verify replies are valid.
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > config.features.limits.default.message_replies {
return Err(create_error!(TooManyReplies {
max: config.features.limits.default.message_replies,
}));
}
for ReplyIntent { id, mention } in entries {
let message = db.fetch_message(&id).await?;
if mention {
mentions.insert(message.author.to_owned());
}
replies.insert(message.id);
}
}
if !mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect());
}
if !replies.is_empty() {
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
// Add attachments to message.
let mut attachments = vec![];
if data
.attachments
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
{
return Err(create_error!(TooManyAttachments {
max: config.features.limits.default.message_attachments,
}));
}
if data
.embeds
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
{
return Err(create_error!(TooManyEmbeds {
max: config.features.limits.default.message_embeds,
}));
}
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
attachments.push(
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
.await?,
);
}
if !attachments.is_empty() {
message.attachments.replace(attachments);
}
// Process included embeds.
for sendable_embed in data.embeds.unwrap_or_default() {
message.attach_sendable_embed(db, sendable_embed).await?;
}
// Set content
message.content = data.content;
// Pass-through nonce value for clients
message.nonce = Some(idempotency.into_key());
// Send the message
message.send(db, author, &channel, generate_embeds).await?;
Ok(message)
}
/// Send a message without any notifications
pub async fn send_without_notifications(
&mut self,
db: &Database,
is_dm: bool,
generate_embeds: bool,
) -> Result<()> {
db.insert_message(self).await?;
// Fan out events
EventV1::Message(self.clone().into())
.p(self.channel.to_string())
.await;
// Update last_message_id
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
// Add mentions for affected users
if let Some(mentions) = &self.mentions {
for user in mentions {
tasks::ack::queue(
self.channel.to_string(),
user.to_string(),
AckEvent::AddMention {
ids: vec![self.id.to_string()],
},
)
.await;
}
}
// Generate embeds
if generate_embeds {
if let Some(content) = &self.content {
tasks::process_embeds::queue(
self.channel.to_string(),
self.id.to_string(),
content.clone(),
)
.await;
}
}
Ok(())
}
/// Send a message
pub async fn send(
&mut self,
db: &Database,
author: MessageAuthor<'_>,
channel: &Channel,
generate_embeds: bool,
) -> Result<()> {
self.send_without_notifications(
db,
matches!(channel, Channel::DirectMessage { .. }),
generate_embeds,
)
.await?;
// Push out Web Push notifications
crate::tasks::web_push::queue(
{
match channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => recipients.clone(),
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
_ => vec![],
}
},
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
)
.await;
Ok(())
}
/// Append content to message
pub async fn append(
db: &Database,
id: String,
channel: String,
append: AppendMessage,
) -> Result<()> {
db.append_message(&id, &append).await?;
EventV1::MessageAppend {
id,
channel: channel.to_string(),
append: append.into(),
}
.p(channel)
.await;
Ok(())
}
/// Convert sendable embed to text embed and attach to message
pub async fn attach_sendable_embed(
&mut self,
db: &Database,
embed: v0::SendableEmbed,
) -> Result<()> {
let media: Option<v0::File> = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
.await?
.into(),
)
} else {
None
};
let embed = v0::Embed::Text(v0::Text {
icon_url: embed.icon_url,
url: embed.url,
title: embed.title,
description: embed.description,
media,
colour: embed.colour,
});
if let Some(embeds) = &mut self.embeds {
embeds.push(embed);
} else {
self.embeds = Some(vec![embed]);
}
Ok(())
}
/// Validate the sum of content of a message is under threshold
pub fn validate_sum(
content: &Option<String>,
embeds: &[SendableEmbed],
max_length: usize,
) -> Result<()> {
let mut running_total = 0;
if let Some(content) = content {
running_total += content.len();
}
for embed in embeds {
if let Some(desc) = &embed.description {
running_total += desc.len();
}
}
if running_total <= max_length {
Ok(())
} else {
Err(create_error!(PayloadTooLarge))
}
}
}
impl SystemMessage {
pub fn into_message(self, channel: String) -> Message {
Message {
id: Ulid::new().to_string(),
channel,
author: "00000000000000000000000000".to_string(),
system: Some(self),
..Default::default()
}
}
}
impl Interactions {
/// Validate interactions info is correct
pub async fn validate(&self, db: &Database, permissions: &PermissionValue) -> Result<()> {
let config = config().await;
if let Some(reactions) = &self.reactions {
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
if reactions.len() > config.features.limits.default.message_reactions {
return Err(create_error!(InvalidOperation));
}
for reaction in reactions {
if !Emoji::can_use(db, reaction).await? {
return Err(create_error!(InvalidOperation));
}
}
}
Ok(())
}
/// Check if we can use a given emoji to react
pub fn can_use(&self, emoji: &str) -> bool {
if self.restrict_reactions {
if let Some(reactions) = &self.reactions {
reactions.contains(emoji)
} else {
false
}
} else {
true
}
}
/// Check if default initialisation of fields
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}
@@ -0,0 +1,39 @@
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractMessages: Sync + Send {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()>;
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message>;
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()>;
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>;
}
@@ -0,0 +1,280 @@
use bson::{to_bson, Document};
use futures::try_join;
use mongodb::options::FindOptions;
use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage};
use super::AbstractMessages;
static COL: &str = "messages";
#[async_trait]
impl AbstractMessages for MongoDb {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()> {
query!(self, insert_one, COL, &message).map(|_| ())
}
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
let mut filter = doc! {};
// 1. Apply message filters
if let Some(channel) = query.filter.channel {
filter.insert("channel", channel);
}
if let Some(author) = query.filter.author {
filter.insert("author", author);
}
let is_search_query = if let Some(query) = query.filter.query {
filter.insert(
"$text",
doc! {
"$search": query
},
);
true
} else {
false
};
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
// 3. Apply message time period
match query.time_period {
MessageTimePeriod::Relative { nearby } => {
// 3.1. Prepare filters
let mut older_message_filter = filter.clone();
let mut newer_message_filter = filter;
older_message_filter.insert(
"_id",
doc! {
"$lt": &nearby
},
);
newer_message_filter.insert(
"_id",
doc! {
"$gte": &nearby
},
);
// 3.2. Execute in both directions
let (a, b) = try_join!(
self.find_with_options::<_, Message>(
COL,
newer_message_filter,
FindOptions::builder()
.limit(limit / 2 + 1)
.sort(doc! {
"_id": 1_i32
})
.build(),
),
self.find_with_options::<_, Message>(
COL,
older_message_filter,
FindOptions::builder()
.limit(limit / 2)
.sort(doc! {
"_id": -1_i32
})
.build(),
)
)
.map_err(|_| create_database_error!("find", COL))?;
Ok([a, b].concat())
}
MessageTimePeriod::Absolute {
before,
after,
sort,
} => {
// 3.1. Apply message ID filter
if let Some(doc) = match (before, after) {
(Some(before), Some(after)) => Some(doc! {
"$lt": before,
"$gt": after
}),
(Some(before), _) => Some(doc! {
"$lt": before
}),
(_, Some(after)) => Some(doc! {
"$gt": after
}),
_ => None,
} {
filter.insert("_id", doc);
}
// 3.2. Execute with given message sort
self.find_with_options(
COL,
filter,
FindOptions::builder()
.limit(limit)
.sort(match sort.unwrap_or(MessageSort::Latest) {
// Sort by relevance, fallback to latest
MessageSort::Relevance => {
if is_search_query {
doc! {
"score": {
"$meta": "textScore"
}
}
} else {
doc! {
"_id": -1_i32
}
}
}
// Sort by latest first
MessageSort::Latest => doc! {
"_id": -1_i32
},
// Sort by oldest first
MessageSort::Oldest => doc! {
"_id": 1_i32
},
})
.build(),
)
.await
.map_err(|_| create_database_error!("find", COL))
}
}
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
}
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
let mut query = doc! {};
if let Some(embeds) = &append.embeds {
if !embeds.is_empty() {
query.insert(
"$push",
doc! {
"embeds": {
"$each": to_bson(embeds)
.map_err(|_| create_database_error!("to_bson", "embeds"))?
}
},
);
}
}
if query.is_empty() {
return Ok(());
}
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
query,
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$addToSet": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$pull": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$unset": {
format!("reactions.{emoji}"): 1
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.delete_many(
doc! {
"channel": channel,
"_id": {
"$in": ids
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL))
}
}
@@ -0,0 +1,272 @@
use indexmap::IndexSet;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
use super::AbstractMessages;
#[async_trait]
impl AbstractMessages for ReferenceDb {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()> {
let mut messages = self.messages.lock().await;
if messages.contains_key(&message.id) {
Err(create_database_error!("insert", "message"))
} else {
messages.insert(message.id.to_string(), message.clone());
Ok(())
}
}
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message> {
let messages = self.messages.lock().await;
messages
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
let messages = self.messages.lock().await;
let matched_messages = messages
.values()
.filter(|message| {
if let Some(channel) = &query.filter.channel {
if &message.channel != channel {
return false;
}
}
if let Some(author) = &query.filter.author {
if &message.author != author {
return false;
}
}
if let Some(query) = &query.filter.query {
if let Some(content) = &message.content {
if !content.to_lowercase().contains(query) {
return false;
}
} else {
return false;
}
}
true
})
.cloned()
.collect();
// 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(())
}
}
+18 -3
View File
@@ -1,9 +1,14 @@
mod admin_migrations; mod admin_migrations;
mod bots; mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks; mod channel_webhooks;
mod channels; mod channels;
mod emojis;
mod files; mod files;
mod safety_strikes; mod messages;
mod ratelimit_events;
mod server_bans;
mod server_members; mod server_members;
mod servers; mod servers;
mod user_settings; mod user_settings;
@@ -11,10 +16,15 @@ mod users;
pub use admin_migrations::*; pub use admin_migrations::*;
pub use bots::*; pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
pub use channel_webhooks::*; pub use channel_webhooks::*;
pub use channels::*; pub use channels::*;
pub use emojis::*;
pub use files::*; pub use files::*;
pub use safety_strikes::*; pub use messages::*;
pub use ratelimit_events::*;
pub use server_bans::*;
pub use server_members::*; pub use server_members::*;
pub use servers::*; pub use servers::*;
pub use user_settings::*; pub use user_settings::*;
@@ -28,9 +38,14 @@ pub trait AbstractDatabase:
+ admin_migrations::AbstractMigrations + admin_migrations::AbstractMigrations
+ bots::AbstractBots + bots::AbstractBots
+ channels::AbstractChannels + channels::AbstractChannels
+ channel_invites::AbstractChannelInvites
+ channel_unreads::AbstractChannelUnreads
+ channel_webhooks::AbstractWebhooks + channel_webhooks::AbstractWebhooks
+ emojis::AbstractEmojis
+ files::AbstractAttachments + files::AbstractAttachments
+ safety_strikes::AbstractAccountStrikes + messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents
+ server_bans::AbstractServerBans
+ server_members::AbstractServerMembers + server_members::AbstractServerMembers
+ servers::AbstractServers + servers::AbstractServers
+ user_settings::AbstractUserSettings + user_settings::AbstractUserSettings
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,47 @@
use std::fmt;
use revolt_result::Result;
use ulid::Ulid;
use crate::Database;
auto_derived!(
/// Ratelimit Event
pub struct RatelimitEvent {
/// Id
#[serde(rename = "_id")]
pub id: String,
/// Relevant Object Id
pub target_id: String,
/// Type of event
pub event_type: RatelimitEventType,
}
/// Event type
pub enum RatelimitEventType {
DiscriminatorChange,
}
);
impl fmt::Display for RatelimitEventType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
#[allow(clippy::disallowed_methods)]
impl RatelimitEvent {
/// Create ratelimit event
pub async fn create(
db: &Database,
target_id: String,
event_type: RatelimitEventType,
) -> Result<()> {
db.insert_ratelimit_event(&RatelimitEvent {
id: Ulid::new().to_string(),
target_id,
event_type,
})
.await
}
}
@@ -0,0 +1,20 @@
use std::time::Duration;
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractRatelimitEvents: Sync + Send {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()>;
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool>;
}
@@ -0,0 +1,40 @@
use std::time::{Duration, SystemTime};
use super::AbstractRatelimitEvents;
use crate::{MongoDb, RatelimitEvent, RatelimitEventType};
use revolt_result::Result;
use ulid::Ulid;
static COL: &str = "ratelimit_events";
#[async_trait]
impl AbstractRatelimitEvents for MongoDb {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
query!(self, insert_one, COL, &event).map(|_| ())
}
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool> {
self.col::<RatelimitEvent>(COL)
.count_documents(
doc! {
"_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
"target_id": target_id,
"event_type": event_type.to_string()
},
None,
)
.await
.map(|c| c as usize >= count)
.map_err(|_| create_database_error!("count_documents", COL))
}
}
@@ -0,0 +1,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(|entry| entry.id == strike.id)
.unwrap();
assert_eq!(fetched_strike, updated_strike);
assert_ne!(fetched_strike, strike);
strike.delete(&db).await.unwrap();
assert_eq!(
1,
db.fetch_account_strikes_by_user(user_id)
.await
.unwrap()
.len()
)
});
}
}
@@ -1,24 +0,0 @@
use revolt_result::Result;
use crate::{AccountStrike, PartialAccountStrike};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAccountStrikes: Sync + Send {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()>;
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike>;
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>>;
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()>;
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()>;
}
@@ -1,55 +0,0 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::MongoDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
static COL: &str = "safety_strikes";
#[async_trait]
impl AbstractAccountStrikes for MongoDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
query!(self, insert_one, COL, &strike).map(|_| ())
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
Ok(self
.col::<AccountStrike>(COL)
.find(
doc! {
"user_id": user_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
@@ -1,60 +0,0 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
#[async_trait]
impl AbstractAccountStrikes for ReferenceDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.contains_key(&strike.id) {
Err(create_database_error!("insert", "strike"))
} else {
strikes.insert(strike.id.to_string(), strike.clone());
Ok(())
}
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
let strikes = self.account_strikes.lock().await;
strikes
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
let strikes = self.account_strikes.lock().await;
Ok(strikes
.values()
.filter(|strike| strike.user_id == user_id)
.cloned()
.collect())
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if let Some(strike) = strikes.get_mut(id) {
strike.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,12 @@
use crate::MemberCompositeKey;
auto_derived!(
/// Server Ban
pub struct ServerBan {
/// Unique member id
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Reason for ban creation
pub reason: Option<String>,
}
);
@@ -0,0 +1,21 @@
use revolt_result::Result;
use crate::{MemberCompositeKey, ServerBan};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractServerBans: Sync + Send {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()>;
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan>;
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>>;
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()>;
}
@@ -0,0 +1,56 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::{MemberCompositeKey, ServerBan};
use super::AbstractServerBans;
static COL: &str = "server_bans";
#[async_trait]
impl AbstractServerBans for MongoDb {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
query!(self, insert_one, COL, &ban).map(|_| ())
}
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
query!(
self,
find_one,
COL,
doc! {
"_id.server": server_id,
"_id.user": user_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
query!(
self,
find,
COL,
doc! {
"_id.server": server_id
}
)
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
query!(
self,
delete_one,
COL,
doc! {
"_id.server": &id.server,
"_id.user": &id.user
}
)
.map(|_| ())
}
}
@@ -0,0 +1,52 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{MemberCompositeKey, ServerBan};
use super::AbstractServerBans;
#[async_trait]
impl AbstractServerBans for ReferenceDb {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
let mut server_bans = self.server_bans.lock().await;
if server_bans.contains_key(&ban.id) {
Err(create_database_error!("insert", "ban"))
} else {
server_bans.insert(ban.id.clone(), ban.clone());
Ok(())
}
}
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
let server_bans = self.server_bans.lock().await;
server_bans
.get(&MemberCompositeKey {
server: server_id.to_string(),
user: user_id.to_string(),
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
let server_bans = self.server_bans.lock().await;
Ok(server_bans
.values()
.filter(|member| member.id.server == server_id)
.cloned()
.collect())
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_bans = self.server_bans.lock().await;
if server_bans.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -1,7 +1,11 @@
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
use revolt_result::Result; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use crate::{Database, File, Server}; use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, 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::*;
+212 -30
View File
@@ -1,9 +1,11 @@
use std::collections::HashSet; use std::{collections::HashSet, time::Duration};
use crate::{Database, File}; use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use revolt_result::{Error, ErrorType, Result}; use rand::seq::SliceRandom;
use revolt_result::{create_error, Error, ErrorType, Result};
use ulid::Ulid;
auto_derived_partial!( auto_derived_partial!(
/// # User /// # User
@@ -49,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,
@@ -106,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> {
@@ -145,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(())
} }
@@ -203,17 +399,3 @@ impl User {
.await .await
} }
} }
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()
});
@@ -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
),
}
}
}
}
}
}
}
+495 -19
View File
@@ -1,15 +1,5 @@
use revolt_models::v0::*; use revolt_models::v0::*;
impl From<crate::AccountStrike> for AccountStrike {
fn from(value: crate::AccountStrike) -> Self {
AccountStrike {
id: value.id,
user_id: value.user_id,
reason: value.reason,
}
}
}
impl crate::Bot { impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot { pub fn into_public_bot(self, user: crate::User) -> PublicBot {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -44,6 +34,70 @@ impl From<crate::Bot> for Bot {
} }
} }
impl From<FieldsBot> for crate::FieldsBot {
fn from(value: FieldsBot) -> Self {
match value {
FieldsBot::InteractionsURL => crate::FieldsBot::InteractionsURL,
FieldsBot::Token => crate::FieldsBot::Token,
}
}
}
impl From<crate::FieldsBot> for FieldsBot {
fn from(value: crate::FieldsBot) -> Self {
match value {
crate::FieldsBot::InteractionsURL => FieldsBot::InteractionsURL,
crate::FieldsBot::Token => FieldsBot::Token,
}
}
}
impl From<crate::Invite> for Invite {
fn from(value: crate::Invite) -> Self {
match value {
crate::Invite::Group {
code,
creator,
channel,
} => Invite::Group {
code,
creator,
channel,
},
crate::Invite::Server {
code,
server,
creator,
channel,
} => Invite::Server {
code,
server,
creator,
channel,
},
}
}
}
impl From<crate::ChannelUnread> for ChannelUnread {
fn from(value: crate::ChannelUnread) -> Self {
ChannelUnread {
id: value.id.into(),
last_id: value.last_id,
mentions: value.mentions.unwrap_or_default(),
}
}
}
impl From<crate::ChannelCompositeKey> for ChannelCompositeKey {
fn from(value: crate::ChannelCompositeKey) -> Self {
ChannelCompositeKey {
channel: value.channel,
user: value.user,
}
}
}
impl From<crate::Webhook> for Webhook { impl From<crate::Webhook> for Webhook {
fn from(value: crate::Webhook) -> Self { fn from(value: crate::Webhook) -> Self {
Webhook { Webhook {
@@ -52,6 +106,7 @@ impl From<crate::Webhook> for Webhook {
avatar: value.avatar.map(|file| file.into()), avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id, channel_id: value.channel_id,
token: value.token, token: value.token,
permissions: value.permissions,
} }
} }
} }
@@ -64,6 +119,7 @@ impl From<crate::PartialWebhook> for PartialWebhook {
avatar: value.avatar.map(|file| file.into()), avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id, channel_id: value.channel_id,
token: value.token, token: value.token,
permissions: value.permissions,
} }
} }
} }
@@ -197,6 +253,28 @@ impl From<crate::FieldsChannel> for FieldsChannel {
} }
} }
impl From<crate::Emoji> for Emoji {
fn from(value: crate::Emoji) -> Self {
Emoji {
id: value.id,
parent: value.parent.into(),
creator_id: value.creator_id,
name: value.name,
animated: value.animated,
nsfw: value.nsfw,
}
}
}
impl From<crate::EmojiParent> for EmojiParent {
fn from(value: crate::EmojiParent) -> Self {
match value {
crate::EmojiParent::Detached => EmojiParent::Detached,
crate::EmojiParent::Server { id } => EmojiParent::Server { id },
}
}
}
impl From<crate::File> for File { impl From<crate::File> for File {
fn from(value: crate::File) -> Self { fn from(value: crate::File) -> Self {
File { File {
@@ -234,19 +312,340 @@ impl From<crate::Metadata> for Metadata {
} }
} }
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
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect(),
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.map(|reactions| {
reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect()
}),
interactions: value.interactions.map(|interactions| interactions.into()),
masquerade: value.masquerade.map(|masq| masq.into()),
}
}
}
impl From<crate::SystemMessage> for SystemMessage {
fn from(value: crate::SystemMessage) -> Self {
match value {
crate::SystemMessage::ChannelDescriptionChanged { by } => {
Self::ChannelDescriptionChanged { by }
}
crate::SystemMessage::ChannelIconChanged { by } => Self::ChannelIconChanged { by },
crate::SystemMessage::ChannelOwnershipChanged { from, to } => {
Self::ChannelOwnershipChanged { from, to }
}
crate::SystemMessage::ChannelRenamed { name, by } => Self::ChannelRenamed { name, by },
crate::SystemMessage::Text { content } => Self::Text { content },
crate::SystemMessage::UserAdded { id, by } => Self::UserAdded { id, by },
crate::SystemMessage::UserBanned { id } => Self::UserBanned { id },
crate::SystemMessage::UserJoined { id } => Self::UserJoined { id },
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
}
}
}
impl From<crate::Interactions> for Interactions {
fn from(value: crate::Interactions) -> Self {
Interactions {
reactions: value
.reactions
.map(|reactions| reactions.into_iter().collect()),
restrict_reactions: value.restrict_reactions,
}
}
}
impl From<Interactions> for crate::Interactions {
fn from(value: Interactions) -> Self {
crate::Interactions {
reactions: value
.reactions
.map(|reactions| reactions.into_iter().collect()),
restrict_reactions: value.restrict_reactions,
}
}
}
impl From<crate::AppendMessage> for AppendMessage {
fn from(value: crate::AppendMessage) -> Self {
AppendMessage {
embeds: value.embeds,
}
}
}
impl From<crate::Masquerade> for Masquerade {
fn from(value: crate::Masquerade) -> Self {
Masquerade {
name: value.name,
avatar: value.avatar,
colour: value.colour,
}
}
}
impl From<Masquerade> for crate::Masquerade {
fn from(value: Masquerade) -> Self {
crate::Masquerade {
name: value.name,
avatar: value.avatar,
colour: value.colour,
}
}
}
impl From<crate::ServerBan> for ServerBan {
fn from(value: crate::ServerBan) -> Self {
ServerBan {
id: value.id.into(),
reason: value.reason,
}
}
}
impl From<crate::Member> for Member {
fn from(value: crate::Member) -> Self {
Member {
id: value.id.into(),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::PartialMember> for PartialMember {
fn from(value: crate::PartialMember) -> Self {
PartialMember {
id: value.id.map(|id| id.into()),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::MemberCompositeKey> for MemberCompositeKey {
fn from(value: crate::MemberCompositeKey) -> Self {
MemberCompositeKey {
server: value.server,
user: value.user,
}
}
}
impl From<crate::FieldsMember> for FieldsMember {
fn from(value: crate::FieldsMember) -> Self {
match value {
crate::FieldsMember::Avatar => FieldsMember::Avatar,
crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout,
}
}
}
impl From<crate::RemovalIntention> for RemovalIntention {
fn from(value: crate::RemovalIntention) -> Self {
match value {
crate::RemovalIntention::Ban => RemovalIntention::Ban,
crate::RemovalIntention::Kick => RemovalIntention::Kick,
crate::RemovalIntention::Leave => RemovalIntention::Leave,
}
}
}
impl From<crate::Server> for Server {
fn from(value: crate::Server) -> Self {
Server {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.unwrap_or_default() as u32,
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::PartialServer> for PartialServer {
fn from(value: crate::PartialServer) -> Self {
PartialServer {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.map(|roles| roles.into_iter().map(|(k, v)| (k, v.into())).collect()),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.map(|v| v as u32),
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::FieldsServer> for FieldsServer {
fn from(value: crate::FieldsServer) -> Self {
match value {
crate::FieldsServer::Banner => FieldsServer::Banner,
crate::FieldsServer::Categories => FieldsServer::Categories,
crate::FieldsServer::Description => FieldsServer::Description,
crate::FieldsServer::Icon => FieldsServer::Icon,
crate::FieldsServer::SystemMessages => FieldsServer::SystemMessages,
}
}
}
impl From<crate::Category> for Category {
fn from(value: crate::Category) -> Self {
Category {
id: value.id,
title: value.title,
channels: value.channels,
}
}
}
impl From<crate::SystemMessageChannels> for SystemMessageChannels {
fn from(value: crate::SystemMessageChannels) -> Self {
SystemMessageChannels {
user_joined: value.user_joined,
user_left: value.user_left,
user_kicked: value.user_kicked,
user_banned: value.user_banned,
}
}
}
impl From<crate::Role> for Role {
fn from(value: crate::Role) -> Self {
Role {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::PartialRole> for PartialRole {
fn from(value: crate::PartialRole) -> Self {
PartialRole {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::FieldsRole> for FieldsRole {
fn from(value: crate::FieldsRole) -> Self {
match value {
crate::FieldsRole::Colour => FieldsRole::Colour,
}
}
}
impl crate::User { impl crate::User {
pub async fn into<P>(self, perspective: P) -> User pub async fn into<'a, P>(self, perspective: P) -> User
where where
P: Into<Option<crate::User>>, P: Into<Option<&'a crate::User>>,
{ {
let relationship = if let Some(perspective) = perspective.into() { let relationship = if let Some(perspective) = perspective.into() {
perspective if perspective.id == self.id {
.relations RelationshipStatus::User
.unwrap_or_default() } else {
.into_iter() perspective
.find(|relationship| relationship.id == self.id) .relations
.map(|relationship| relationship.status.into()) .as_ref()
.unwrap_or_default() .map(|relations| {
relations
.iter()
.find(|relationship| relationship.id == self.id)
.map(|relationship| relationship.status.clone().into())
.unwrap_or_default()
})
.unwrap_or_default()
}
} else { } else {
RelationshipStatus::None RelationshipStatus::None
}; };
@@ -272,6 +671,83 @@ impl crate::User {
id: self.id, id: self.id,
} }
} }
pub async fn into_self(self) -> User {
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: self
.relations
.map(|relationships| {
relationships
.into_iter()
.map(|relationship| relationship.into())
.collect()
})
.unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32,
status: self.status.map(|status| status.into()),
profile: self.profile.map(|profile| profile.into()),
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship: RelationshipStatus::User,
online: revolt_presence::is_online(&self.id).await,
id: self.id,
}
}
}
impl From<crate::PartialUser> for PartialUser {
fn from(value: crate::PartialUser) -> Self {
PartialUser {
username: value.username,
discriminator: value.discriminator,
display_name: value.display_name,
avatar: value.avatar.map(|file| file.into()),
relations: value.relations.map(|relationships| {
relationships
.into_iter()
.map(|relationship| relationship.into())
.collect()
}),
badges: value.badges.map(|badges| badges as u32),
status: value.status.map(|status| status.into()),
profile: value.profile.map(|profile| profile.into()),
flags: value.flags.map(|flags| flags as u32),
privileged: value.privileged,
bot: value.bot.map(|bot| bot.into()),
relationship: None,
online: None,
id: value.id,
}
}
}
impl From<FieldsUser> for crate::FieldsUser {
fn from(value: FieldsUser) -> Self {
match value {
FieldsUser::Avatar => crate::FieldsUser::Avatar,
FieldsUser::ProfileBackground => crate::FieldsUser::ProfileBackground,
FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent,
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
FieldsUser::StatusText => crate::FieldsUser::StatusText,
}
}
}
impl From<crate::FieldsUser> for FieldsUser {
fn from(value: crate::FieldsUser) -> Self {
match value {
crate::FieldsUser::Avatar => FieldsUser::Avatar,
crate::FieldsUser::ProfileBackground => FieldsUser::ProfileBackground,
crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent,
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
crate::FieldsUser::StatusText => FieldsUser::StatusText,
}
}
} }
impl From<crate::RelationshipStatus> for RelationshipStatus { impl From<crate::RelationshipStatus> for RelationshipStatus {
@@ -1,23 +1,18 @@
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 revolt_rocket_okapi::gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
use serde::{Deserialize, Serialize};
use validator::Validate;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
#[derive(Validate, Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct IdempotencyKey { pub struct IdempotencyKey {
#[validate(length(min = 1, max = 64))]
key: String, key: String,
} }
static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> = Lazy::new(|| Mutex::new(lru::LruCache::new(100))); static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
impl IdempotencyKey { impl IdempotencyKey {
// Backwards compatibility. // Backwards compatibility.
@@ -26,7 +21,7 @@ impl IdempotencyKey {
if let Some(v) = v { if let Some(v) = v {
let mut cache = TOKEN_CACHE.lock().await; let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&v).is_some() { if cache.get(&v).is_some() {
return Err(Error::DuplicateNonce); return Err(create_error!(DuplicateNonce));
} }
cache.put(v.clone(), ()); cache.put(v.clone(), ());
@@ -41,6 +36,17 @@ impl IdempotencyKey {
} }
} }
#[cfg(feature = "rocket-impl")]
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
revolt_okapi::openapi3::{Parameter, ParameterValue},
};
#[cfg(feature = "rocket-impl")]
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
#[cfg(feature = "rocket-impl")]
impl<'r> OpenApiFromRequest<'r> for IdempotencyKey { impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
fn from_request_input( fn from_request_input(
_gen: &mut OpenApiGenerator, _gen: &mut OpenApiGenerator,
@@ -70,6 +76,13 @@ impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
} }
} }
#[cfg(feature = "rocket-impl")]
use rocket::{
http::Status,
request::{FromRequest, Outcome},
};
#[cfg(feature = "rocket-impl")]
#[async_trait] #[async_trait]
impl<'r> FromRequest<'r> for IdempotencyKey { impl<'r> FromRequest<'r> for IdempotencyKey {
type Error = Error; type Error = Error;
@@ -81,14 +94,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(), ());
+1
View File
@@ -1,3 +1,4 @@
pub mod bridge; 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, Webhook}; use crate::{Bot, Channel, Database, Webhook};
/// Reference to some object in the database /// Reference to some object in the database
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -27,6 +27,11 @@ impl Reference {
db.fetch_bot(&self.id).await db.fetch_bot(&self.id).await
} }
/// Fetch channel from Ref
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
db.fetch_channel(&self.id).await
}
/// Fetch webhook from Ref /// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> { pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await db.fetch_webhook(&self.id).await
+15 -8
View File
@@ -1,28 +1,35 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.6.1" 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", "revolt-permissions/serde" ] serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
schemas = [ "dep:schemars", "revolt-permissions/schemas" ] schemas = ["dep:schemars", "revolt-permissions/schemas"]
validator = [ "dep:validator" ] validator = ["dep:validator"]
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ] partials = ["dep:revolt_optional_struct", "serde", "schemas"]
default = [ "serde", "partials" ] default = ["serde", "partials"]
[dependencies] [dependencies]
# Core # Core
revolt-permissions = { version = "0.6.1", path = "../permissions" } revolt-config = { version = "0.6.8", path = "../config" }
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 } revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
# Spec Generation # Spec Generation
schemars = { version = "0.8.8", optional = true } schemars = { version = "0.8.8", optional = true }
@@ -1,28 +0,0 @@
auto_derived!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// New strike information
pub struct DataCreateStrike {
/// Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// New strike information
pub struct DataEditAccountStrike {
/// New attached reason
pub reason: String,
}
);
+80 -3
View File
@@ -2,6 +2,7 @@ use super::User;
auto_derived!( auto_derived!(
/// Bot /// Bot
#[derive(Default)]
pub struct Bot { pub struct Bot {
/// Bot Id /// Bot Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))] #[cfg_attr(feature = "serde", serde(rename = "_id"))]
@@ -55,6 +56,12 @@ auto_derived!(
pub flags: u32, pub flags: u32,
} }
/// Optional fields on bot object
pub enum FieldsBot {
Token,
InteractionsURL,
}
/// Flags that may be attributed to a bot /// Flags that may be attributed to a bot
#[repr(u32)] #[repr(u32)]
pub enum BotFlags { pub enum BotFlags {
@@ -65,16 +72,22 @@ auto_derived!(
/// Public Bot /// Public Bot
pub struct PublicBot { pub struct PublicBot {
/// Bot Id /// Bot Id
#[serde(rename = "_id")] #[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String, pub id: String,
/// Bot Username /// Bot Username
pub username: String, pub username: String,
/// Profile Avatar /// Profile Avatar
#[serde(skip_serializing_if = "String::is_empty")] #[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub avatar: String, pub avatar: String,
/// Profile Description /// Profile Description
#[serde(skip_serializing_if = "String::is_empty")] #[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub description: String, pub description: String,
} }
@@ -85,4 +98,68 @@ auto_derived!(
/// User object /// User object
pub user: User, pub user: User,
} }
/// Bot Details
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataCreateBot {
/// Bot username
#[cfg_attr(
feature = "validator",
validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")
)]
pub name: String,
}
/// New Bot Details
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct DataEditBot {
/// Bot username
#[cfg_attr(
feature = "validator",
validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Whether the bot can be added by anyone
pub public: Option<bool>,
/// Whether analytics should be gathered for this bot
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
pub analytics: Option<bool>,
/// Interactions URL
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
pub interactions_url: Option<String>,
/// Fields to remove from bot object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsBot>>,
}
/// Where we are inviting a bot to
#[serde(untagged)]
pub enum InviteBotDestination {
/// Invite to a server
Server {
/// Server Id
server: String,
},
/// Invite to a group
Group {
/// Group Id
group: String,
},
}
/// Owned Bots Response
///
/// Both lists are sorted by their IDs.
///
/// TODO: user should be in bot object
pub struct OwnedBotsResponse {
/// Bot objects
pub bots: Vec<Bot>,
/// User objects
pub users: Vec<User>,
}
); );
@@ -0,0 +1,27 @@
auto_derived!(
/// Invite
pub enum Invite {
/// Invite to a specific server channel
Server {
/// Invite code
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
code: String,
/// Id of the server this invite points to
server: String,
/// Id of user who created this invite
creator: String,
/// Id of the server channel this invite points to
channel: String,
},
/// Invite to a group channel
Group {
/// Invite code
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
code: String,
/// Id of user who created this invite
creator: String,
/// Id of the group channel this invite points to
channel: String,
},
}
);
@@ -0,0 +1,27 @@
auto_derived!(
/// Channel Unread
pub struct ChannelUnread {
/// Composite key pointing to a user's view of a channel
#[serde(rename = "_id")]
pub id: ChannelCompositeKey,
/// Id of the last message read in this channel by a user
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub last_id: Option<String>,
/// Array of message ids that mention the user
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Vec::is_empty", default)
)]
pub mentions: Vec<String>,
}
/// Composite primary key consisting of channel and user id
#[derive(Hash)]
pub struct ChannelCompositeKey {
/// Channel Id
pub channel: String,
/// User Id
pub user: String,
}
);
@@ -1,3 +1,5 @@
use validator::Validate;
use super::File; use super::File;
auto_derived_partial!( auto_derived_partial!(
@@ -16,6 +18,9 @@ auto_derived_partial!(
/// The channel this webhook belongs to /// The channel this webhook belongs to
pub channel_id: String, pub channel_id: String,
/// The permissions for the webhook
pub permissions: u64,
/// The private token for the webhook /// The private token for the webhook
pub token: Option<String>, pub token: Option<String>,
}, },
@@ -43,6 +48,9 @@ auto_derived!(
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))] #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub avatar: Option<String>, pub avatar: Option<String>,
/// Webhook permissions
pub permissions: Option<u64>,
/// Fields to remove from webhook /// Fields to remove from webhook
#[cfg_attr(feature = "serde", serde(default))] #[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsWebhook>, pub remove: Vec<FieldsWebhook>,
@@ -61,12 +69,25 @@ auto_derived!(
/// The channel this webhook belongs to /// The channel this webhook belongs to
pub channel_id: String, pub channel_id: String,
/// The permissions for the webhook
pub permissions: u64,
} }
/// Optional fields on webhook object /// Optional fields on webhook object
pub enum FieldsWebhook { pub enum FieldsWebhook {
Avatar, Avatar,
} }
/// Information for the webhook
#[derive(Validate)]
pub struct CreateWebhookBody {
#[validate(length(min = 1, max = 32))]
pub name: String,
#[validate(length(min = 1, max = 128))]
pub avatar: Option<String>,
}
); );
impl From<Webhook> for MessageWebhook { impl From<Webhook> for MessageWebhook {
@@ -85,6 +106,7 @@ impl From<Webhook> for ResponseWebhook {
name: value.name, name: value.name,
avatar: value.avatar.map(|file| file.id), avatar: value.avatar.map(|file| file.id),
channel_id: value.channel_id, channel_id: value.channel_id,
permissions: value.permissions,
} }
} }
} }
+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,
}
);
+8 -8
View File
@@ -2,7 +2,7 @@ auto_derived!(
/// File /// File
pub struct File { pub struct File {
/// Unique Id /// Unique Id
#[serde(rename = "_id")] #[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String, pub id: String,
/// Tag / bucket this file was uploaded to /// Tag / bucket this file was uploaded to
pub tag: String, pub tag: String,
@@ -16,29 +16,29 @@ auto_derived!(
pub size: isize, pub size: isize,
/// Whether this file was deleted /// Whether this file was deleted
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub deleted: Option<bool>, pub deleted: Option<bool>,
/// Whether this file was reported /// Whether this file was reported
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub reported: Option<bool>, pub reported: Option<bool>,
// TODO: migrate this mess to having: // TODO: migrate this mess to having:
// - author_id // - author_id
// - parent: Parent { Message(id), User(id), etc } // - parent: Parent { Message(id), User(id), etc }
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub message_id: Option<String>, pub message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_id: Option<String>, pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub server_id: Option<String>, pub server_id: Option<String>,
/// Id of the object this file is associated with /// Id of the object this file is associated with
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub object_id: Option<String>, pub object_id: Option<String>,
} }
/// Metadata associated with a file /// Metadata associated with a file
#[serde(tag = "type")] #[cfg_attr(feature = "serde", serde(tag = "type"))]
#[derive(Default)] #[derive(Default)]
pub enum Metadata { pub enum Metadata {
/// File is just a generic uncategorised file /// File is just a generic uncategorised file
+330
View File
@@ -0,0 +1,330 @@
use std::{
collections::{HashMap, HashSet},
time::SystemTime,
};
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_config::config;
use validator::Validate;
use iso8601_timestamp::Timestamp;
use super::{Embed, File, MessageWebhook, User, Webhook, RE_COLOUR};
pub static RE_MENTION: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
auto_derived_partial!(
/// Message
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 = "HashMap::is_empty", default)]
pub reactions: HashMap<String, HashSet<String>>,
/// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
/// 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
#[derive(Validate)]
pub struct Masquerade {
// FIXME: missing validation
/// 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<HashSet<String>>,
/// Whether reactions should be restricted to the given list
///
/// Can only be set to true if reactions list is of at least length 1
#[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 Sort
///
/// Sort used for retrieving messages
#[derive(Default)]
pub enum MessageSort {
/// Sort by the most relevant messages
#[default]
Relevance,
/// Sort by the newest messages first
Latest,
/// Sort by the oldest messages first
Oldest,
}
/// Push Notification
pub struct PushNotification {
/// Known author name
pub author: String,
/// URL to author avatar
pub icon: String,
/// URL to first matching attachment
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
/// Message content or system message information
pub body: String,
/// Unique tag, usually the channel ID
pub tag: String,
/// Timestamp at which this notification was created
pub timestamp: u64,
/// URL to open when clicking notification
pub url: String,
}
/// Representation of a text embed before it is sent.
#[derive(Default, Validate)]
pub struct SendableEmbed {
#[validate(length(min = 1, max = 128))]
pub icon_url: Option<String>,
#[validate(length(min = 1, max = 256))]
pub url: Option<String>,
#[validate(length(min = 1, max = 100))]
pub title: Option<String>,
#[validate(length(min = 1, max = 2000))]
pub description: Option<String>,
pub media: Option<String>,
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
pub colour: Option<String>,
}
/// What this message should reply to and how
pub struct ReplyIntent {
/// Message Id
pub id: String,
/// Whether this reply should mention the message's author
pub mention: bool,
}
/// Message to send
#[derive(Validate)]
pub struct DataMessageSend {
/// Unique token to prevent duplicate message sending
///
/// **This is deprecated and replaced by `Idempotency-Key`!**
#[validate(length(min = 1, max = 64))]
pub nonce: Option<String>,
/// Message content to send
#[validate(length(min = 0, max = 2000))]
pub content: Option<String>,
/// Attachments to include in message
pub attachments: Option<Vec<String>>,
/// Messages to reply to
pub replies: Option<Vec<ReplyIntent>>,
/// Embeds to include in message
///
/// Text embed content contributes to the content length cap
#[validate]
pub embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message
#[validate]
pub masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
pub interactions: Option<Interactions>,
}
);
/// Message Author Abstraction
pub enum MessageAuthor<'a> {
User(&'a User),
Webhook(&'a Webhook),
System {
username: &'a str,
avatar: Option<&'a str>,
},
}
impl Interactions {
/// Check if default initialisation of fields
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}
impl<'a> MessageAuthor<'a> {
pub fn id(&self) -> &str {
match self {
MessageAuthor::User(user) => &user.id,
MessageAuthor::Webhook(webhook) => &webhook.id,
MessageAuthor::System { .. } => "00000000000000000000000000",
}
}
pub fn avatar(&self) -> Option<&str> {
match self {
MessageAuthor::User(user) => user.avatar.as_ref().map(|file| file.id.as_str()),
MessageAuthor::Webhook(webhook) => webhook.avatar.as_ref().map(|file| file.id.as_str()),
MessageAuthor::System { avatar, .. } => *avatar,
}
}
pub fn username(&self) -> &str {
match self {
MessageAuthor::User(user) => &user.username,
MessageAuthor::Webhook(webhook) => &webhook.name,
MessageAuthor::System { username, .. } => username,
}
}
}
impl From<SystemMessage> for String {
fn from(s: SystemMessage) -> String {
match s {
SystemMessage::Text { content } => content,
SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(),
SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(),
SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(),
SystemMessage::UserLeft { .. } => "User left the channel.".to_string(),
SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(),
SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(),
SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(),
SystemMessage::ChannelDescriptionChanged { .. } => {
"Channel description changed.".to_string()
}
SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(),
SystemMessage::ChannelOwnershipChanged { .. } => {
"Channel ownership changed.".to_string()
}
}
}
}
impl PushNotification {
/// Create a new notification from a given message, author and channel ID
pub async fn from(msg: Message, author: Option<MessageAuthor<'_>>, channel_id: &str) -> Self {
let config = config().await;
let icon = if let Some(author) = &author {
if let Some(avatar) = author.avatar() {
format!("{}/avatars/{}", config.hosts.autumn, avatar)
} else {
format!("{}/users/{}/default_avatar", config.hosts.api, author.id())
}
} else {
format!("{}/assets/logo.png", config.hosts.app)
};
let image = msg.attachments.and_then(|attachments| {
attachments
.first()
.map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
});
let body = if let Some(sys) = msg.system {
sys.into()
} else if let Some(text) = msg.content {
text
} else {
"Empty Message".to_string()
};
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Time went backwards")
.as_secs();
Self {
author: author
.map(|x| x.username().to_string())
.unwrap_or_else(|| "Revolt".to_string()),
icon,
image,
body,
tag: channel_id.to_string(),
timestamp,
url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id),
}
}
}
+18 -2
View File
@@ -1,13 +1,29 @@
mod account_strikes;
mod bots; mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks; mod channel_webhooks;
mod channels; mod channels;
mod embeds;
mod emojis;
mod files; mod files;
mod messages;
mod server_bans;
mod server_members;
mod servers;
mod user_settings;
mod users; mod users;
pub use account_strikes::*;
pub use bots::*; pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
pub use channel_webhooks::*; pub use channel_webhooks::*;
pub use channels::*; pub use channels::*;
pub use embeds::*;
pub use emojis::*;
pub use files::*; pub use files::*;
pub use messages::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
pub use user_settings::*;
pub use users::*; pub use users::*;
+12
View File
@@ -0,0 +1,12 @@
use super::MemberCompositeKey;
auto_derived!(
/// Server Ban
pub struct ServerBan {
/// Unique member id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: MemberCompositeKey,
/// Reason for ban creation
pub reason: Option<String>,
}
);
@@ -0,0 +1,80 @@
use super::File;
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use regex::Regex;
/// Regex for valid role colours
///
/// Allows the use of named colours, rgb(a), variables and all gradients.
///
/// Flags:
/// - Case-insensitive (`i`)
///
/// Source:
/// ```regex
/// VALUE = [a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+
/// ADDITIONAL_VALUE = \d+deg
/// STOP = ([ ]+(\d{1,3}%|0))?
///
/// ^(?:VALUE|(repeating-)?(linear|conic|radial)-gradient\((VALUE|ADDITIONAL_VALUE)STOP(,[ ]*(VALUE)STOP)+\))$
/// ```
pub static RE_COLOUR: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^(?:[a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|(repeating-)?(linear|conic|radial)-gradient\(([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|\d+deg)([ ]+(\d{1,3}%|0))?(,[ ]*([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+)([ ]+(\d{1,3}%|0))?)+\))$").unwrap()
});
auto_derived_partial!(
/// Server Member
pub struct Member {
/// Unique member id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: MemberCompositeKey,
/// Time at which this user joined the server
pub joined_at: Timestamp,
/// Member's nickname
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub nickname: Option<String>,
/// Avatar attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub avatar: Option<File>,
/// Member's roles
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Vec::is_empty", default)
)]
pub roles: Vec<String>,
/// Timestamp this member is timed out until
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub timeout: Option<Timestamp>,
},
"PartialMember"
);
auto_derived!(
/// Composite primary key consisting of server and user id
#[derive(Hash, Default)]
pub struct MemberCompositeKey {
/// Server Id
pub server: String,
/// User Id
pub user: String,
}
/// Optional fields on server member object
pub enum FieldsMember {
Nickname,
Avatar,
Roles,
Timeout,
}
/// Member removal intention
pub enum RemovalIntention {
Leave,
Kick,
Ban,
}
);
+144
View File
@@ -0,0 +1,144 @@
use super::File;
use revolt_permissions::OverrideField;
use std::collections::HashMap;
auto_derived_partial!(
/// Server
pub struct Server {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// User id of the owner
pub owner: String,
/// Name of the server
pub name: String,
/// Description for the server
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub description: Option<String>,
/// Channels within this server
// ! FIXME: this may be redundant
pub channels: Vec<String>,
/// Categories for this server
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub categories: Option<Vec<Category>>,
/// Configuration for sending system event messages
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub system_messages: Option<SystemMessageChannels>,
/// Roles for this server
#[cfg_attr(
feature = "serde",
serde(
default = "HashMap::<String, Role>::new",
skip_serializing_if = "HashMap::<String, Role>::is_empty"
)
)]
pub roles: HashMap<String, Role>,
/// Default set of server and channel permissions
pub default_permissions: i64,
/// Icon attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub icon: Option<File>,
/// Banner attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub banner: Option<File>,
/// Bitfield of server flags
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
)]
pub flags: u32,
/// Whether this server is flagged as not safe for work
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub nsfw: bool,
/// Whether to enable analytics
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub analytics: bool,
/// Whether this server should be publicly discoverable
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub discoverable: bool,
},
"PartialServer"
);
auto_derived_partial!(
/// Role
pub struct Role {
/// Role name
pub name: String,
/// Permissions available to this role
pub permissions: OverrideField,
/// Colour used for this role
///
/// This can be any valid CSS colour
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub colour: Option<String>,
/// Whether this role should be shown separately on the member sidebar
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub hoist: bool,
/// Ranking of this role
#[cfg_attr(feature = "serde", serde(default))]
pub rank: i64,
},
"PartialRole"
);
auto_derived!(
/// Optional fields on server object
pub enum FieldsServer {
Description,
Categories,
SystemMessages,
Icon,
Banner,
}
/// Optional fields on server object
pub enum FieldsRole {
Colour,
}
/// Channel category
pub struct Category {
/// Unique ID for this category
pub id: String,
/// Title for this category
pub title: String,
/// Channels in this category
pub channels: Vec<String>,
}
/// System message channel assignments
pub struct SystemMessageChannels {
/// ID of channel to send user join messages in
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_joined: Option<String>,
/// ID of channel to send user left messages in
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_left: Option<String>,
/// ID of channel to send user kicked messages in
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_kicked: Option<String>,
/// ID of channel to send user banned messages in
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub user_banned: Option<String>,
}
);
@@ -0,0 +1,6 @@
use std::collections::HashMap;
/// HashMap of user settings
/// Each key is mapped to a tuple consisting of the
/// revision timestamp and serialised data (in JSON format)
pub type UserSettings = HashMap<String, (i64, String)>;
+59 -17
View File
@@ -1,68 +1,110 @@
use once_cell::sync::Lazy;
use regex::Regex;
use super::File; use super::File;
auto_derived!( /// Regex for valid usernames
///
/// Block zero width space
/// Block lookalike characters
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
auto_derived_partial!(
/// User /// User
pub struct User { pub struct User {
/// Unique Id /// Unique Id
#[serde(rename = "_id")] #[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String, pub id: String,
/// Username /// Username
pub username: String, pub username: String,
/// Discriminator /// Discriminator
pub discriminator: String, pub discriminator: String,
/// Display name /// Display name
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub display_name: Option<String>, pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
/// Avatar attachment /// Avatar attachment
pub avatar: Option<File>, pub avatar: Option<File>,
/// Relationships with other users /// Relationships with other users
#[serde(skip_serializing_if = "Vec::is_empty", default)] #[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "Vec::is_empty", default)
)]
pub relations: Vec<Relationship>, pub relations: Vec<Relationship>,
/// Bitfield of user badges /// Bitfield of user badges
#[serde(skip_serializing_if = "crate::if_zero_u32", default)] #[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
)]
pub badges: u32, pub badges: u32,
/// User's current status /// User's current status
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub status: Option<UserStatus>, pub status: Option<UserStatus>,
/// User's profile page /// User's profile page
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub profile: Option<UserProfile>, pub profile: Option<UserProfile>,
/// Enum of user flags /// Enum of user flags
#[serde(skip_serializing_if = "crate::if_zero_u32", default)] #[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
)]
pub flags: u32, pub flags: u32,
/// Whether this user is privileged /// Whether this user is privileged
#[serde(skip_serializing_if = "crate::if_false", default)] #[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub privileged: bool, pub privileged: bool,
/// Bot information /// Bot information
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub bot: Option<BotInformation>, pub bot: Option<BotInformation>,
/// Current session user's relationship with this user /// Current session user's relationship with this user
pub relationship: RelationshipStatus, pub relationship: RelationshipStatus,
/// Whether this user is currently online /// Whether this user is currently online
pub online: bool, pub online: bool,
},
"PartialUser"
);
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)
#[derive(Default)] #[derive(Default)]
pub enum RelationshipStatus { pub enum RelationshipStatus {
/// No relationship with other user
#[default] #[default]
None, None,
/// Other user is us
User, User,
/// Friends with the other user
Friend, Friend,
/// Pending friend request to user
Outgoing, Outgoing,
/// Incoming friend request from user
Incoming, Incoming,
/// Blocked this user
Blocked, Blocked,
/// Blocked by this user
BlockedOther, BlockedOther,
} }
/// Relationship entry indicating current status with other user /// Relationship entry indicating current status with other user
pub struct Relationship { pub struct Relationship {
#[serde(rename = "_id")] /// Other user's Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub user_id: String, pub user_id: String,
/// Relationship status with them
pub status: RelationshipStatus, pub status: RelationshipStatus,
} }
@@ -83,20 +125,20 @@ auto_derived!(
/// User's active status /// User's active status
pub struct UserStatus { pub struct UserStatus {
/// Custom status text /// Custom status text
#[serde(skip_serializing_if = "String::is_empty")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub text: String, pub text: String,
/// Current presence option /// Current presence option
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub presence: Option<Presence>, pub presence: Option<Presence>,
} }
/// User's profile /// User's profile
pub struct UserProfile { pub struct UserProfile {
/// Text content on user's profile /// Text content on user's profile
#[serde(skip_serializing_if = "String::is_empty")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub content: String, pub content: String,
/// Background visible on user's profile /// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub background: Option<File>, pub background: Option<File>,
} }
@@ -143,7 +185,7 @@ auto_derived!(
/// Bot information for if the user is a bot /// Bot information for if the user is a bot
pub struct BotInformation { pub struct BotInformation {
/// Id of the owner of this bot /// Id of the owner of this bot
#[serde(rename = "owner")] #[cfg_attr(feature = "serde", serde(rename = "owner"))]
pub owner_id: String, pub owner_id: String,
} }
); );
+10 -7
View File
@@ -1,18 +1,18 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.6.1" 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: Permission Logic" description = "Revolt Backend: Permission Logic"
# 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]
bson = ["dep:bson"] bson = ["dep:bson"]
serde = [ "dep:serde" ] serde = ["dep:serde"]
schemas = [ "dep:schemars" ] schemas = ["dep:schemars"]
try-from-primitive = [ "dep:num_enum" ] try-from-primitive = ["dep:num_enum"]
[dev-dependencies] [dev-dependencies]
@@ -20,6 +20,9 @@ try-from-primitive = [ "dep:num_enum" ]
async-std = { version = "1.8.0", features = ["attributes"] } async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies] [dependencies]
# Core
revolt-result = { version = "0.6.5", path = "../result" }
# Utility # Utility
auto_ops = "0.3.0" auto_ops = "0.3.0"
once_cell = "1.17" once_cell = "1.17"
@@ -30,7 +33,7 @@ async-trait = "0.1.51"
# Serialisation # Serialisation
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
bson = { version = "2.1.0", optional = true} bson = { version = "2.1.0", optional = true }
# Spec Generation # Spec Generation
schemars = { version = "0.8.8", optional = true } schemars = { version = "0.8.8", optional = true }
+4
View File
@@ -118,6 +118,10 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
permissions.restrict(*ALLOW_IN_TIMEOUT); permissions.restrict(*ALLOW_IN_TIMEOUT);
} }
if !permissions.has_channel_permission(ChannelPermission::ViewChannel) {
permissions.revoke_all();
}
permissions permissions
} else { } else {
0_u64.into() 0_u64.into()
+14 -1
View File
@@ -1,5 +1,5 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::ops::Add; use std::{fmt, ops::Add};
/// Abstract channel type /// Abstract channel type
pub enum ChannelType { pub enum ChannelType {
@@ -102,6 +102,12 @@ pub enum ChannelPermission {
GrantAll = u64::MAX, GrantAll = u64::MAX,
} }
impl fmt::Display for ChannelPermission {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u64 { *a as u64 | *b as u64 }); impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u64 { *a as u64 | *b as u64 });
impl_op_ex_commutative!(+ |a: &u64, b: &ChannelPermission| -> u64 { *a | *b as u64 }); impl_op_ex_commutative!(+ |a: &u64, b: &ChannelPermission| -> u64 { *a | *b as u64 });
@@ -135,3 +141,10 @@ pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| {
+ ChannelPermission::ChangeAvatar, + ChannelPermission::ChangeAvatar,
) )
}); });
pub static DEFAULT_WEBHOOK_PERMISSIONS: Lazy<u64> = Lazy::new(|| {
ChannelPermission::SendMessage
+ ChannelPermission::SendEmbeds
+ ChannelPermission::Masquerade
+ ChannelPermission::React
});
+27
View File
@@ -3,6 +3,7 @@ mod server;
mod user; mod user;
pub use channel::*; pub use channel::*;
use revolt_result::{create_error, Result};
pub use server::*; pub use server::*;
pub use user::*; pub use user::*;
@@ -27,10 +28,36 @@ impl PermissionValue {
self.0 &= !v; self.0 &= !v;
} }
/// Revoke all permissions
pub fn revoke_all(&mut self) {
self.0 = 0;
}
/// Restrict to given permissions /// Restrict to given permissions
pub fn restrict(&mut self, v: u64) { pub fn restrict(&mut self, v: u64) {
self.0 &= v; self.0 &= v;
} }
/// Check whether certain a permission has been granted
pub fn has(&self, v: u64) -> bool {
(self.0 & v) == v
}
/// Check whether certain a channel permission has been granted
pub fn has_channel_permission(&self, permission: ChannelPermission) -> bool {
self.has(permission as u64)
}
/// Throw if missing channel permission
pub fn throw_if_lacking_channel_permission(&self, permission: ChannelPermission) -> Result<()> {
if self.has_channel_permission(permission) {
Ok(())
} else {
Err(create_error!(MissingPermission {
permission: permission.to_string()
}))
}
}
} }
impl From<i64> for PermissionValue { impl From<i64> for PermissionValue {

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