forked from jmug/stoatchat
Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9929f7499a | ||
|
|
279d9ef1b5 | ||
|
|
5a9bb9e68d | ||
|
|
a8fc9e928a | ||
|
|
da231ae390 | ||
|
|
7a31da4189 | ||
|
|
7d0b5392b5 | ||
|
|
1e1d44c0e4 | ||
|
|
1c4588967f | ||
|
|
853366a297 | ||
|
|
7568f49755 | ||
|
|
0542788567 | ||
|
|
73f7b8f007 | ||
|
|
c2412190a8 | ||
|
|
5372296dc0 | ||
|
|
41a47bdf8f | ||
|
|
51c26e324d | ||
|
|
3a55d00c6a | ||
|
|
004e84ece9 | ||
|
|
3d0914ce52 | ||
|
|
bde432cb75 | ||
|
|
28d225952a | ||
|
|
2fb9e7a802 | ||
|
|
431046cb4c | ||
|
|
92032cd9bc | ||
|
|
bfdd713d38 | ||
|
|
430df03b20 | ||
|
|
f5f70287e7 | ||
|
|
d5ba4ebf0c | ||
|
|
3bfead4ff1 | ||
|
|
d04621b1d6 | ||
|
|
a681df04bd | ||
|
|
9f3c1036d0 | ||
|
|
c9011ac692 | ||
|
|
42f977f536 | ||
|
|
a0580c5f5a | ||
|
|
121a9cd87c | ||
|
|
d87d608d9e | ||
|
|
bd9ecc1519 | ||
|
|
f5a113da47 | ||
|
|
11fdb0c1dc | ||
|
|
e0033ceb12 | ||
|
|
7318ec6ef6 | ||
|
|
a516c7adcf | ||
|
|
c48109ca66 | ||
|
|
f0ef781cd7 | ||
|
|
d9dd040db4 | ||
|
|
fec947b4c8 | ||
|
|
ec6df36c25 | ||
|
|
76150db293 | ||
|
|
815c3fe99e | ||
|
|
54878e8e8d | ||
|
|
6e4798f1d4 | ||
|
|
55bd6fb087 | ||
|
|
49035f4817 | ||
|
|
c0ebaa0bd3 | ||
|
|
b98b244fc3 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ target
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
.vercel
|
.vercel
|
||||||
|
.DS_Store
|
||||||
|
|||||||
Vendored
+4
-3
@@ -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
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -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" }
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
@@ -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",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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")),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, ()>>>,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": ¤t_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, ¤t_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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
+35
-17
@@ -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,3 +1,4 @@
|
|||||||
pub mod bridge;
|
pub mod bridge;
|
||||||
|
pub mod idempotency;
|
||||||
pub mod permissions;
|
pub mod permissions;
|
||||||
pub mod reference;
|
pub mod reference;
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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)>;
|
||||||
@@ -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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user