Compare commits

..
Author SHA1 Message Date
izzy 4fb99e3bd0 ci: also include gifbox src 2025-09-23 12:12:33 -05:00
izzy f0a83abcfa ci: add missing src copies for Docker build 2025-09-23 12:09:19 -05:00
izzy 6f1c715b8c chore: bump version to 0.8.9 2025-09-23 12:02:35 -05:00
Zomatree 38dd4d1079 fix: swap to using reqwest for query building 2025-09-19 02:50:00 +01:00
Zomatree 154204742d feat: add ratelimits to gifbox 2025-09-19 02:50:00 +01:00
Zomatree db55998546 docs: document revolt-coalesced 2025-09-19 02:50:00 +01:00
Zomatree 5885e067a6 feat: trending and categories routes 2025-09-19 02:50:00 +01:00
Zomatree a92152d86d fix: use our own result types instead of tenors types
add user auth
2025-09-19 02:50:00 +01:00
Zomatree b5cd5e30ef feat: require auth for search 2025-09-19 02:50:00 +01:00
Zomatree b0c977b324 feat: initial work on tenor gif searching 2025-09-19 02:50:00 +01:00
Zomatree bfe4018e43 fix: apple music to use original url instead of metadata url
Apple Music returns the url without the query parameter in the opengraph meta tags, causing the regex to not find the track id, meaning any embed links generated would only link to the album.
2025-09-19 02:41:53 +01:00
Zomatree cc7a7962a8 fix: use trust_cloudflare config value instead of env var 2025-09-18 21:22:31 +01:00
Zomatree fb4011084d refactor: move ratelimits to a generic system for all web servers 2025-09-18 21:22:31 +01:00
Zomatree 3a3415915f fix: relax settings name regex 2025-09-17 15:41:54 +01:00
Zomatree 964884a5de chore: add policy changes 2025-09-17 15:41:54 +01:00
Zomatree db57706794 feat: ready payload field customisation 2025-09-17 15:41:54 +01:00
Paul Makles a186732ca5 merge: pull request #428 from ElfFlu/bugfix/16byte-attachment-gain 2025-09-12 12:48:28 +01:00
Tom e635bc23ec fix: don't remove timeouts when a member leaves a server (#409) 2025-09-06 18:41:23 -07:00
Zomatree 3cb7da95e3 chore: add missing traits implementations to PermissionValue 2025-09-05 15:22:55 +01:00
Zomatree dea0f675dd fix: implement Serialize to ClientMessage 2025-09-05 13:07:52 +01:00
Zomatree 480f210ce8 feat: include member payload in ServerMemberJoin event 2025-08-24 06:32:38 +01:00
Zomatree caa8607468 feat: include groups and dms in fetch mutuals 2025-08-24 03:40:10 +01:00
ElfFlu 32e6600272 fix: remove authentication tag bytes from attachment download
Signed-off-by: ElfFlu <228066338+ElfFlu@users.noreply.github.com>
2025-08-23 19:05:32 +02:00
Zomatree 5335124306 fix: add license to revolt-parser 2025-08-20 18:55:52 +01:00
Zomatree 1561481eb4 feat: make message lexer use unowned string 2025-08-20 18:55:39 +01:00
IAmTomahawkx 8f9015a6ff fix: apply uname fix to january and autumn 2025-08-18 16:59:08 -07:00
174 changed files with 3856 additions and 2134 deletions

No files matched your search

+20
View File
@@ -152,6 +152,26 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-january.outputs.labels }} labels: ${{ steps.meta-january.outputs.labels }}
# revoltchat/gifbox
- name: Docker meta
id: meta-gifbox
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/gifbox
ghcr.io/revoltchat/gifbox
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/gifbox/Dockerfile
tags: ${{ steps.meta-gifbox.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-gifbox.outputs.labels }}
# revoltchat/crond # revoltchat/crond
- name: Docker meta - name: Docker meta
id: meta-crond id: meta-crond
Generated
+986 -760
View File
File diff suppressed because it is too large. Load diff
+3
View File
@@ -27,8 +27,11 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/ COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/ COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/ COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/ COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/ COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/ COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps RUN sh /tmp/build-image-layer.sh deps
+3
View File
@@ -23,8 +23,11 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/ COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/ COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/ COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/ COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/ COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/ COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps RUN sh /tmp/build-image-layer.sh deps
+5
View File
@@ -21,9 +21,11 @@ The services and libraries that power the Revolt service.<br/>
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) | | `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) | | `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) | | `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service | ![Crates.io Version](https://img.shields.io/crates/v/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/size/revolt-coalesced) ![Crates.io License](https://img.shields.io/crates/l/revolt-coalesced) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/gifbox` | [crates/services/gifbox](crates/services/gifbox) | Tenor proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) | | `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
@@ -66,6 +68,7 @@ As a heads-up, the development environment uses the following ports:
| `crates/bonfire` | 14703 | | `crates/bonfire` | 14703 |
| `crates/services/autumn` | 14704 | | `crates/services/autumn` | 14704 |
| `crates/services/january` | 14705 | | `crates/services/january` | 14705 |
| `crates/services/gifbox` | 14706 |
Now you can clone and build the project: Now you can clone and build the project:
@@ -141,6 +144,8 @@ cargo run --bin revolt-bonfire
cargo run --bin revolt-autumn cargo run --bin revolt-autumn
# run the proxy server # run the proxy server
cargo run --bin revolt-january cargo run --bin revolt-january
# run the tenor proxy
cargo run --bin revolt-gifbox
# run the push daemon (not usually needed in regular development) # run the push daemon (not usually needed in regular development)
cargo run --bin revolt-pushd cargo run --bin revolt-pushd
-4
View File
@@ -26,7 +26,3 @@ disallowed-methods = [
# 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",
] ]
disallowed-types = [
"rocket::serde::json::Json",
]
+4 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.8.8" version = "0.8.9"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -19,6 +19,7 @@ async-channel = "2.3.1"
# parsing # parsing
querystring = "1.1.0" querystring = "1.1.0"
regex = "1.11.1"
# serde # serde
bincode = "1.3.3" bincode = "1.3.3"
@@ -37,11 +38,11 @@ async-std = { version = "1.8.0", features = [
# core # core
authifier = { version = "1.0.15" } authifier = { version = "1.0.15" }
revolt-result = { path = "../core/result", features = ["sentry"] } revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" } revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" } revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.8", path = "../core/permissions" } revolt-permissions = { version = "0.8.9", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis # redis
+57 -19
View File
@@ -1,9 +1,15 @@
use async_tungstenite::tungstenite::{handshake, Message}; use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender; use futures::channel::oneshot::Sender;
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_database::events::client::ReadyPayloadFields; use revolt_database::events::client::ReadyPayloadFields;
use revolt_result::{create_error, Result, ToRevoltError}; use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// matches either a single word ie "users" or a key and value ie "settings[notifications]"
static READY_PAYLOAD_FIELD_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap());
/// Enumeration of supported protocol formats /// Enumeration of supported protocol formats
#[derive(Debug)] #[derive(Debug)]
pub enum ProtocolFormat { pub enum ProtocolFormat {
@@ -17,6 +23,7 @@ pub struct ProtocolConfiguration {
protocol_version: i32, protocol_version: i32,
format: ProtocolFormat, format: ProtocolFormat,
session_token: Option<String>, session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
} }
impl ProtocolConfiguration { impl ProtocolConfiguration {
@@ -25,11 +32,13 @@ impl ProtocolConfiguration {
protocol_version: i32, protocol_version: i32,
format: ProtocolFormat, format: ProtocolFormat,
session_token: Option<String>, session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
) -> Self { ) -> Self {
Self { Self {
protocol_version, protocol_version,
format, format,
session_token, session_token,
ready_payload_fields,
} }
} }
@@ -38,22 +47,16 @@ impl ProtocolConfiguration {
match self.format { match self.format {
ProtocolFormat::Json => { ProtocolFormat::Json => {
if let Message::Text(text) = msg { if let Message::Text(text) = msg {
// Log the error in-case we make a breaking change to the payload serde_json::from_str(text).map_err(|_| create_error!(InternalError))
serde_json::from_str(text)
.capture_error()
.map_err(|_| create_error!(UnprocessableEntity))
} else { } else {
Err(create_error!(UnprocessableEntity)) Err(create_error!(InternalError))
} }
} }
ProtocolFormat::Msgpack => { ProtocolFormat::Msgpack => {
if let Message::Binary(buf) = msg { if let Message::Binary(buf) = msg {
rmp_serde::from_slice(buf) rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
.capture_error()
.map_err(|_| create_error!(UnprocessableEntity))
} else { } else {
Err(create_error!(UnprocessableEntity)) Err(create_error!(InternalError))
} }
} }
} }
@@ -92,14 +95,8 @@ impl ProtocolConfiguration {
} }
/// Get ready payload fields /// Get ready payload fields
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> { pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields {
vec![ &self.ready_payload_fields
ReadyPayloadFields::Users,
ReadyPayloadFields::Servers,
ReadyPayloadFields::Channels,
ReadyPayloadFields::Members,
ReadyPayloadFields::Emoji,
]
} }
} }
@@ -130,6 +127,22 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
let mut protocol_version = 1; let mut protocol_version = 1;
let mut format = ProtocolFormat::Json; let mut format = ProtocolFormat::Json;
let mut session_token = None; let mut session_token = None;
let mut ready_payload_fields = if params.iter().any(|(k, _)| *k == "ready") {
// If they pass the ready field, set all fields to false
ReadyPayloadFields {
users: false,
servers: false,
channels: false,
members: false,
emojis: false,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: false,
}
} else {
ReadyPayloadFields::default()
};
// Parse and map parameters from key-value to known variables. // Parse and map parameters from key-value to known variables.
for (key, value) in params { for (key, value) in params {
@@ -145,6 +158,30 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
_ => {} _ => {}
}, },
"token" => session_token = Some(value.into()), "token" => session_token = Some(value.into()),
"ready" => {
// Re-enable all the fields the client specifies
if let Some(captures) = READY_PAYLOAD_FIELD_REGEX.captures(value) {
if let Some(field) = captures.get(0) {
match field.as_str() {
"users" => ready_payload_fields.users = true,
"servers" => ready_payload_fields.servers = true,
"channels" => ready_payload_fields.channels = true,
"members" => ready_payload_fields.members = true,
"emojis" => ready_payload_fields.emojis = true,
"channel_unreads" => ready_payload_fields.channel_unreads = true,
"user_settings" => {
if let Some(subkey) = captures.get(1) {
ready_payload_fields
.user_settings
.push(subkey.as_str().to_string());
}
}
"policy_changes" => ready_payload_fields.policy_changes = true,
_ => {}
}
}
}
}
_ => {} _ => {}
} }
} }
@@ -157,6 +194,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
protocol_version, protocol_version,
format, format,
session_token, session_token,
ready_payload_fields,
}) })
.is_ok() .is_ok()
{ {
+35 -29
View File
@@ -95,21 +95,23 @@ impl State {
pub async fn generate_ready_payload( pub async fn generate_ready_payload(
&mut self, &mut self,
db: &Database, db: &Database,
fields: Vec<ReadyPayloadFields>, fields: &ReadyPayloadFields,
) -> Result<EventV1> { ) -> Result<EventV1> {
let user = self.clone_user(); let user = self.clone_user();
self.cache.is_bot = user.bot.is_some(); self.cache.is_bot = user.bot.is_some();
// Fetch pending policy changes. // Fetch pending policy changes.
let policy_changes = if user.bot.is_some() { let policy_changes = if user.bot.is_some() || !fields.policy_changes {
vec![] None
} else { } else {
db.fetch_policy_changes() Some(
.await? db.fetch_policy_changes()
.into_iter() .await?
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change) .into_iter()
.map(Into::into) .filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.collect() .map(Into::into)
.collect(),
)
}; };
// Find all relationships to the user. // Find all relationships to the user.
@@ -168,7 +170,7 @@ impl State {
.await?; .await?;
// Fetch customisations. // Fetch customisations.
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) { let emojis = if fields.emojis {
Some( Some(
db.fetch_emoji_by_parent_ids( db.fetch_emoji_by_parent_ids(
&servers &servers
@@ -176,25 +178,34 @@ impl State {
.map(|x| x.id.to_string()) .map(|x| x.id.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.await?, .await?
.into_iter()
.map(|emoji| emoji.into())
.collect(),
) )
} else { } else {
None None
}; };
// Fetch user settings // Fetch user settings
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields let user_settings = if !fields.user_settings.is_empty() {
.iter() Some(
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_))) db.fetch_user_settings(&user.id, &fields.user_settings)
{ .await?,
Some(db.fetch_user_settings(&user.id, keys).await?) )
} else { } else {
None None
}; };
// Fetch channel unreads // Fetch channel unreads
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) { let channel_unreads = if fields.channel_unreads {
Some(db.fetch_unreads(&user.id).await?) Some(
db.fetch_unreads(&user.id)
.await?
.into_iter()
.map(|unread| unread.into())
.collect(),
)
} else { } else {
None None
}; };
@@ -241,30 +252,25 @@ impl State {
} }
Ok(EventV1::Ready { Ok(EventV1::Ready {
users: if fields.contains(&ReadyPayloadFields::Users) { users: if fields.users { Some(users) } else { None },
Some(users) servers: if fields.servers {
} else {
None
},
servers: if fields.contains(&ReadyPayloadFields::Servers) {
Some(servers.into_iter().map(Into::into).collect()) Some(servers.into_iter().map(Into::into).collect())
} else { } else {
None None
}, },
channels: if fields.contains(&ReadyPayloadFields::Channels) { channels: if fields.channels {
Some(channels.into_iter().map(Into::into).collect()) Some(channels.into_iter().map(Into::into).collect())
} else { } else {
None None
}, },
members: if fields.contains(&ReadyPayloadFields::Members) { members: if fields.members {
Some(members.into_iter().map(Into::into).collect()) Some(members.into_iter().map(Into::into).collect())
} else { } else {
None None
}, },
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()), emojis,
user_settings, user_settings,
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()), channel_unreads,
policy_changes, policy_changes,
}) })
+20 -20
View File
@@ -14,6 +14,7 @@ use futures::{
FutureExt, SinkExt, StreamExt, TryStreamExt, FutureExt, SinkExt, StreamExt, TryStreamExt,
}; };
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI}; use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_config::report_internal_error;
use revolt_database::{ use revolt_database::{
events::{client::EventV1, server::ClientMessage}, events::{client::EventV1, server::ClientMessage},
iso8601_timestamp::Timestamp, iso8601_timestamp::Timestamp,
@@ -26,7 +27,7 @@ use async_std::{
sync::{Mutex, RwLock}, sync::{Mutex, RwLock},
task::spawn, task::spawn,
}; };
use revolt_result::{create_error, ToRevoltError}; use revolt_result::create_error;
use sentry::Level; use sentry::Level;
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback}; use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
@@ -109,21 +110,21 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
let user_id = state.cache.user_id.clone(); let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated. // Notify socket we have authenticated.
if write.send(config.encode(&EventV1::Authenticated)).await.to_internal_error().is_err() { if report_internal_error!(write.send(config.encode(&EventV1::Authenticated)).await).is_err() {
return; return;
} }
// Download required data to local cache and send Ready payload. // Download required data to local cache and send Ready payload.
let ready_payload = match state let ready_payload = match report_internal_error!(
state
.generate_ready_payload(db, config.get_ready_payload_fields()) .generate_ready_payload(db, config.get_ready_payload_fields())
.await .await
.to_internal_error() ) {
{
Ok(ready_payload) => ready_payload, Ok(ready_payload) => ready_payload,
Err(_) => return, Err(_) => return,
}; };
if write.send(config.encode(&ready_payload)).await.to_internal_error().is_err() { if report_internal_error!(write.send(config.encode(&ready_payload)).await).is_err() {
return; return;
} }
@@ -218,15 +219,14 @@ async fn listener(
write: &Mutex<WsWriter>, write: &Mutex<WsWriter>,
) { ) {
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap(); let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
let subscriber = match fred::types::Builder::from_config(redis_config) let subscriber = match report_internal_error!(
.build_subscriber_client() fred::types::Builder::from_config(redis_config).build_subscriber_client()
.to_internal_error() ) {
{
Ok(subscriber) => subscriber, Ok(subscriber) => subscriber,
Err(_) => return, Err(_) => return,
}; };
if subscriber.init().await.to_internal_error().is_err() { if report_internal_error!(subscriber.init().await).is_err() {
return; return;
} }
@@ -249,13 +249,13 @@ async fn listener(
// Check for state changes for subscriptions. // Check for state changes for subscriptions.
match state.apply_state().await { match state.apply_state().await {
SubscriptionStateChange::Reset => { SubscriptionStateChange::Reset => {
if subscriber.unsubscribe_all().await.to_internal_error().is_err() { if report_internal_error!(subscriber.unsubscribe_all().await).is_err() {
break 'out; break 'out;
} }
let subscribed = state.subscribed.read().await; let subscribed = state.subscribed.read().await;
for id in subscribed.iter() { for id in subscribed.iter() {
if subscriber.subscribe(id).await.to_internal_error().is_err() { if report_internal_error!(subscriber.subscribe(id).await).is_err() {
break 'out; break 'out;
} }
} }
@@ -268,7 +268,7 @@ async fn listener(
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}"); info!("{addr:?} unsubscribing from {id}");
if subscriber.unsubscribe(id).await.to_internal_error().is_err() { if report_internal_error!(subscriber.unsubscribe(id).await).is_err() {
break 'out; break 'out;
} }
} }
@@ -277,7 +277,7 @@ async fn listener(
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}"); info!("{addr:?} subscribing to {id}");
if subscriber.subscribe(id).await.to_internal_error().is_err() { if report_internal_error!(subscriber.subscribe(id).await).is_err() {
break 'out; break 'out;
} }
} }
@@ -302,7 +302,7 @@ async fn listener(
_ = t2 => {}, _ = t2 => {},
message = t1 => { message = t1 => {
// Handle incoming events. // Handle incoming events.
let message = match message.to_internal_error() { let message = match report_internal_error!(message) {
Ok(message) => message, Ok(message) => message,
Err(_) => break 'out Err(_) => break 'out
}; };
@@ -311,15 +311,15 @@ async fn listener(
PayloadType::Json => message PayloadType::Json => message
.value .value
.as_str() .as_str()
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).to_internal_error().ok()), .and_then(|s| report_internal_error!(serde_json::from_str::<EventV1>(s.as_ref())).ok()),
PayloadType::Msgpack => message PayloadType::Msgpack => message
.value .value
.as_bytes() .as_bytes()
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).to_internal_error().ok()), .and_then(|b| report_internal_error!(rmp_serde::from_slice::<EventV1>(b)).ok()),
PayloadType::Bincode => message PayloadType::Bincode => message
.value .value
.as_bytes() .as_bytes()
.and_then(|b| bincode::deserialize::<EventV1>(b).to_internal_error().ok()), .and_then(|b| report_internal_error!(bincode::deserialize::<EventV1>(b)).ok()),
}; };
let Some(mut event) = event else { let Some(mut event) = event else {
@@ -379,7 +379,7 @@ async fn listener(
} }
} }
subscriber.quit().await.to_internal_error().ok(); report_internal_error!(subscriber.quit().await).ok();
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "revolt-coalesced"
version = "0.8.9"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
description = "Revolt Backend: Coalescion service"
[features]
tokio = ["dep:tokio"]
queue = ["dep:indexmap"]
cache = ["dep:lru"]
default = ["tokio"]
[dependencies]
tokio = { version = "1.47.0", features = ["sync"], optional = true }
indexmap = { version = "*", optional = true }
lru = { version = "*", optional = true }
[dev-dependencies]
tokio = { version = "1.47.0", features = ["rt", "rt-multi-thread", "macros", "time"] }
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+24
View File
@@ -0,0 +1,24 @@
#[derive(Clone, PartialEq, Eq, Debug)]
/// Config values for [`CoalescionService`].
pub struct CoalescionServiceConfig {
/// How many tasks are running at once
pub max_concurrent: Option<usize>,
/// Whether to queue tasks once `max_concurrent` is reached
#[cfg(feature = "queue")]
pub queue_requests: bool,
/// Max amount of tasks in the buffer queue
#[cfg(feature = "queue")]
pub max_queue: Option<usize>,
}
impl Default for CoalescionServiceConfig {
fn default() -> Self {
Self {
max_concurrent: Some(100),
#[cfg(feature = "queue")]
queue_requests: true,
#[cfg(feature = "queue")]
max_queue: Some(100)
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
/// Coalescion service error.
pub enum Error {
/// Failed to receive the actions return from the channel for unknown reason
RecvError,
/// Reached the `max_concurrent` amount of actions running at once and could not queue the action
MaxConcurrent,
/// Reached the `max_queue` amount of actions in the queue
MaxQueue,
/// Failed to downcast the type to the current type being returned, this will be most likely an ID collision
DowncastError,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::RecvError => write!(f, "Unable to receive data from the channel"),
Error::MaxConcurrent => write!(f, "Max number of tasks running at once"),
Error::MaxQueue => write!(f, "Max number of tasks in queue"),
Error::DowncastError => write!(f, "Failed to downcast type, possible key collision with different types")
}
}
}
impl std::error::Error for Error {}
+39
View File
@@ -0,0 +1,39 @@
//! # Coalesced
//!
//! Coalescion service to group, caching and queue duplicate actions.
//! useful for deduplicating web requests, database lookups and other similar resource
//! intensive or rate-limited actions.
//!
//! ## Features
//! - `tokio`: Uses tokio for the async backend, this is currently the only backend.
//! - `queue`: Whether to support queueing requests to only allow X amount of actions running at once.
//! - `cache`: Whether to cache the actions results for future actions with the same id, uses an LRU cache internally.
//!
//! [`CoalescionService`] uses both [`Arc`] and [`RwLock`] internally and can be cheaply cloned to
//! use in your codebase.
//!
//! It is common practice to wrap the service and in your own which delegates the executions to ensure all ids are tracked in one location across your codebase.
//!
//! All values are stored using [`Any`] and must be [`'static`] + [`Send`] + [`Sync`], if there is an id mismatch
//! and a type is wrong the library will return an error, values returned from the service are also
//! wrapped in an [`Arc`] as they are shared to each duplicate action.
//!
//! ## Example:
//! ```rs
//! use revolt_coalesced::CoalescionService;
//!
//! let service = CoalescionService::new();
//!
//! let user_id = "my_user_id";
//! let user = service.execute(user_id, || async move {
//! database.fetch_user(user_id).await.unwrap()
//! }).await;
//! ```
mod config;
mod error;
mod service;
pub use config::CoalescionServiceConfig;
pub use error::Error;
pub use service::CoalescionService;
+208
View File
@@ -0,0 +1,208 @@
use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc};
use tokio::sync::{
watch::{channel as watch_channel, Receiver},
RwLock,
};
#[cfg(feature = "cache")]
use lru::LruCache;
#[cfg(feature = "queue")]
use indexmap::IndexMap;
use crate::{CoalescionServiceConfig, Error};
#[derive(Debug, Clone)]
#[allow(clippy::type_complexity)]
/// # Coalescion service
///
/// See module description for example usage.
pub struct CoalescionService<Id: Hash + Clone + Eq> {
config: Arc<CoalescionServiceConfig>,
watchers: Arc<RwLock<HashMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "queue")]
queue: Arc<RwLock<IndexMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "cache")]
cache: Option<Arc<tokio::sync::Mutex<LruCache<Id, Arc<dyn Any + Send + Sync>>>>>,
}
impl<Id: Hash + Clone + Eq> CoalescionService<Id> {
pub fn new() -> Self {
Default::default()
}
pub fn from_config(config: CoalescionServiceConfig) -> Self {
Self {
config: Arc::new(config),
watchers: Arc::new(RwLock::new(HashMap::new())),
#[cfg(feature = "queue")]
queue: Arc::new(RwLock::new(IndexMap::new())),
#[cfg(feature = "cache")]
cache: None,
}
}
#[cfg(feature = "cache")]
pub fn from_cache(
config: CoalescionServiceConfig,
cache: LruCache<Id, Arc<dyn Any + Send + Sync>>,
) -> Self {
Self {
cache: Some(Arc::new(Mutex::new(cache))),
..Self::from_config(config)
}
}
async fn wait_for<Value: Any + Send + Sync>(
&self,
mut receiver: Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>,
) -> Result<Arc<Value>, Error> {
receiver
.wait_for(|v| v.is_some())
.await
.map_err(|_| Error::RecvError)
.and_then(|r| r.clone().unwrap())
.and_then(|arc| Arc::downcast(arc).map_err(|_| Error::DowncastError))
}
async fn insert_and_execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
let (send, recv) = watch_channel(None);
self.watchers.write().await.insert(id.clone(), recv);
let value = Ok(Arc::new(func().await));
send.send_modify(|opt| {
opt.replace(value.clone().map(|v| v as Arc<dyn Any + Send + Sync>));
});
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Ok(value) = &value {
cache.lock().await.push(id.clone(), value.clone());
}
};
self.watchers.write().await.remove(&id);
value
}
/// Coalesces an function, the actual function may not run if one with the same id is already running,
/// queued to be ran, or cached, the id should be globally unique for this specific action.
pub async fn execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Some(value) = cache.lock().await.get(&id) {
return Arc::downcast::<Value>(value.clone()).map_err(|_| Error::DowncastError);
}
};
let (receiver, length) = {
let watchers = self.watchers.read().await;
let length = watchers.len();
(watchers.get(&id).cloned(), length)
};
if let Some(receiver) = receiver {
self.wait_for(receiver).await
} else {
match self.config.max_concurrent {
Some(max_concurrent) if length >= max_concurrent => {
#[cfg(feature = "queue")]
if self.config.queue_requests {
let (receiver, length) = {
let queue = self.queue.read().await;
(queue.get(&id).cloned(), queue.len())
};
if let Some(receiver) = receiver {
return self.wait_for(receiver).await;
} else {
if self
.config
.max_queue
.is_some_and(|max_queue| max_queue >= length)
{
return Err(Error::MaxQueue);
};
let (send, recv) = watch_channel(None);
self.queue.write().await.insert(id.clone(), recv);
loop {
let length = self.watchers.read().await.len();
if length < max_concurrent {
let first_key = {
let queue = self.queue.read().await;
queue.first().map(|v| v.0).cloned()
};
if first_key == Some(id.clone()) {
self.queue.write().await.shift_remove(&id);
let response = self.insert_and_execute(id, func).await;
send.send_modify(|opt| {
opt.replace(
response
.clone()
.map(|v| v as Arc<dyn Any + Send + Sync>),
);
});
return response;
}
}
}
}
} else {
Err(Error::MaxConcurrent)
}
#[cfg(not(feature = "queue"))]
Err(Error::MaxConcurrent)
}
_ => self.insert_and_execute(id, func).await,
}
}
}
/// Fetches the amount of currently running tasks
pub async fn current_task_count(&self) -> usize {
self.watchers.read().await.len()
}
#[cfg(feature = "queue")]
/// Fetches the current length of the queue
pub async fn current_queue_len(&self) -> usize {
self.queue.read().await.len()
}
}
impl<Id: Hash + Clone + Eq> Default for CoalescionService<Id> {
fn default() -> Self {
Self::from_config(CoalescionServiceConfig::default())
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -37,4 +37,4 @@ sentry = { version = "0.31.5", optional = true }
sentry-anyhow = { version = "0.38.1", optional = true } sentry-anyhow = { version = "0.38.1", optional = true }
# Core # Core
revolt-result = { version = "0.8.8", path = "../result", optional = true } revolt-result = { version = "0.8.9", path = "../result", optional = true }
+3
View File
@@ -56,6 +56,8 @@ voso_legacy_token = ""
trust_cloudflare = false trust_cloudflare = false
# easypwned endpoint # easypwned endpoint
easypwned = "" easypwned = ""
# Tenor API Key
tenor_key = ""
[api.security.captcha] [api.security.captcha]
# hCaptcha configuration # hCaptcha configuration
@@ -277,3 +279,4 @@ files = ""
proxy = "" proxy = ""
pushd = "" pushd = ""
crond = "" crond = ""
gifbox = ""
+2
View File
@@ -190,6 +190,7 @@ pub struct ApiSecurity {
pub captcha: ApiSecurityCaptcha, pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool, pub trust_cloudflare: bool,
pub easypwned: String, pub easypwned: String,
pub tenor_key: String,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
@@ -365,6 +366,7 @@ pub struct Sentry {
pub proxy: String, pub proxy: String,
pub pushd: String, pub pushd: String,
pub crond: String, pub crond: String,
pub gifbox: String,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
+7 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -24,19 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.8", path = "../config", features = [ revolt-config = { version = "0.8.9", path = "../config", features = [
"report-macros", "report-macros",
] } ] }
revolt-result = { version = "0.8.8", path = "../result" } revolt-result = { version = "0.8.9", path = "../result" }
revolt-models = { version = "0.8.8", path = "../models", features = [ revolt-models = { version = "0.8.9", path = "../models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.8.8", path = "../presence" } revolt-presence = { version = "0.8.9", path = "../presence" }
revolt-permissions = { version = "0.8.8", path = "../permissions", features = [ revolt-permissions = { version = "0.8.9", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
revolt-parser = { version = "0.8.8", path = "../parser" } revolt-parser = { version = "0.8.9", path = "../parser" }
# Utility # Utility
log = "0.4" log = "0.4"
+33 -11
View File
@@ -20,16 +20,31 @@ pub enum Ping {
} }
/// Fields provided in Ready payload /// Fields provided in Ready payload
#[derive(PartialEq)] #[derive(PartialEq, Debug, Clone, Deserialize)]
pub enum ReadyPayloadFields { pub struct ReadyPayloadFields {
Users, pub users: bool,
Servers, pub servers: bool,
Channels, pub channels: bool,
Members, pub members: bool,
Emoji, pub emojis: bool,
pub user_settings: Vec<String>,
pub channel_unreads: bool,
pub policy_changes: bool,
}
UserSettings(Vec<String>), impl Default for ReadyPayloadFields {
ChannelUnreads, fn default() -> Self {
Self {
users: true,
servers: true,
channels: true,
members: true,
emojis: true,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: true,
}
}
} }
/// Protocol Events /// Protocol Events
@@ -63,7 +78,8 @@ pub enum EventV1 {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
channel_unreads: Option<Vec<ChannelUnread>>, channel_unreads: Option<Vec<ChannelUnread>>,
policy_changes: Vec<PolicyChange>, #[serde(skip_serializing_if = "Option::is_none")]
policy_changes: Option<Vec<PolicyChange>>,
}, },
/// Ping response /// Ping response
@@ -144,7 +160,13 @@ pub enum EventV1 {
}, },
/// User joins server /// User joins server
ServerMemberJoin { id: String, user: String }, ServerMemberJoin {
id: String,
// Deprecated: use member.id.user
#[deprecated = "Use member.id.user instead"]
user: String,
member: Member,
},
/// User left server /// User left server
ServerMemberLeave { ServerMemberLeave {
+2 -2
View File
@@ -1,8 +1,8 @@
use serde::Deserialize; use serde::{Serialize, Deserialize};
use super::client::Ping; use super::client::Ping;
#[derive(Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ClientMessage { pub enum ClientMessage {
Authenticate { token: String }, Authenticate { token: String },
@@ -1,7 +1,7 @@
use bson::to_document; use bson::to_document;
use bson::Document; use bson::Document;
use revolt_config::report_internal_error;
use revolt_result::Result; use revolt_result::Result;
use revolt_result::ToRevoltError;
use crate::File; use crate::File;
use crate::FileUsedFor; use crate::FileUsedFor;
@@ -106,7 +106,7 @@ impl AbstractAttachments for MongoDb {
}, },
doc! { doc! {
"$set": { "$set": {
"used_for": to_document(&used_for).to_internal_error()?, "used_for": report_internal_error!(to_document(&used_for))?,
"uploader_id": uploader_id "uploader_id": uploader_id
} }
}, },
@@ -10,6 +10,7 @@ use revolt_models::v0::{
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
use revolt_result::{ErrorType, Result}; use revolt_result::{ErrorType, Result};
use ulid::Ulid; use ulid::Ulid;
use validator::Validate;
use crate::{ use crate::{
events::client::EventV1, events::client::EventV1,
@@ -488,28 +489,31 @@ impl Message {
| Channel::VoiceChannel { ref server, .. } => { | Channel::VoiceChannel { ref server, .. } => {
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned()); let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await?; let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
if let Ok(valid_members) = valid_members {
let valid_mentions = HashSet::<&String, RandomState>::from_iter(
valid_members.iter().map(|m| &m.id.user),
);
let valid_mentions = HashSet::<&String, RandomState>::from_iter( user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
valid_members.iter().map(|m| &m.id.user),
);
user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server if !user_mentions.is_empty() {
// if there are still mentions, drill down to a channel-level
let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server)
.await
.channel(&channel)
.members(&valid_members)
.members_can_see_channel()
.await;
if !user_mentions.is_empty() { user_mentions
// if there are still mentions, drill down to a channel-level .retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
let member_channel_view_perms = }
BulkDatabasePermissionQuery::from_server_id(db, server) } else {
.await revolt_config::capture_error(&valid_members.unwrap_err());
.channel(&channel) return Err(create_error!(InternalError));
.members(&valid_members)
.members_can_see_channel()
.await;
user_mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
} }
} }
Channel::SavedMessages { .. } => { Channel::SavedMessages { .. } => {
user_mentions.clear(); user_mentions.clear();
@@ -713,6 +717,12 @@ impl Message {
/// Create text embed from sendable embed /// Create text embed from sendable embed
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> { pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
embed.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let media = if let Some(id) = embed.media { let media = if let Some(id) = embed.media {
Some(File::use_attachment(db, &id, &self.id, &self.author).await?) Some(File::use_attachment(db, &id, &self.id, &self.author).await?)
} else { } else {
@@ -30,6 +30,9 @@ auto_derived_partial!(
/// Timestamp this member is timed out until /// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Timestamp>, pub timeout: Option<Timestamp>,
// This value only exists in the database, not the models.
// If it is not-None, the database layer should return None to member fetching queries.
// pub pending_deletion_at: Option<Timestamp>
}, },
"PartialMember" "PartialMember"
); );
@@ -50,6 +53,7 @@ auto_derived!(
Avatar, Avatar,
Roles, Roles,
Timeout, Timeout,
JoinedAt,
} }
/// Member removal intention /// Member removal intention
@@ -90,7 +94,7 @@ impl Member {
return Err(create_error!(AlreadyInServer)); return Err(create_error!(AlreadyInServer));
} }
let member = Member { let mut member = Member {
id: MemberCompositeKey { id: MemberCompositeKey {
server: server.id.to_string(), server: server.id.to_string(),
user: user.id.to_string(), user: user.id.to_string(),
@@ -98,7 +102,9 @@ impl Member {
..Default::default() ..Default::default()
}; };
db.insert_member(&member).await?; if let Some(updated) = db.insert_or_merge_member(&member).await? {
member = updated;
}
let should_fetch = channels.is_none(); let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default(); let mut channels = channels.unwrap_or_default();
@@ -124,6 +130,7 @@ impl Member {
EventV1::ServerMemberJoin { EventV1::ServerMemberJoin {
id: server.id.clone(), id: server.id.clone(),
user: user.id.clone(), user: user.id.clone(),
member: member.clone().into(),
} }
.p(server.id.clone()) .p(server.id.clone())
.await; .await;
@@ -186,6 +193,7 @@ impl Member {
pub fn remove_field(&mut self, field: &FieldsMember) { pub fn remove_field(&mut self, field: &FieldsMember) {
match field { match field {
FieldsMember::JoinedAt => (),
FieldsMember::Avatar => self.avatar = None, FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None, FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(), FieldsMember::Roles => self.roles.clear(),
@@ -224,7 +232,7 @@ impl Member {
intention: RemovalIntention, intention: RemovalIntention,
silent: bool, silent: bool,
) -> Result<()> { ) -> Result<()> {
db.delete_member(&self.id).await?; db.soft_delete_member(&self.id).await?;
EventV1::ServerMemberLeave { EventV1::ServerMemberLeave {
id: self.id.server.to_string(), id: self.id.server.to_string(),
@@ -260,3 +268,74 @@ impl Member {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use iso8601_timestamp::{Duration, Timestamp};
use revolt_models::v0::DataCreateServer;
use crate::{Member, PartialMember, RemovalIntention, Server, User};
#[async_std::test]
async fn muted_member_rejoin() {
database_test!(|db| async move {
match db {
crate::Database::Reference(_) => return,
crate::Database::MongoDb(_) => (),
}
let owner = User::create(&db, "Server Owner".to_string(), None, None)
.await
.unwrap();
let kickable_user = User::create(&db, "Member".to_string(), None, None)
.await
.unwrap();
let server = Server::create(
&db,
DataCreateServer {
name: "Server".to_string(),
description: None,
nsfw: None,
},
&owner,
false,
)
.await
.unwrap()
.0;
Member::create(&db, &server, &owner, None).await.unwrap();
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
kickable_member
.update(
&db,
PartialMember {
timeout: Some(Timestamp::now_utc() + Duration::minutes(5)),
..Default::default()
},
vec![],
)
.await
.unwrap();
assert!(kickable_member.in_timeout());
kickable_member
.remove(&db, &server, RemovalIntention::Kick, false)
.await
.unwrap();
let kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
assert!(kickable_member.in_timeout())
});
}
}
@@ -73,13 +73,13 @@ impl ChunkedServerMembersGenerator {
#[async_trait] #[async_trait]
pub trait AbstractServerMembers: Sync + Send { pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database /// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()>; async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>>;
/// Fetch a server member by their id /// Fetch a server member by their id
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>; async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
/// Fetch all members in a server /// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>; async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all members in a server as an iterator /// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked( async fn fetch_all_members_chunked(
@@ -100,10 +100,10 @@ pub trait AbstractServerMembers: Sync + Send {
) -> Result<ChunkedServerMembersGenerator>; ) -> Result<ChunkedServerMembersGenerator>;
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>; async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>>;
/// Fetch multiple members by their ids /// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>; async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>>;
/// Fetch member count of a server /// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>; async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
@@ -119,6 +119,14 @@ pub trait AbstractServerMembers: Sync + Send {
remove: Vec<FieldsMember>, remove: Vec<FieldsMember>,
) -> Result<()>; ) -> Result<()>;
/// Delete a server member by their id /// Marks a user as no longer a member of a server, while retaining the database value.
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>; /// This is used to keep information such as timeouts in place, but will remove information such as join date and applied roles.
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Forcibly delete a server member by their id.
/// This will cancel any pending timeouts or other longer term actions, and they will not be reapplied on rejoin.
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Fetch all members who have been marked for deletion.
async fn remove_dangling_members(&self) -> Result<()>;
} }
@@ -1,4 +1,6 @@
use bson::Document;
use futures::StreamExt; use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use mongodb::options::ReadConcern; use mongodb::options::ReadConcern;
use revolt_result::Result; use revolt_result::Result;
@@ -11,9 +13,42 @@ static COL: &str = "server_members";
#[async_trait] #[async_trait]
impl AbstractServerMembers for MongoDb { impl AbstractServerMembers for MongoDb {
/// Insert a new server member into the database /// Insert a new server member (or use the existing member if one is found)
async fn insert_member(&self, member: &Member) -> Result<()> { async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
query!(self, insert_one, COL, &member).map(|_| ()) let existing: Result<Option<Document>> = query!(
self,
find_one,
COL,
doc! {
"_id.server": &member.id.server,
"_id.user": &member.id.user,
"pending_deletion_at": {"$exists": true}
}
);
// Update the existing record if it exist, otherwise make a new record
if existing.is_ok_and(|x| x.is_some()) {
self.col::<Member>(COL)
.find_one_and_update(
doc! {
"_id.server": &member.id.server,
"_id.user": &member.id.user,
},
doc! {
"$set": {
"joined_at": member.joined_at.duration_since(Timestamp::UNIX_EPOCH).whole_seconds(),
},
"$unset": {
"pending_deletion_at": ""
}
},
)
.return_document(mongodb::options::ReturnDocument::After)
.await
.map_err(|_| create_database_error!("update_one", COL))
} else {
query!(self, insert_one, COL, &member).map(|_| ())?;
Ok(None)
}
} }
/// Fetch a server member by their id /// Fetch a server member by their id
@@ -24,18 +59,20 @@ impl AbstractServerMembers for MongoDb {
COL, COL,
doc! { doc! {
"_id.server": server_id, "_id.server": server_id,
"_id.user": user_id "_id.user": user_id,
"pending_deletion_at": {"$exists": false}
} }
)? )?
.ok_or_else(|| create_error!(NotFound)) .ok_or_else(|| create_error!(NotFound))
} }
/// Fetch all members in a server /// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> { async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find(doc! { .find(doc! {
"_id.server": server_id "_id.server": server_id,
"pending_deletion_at": {"$exists": false}
}) })
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
@@ -139,11 +176,12 @@ impl AbstractServerMembers for MongoDb {
} }
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> { async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find(doc! { .find(doc! {
"_id.user": user_id "_id.user": user_id,
"pending_deletion_at": {"$exists": false}
}) })
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
@@ -159,11 +197,12 @@ impl AbstractServerMembers for MongoDb {
} }
/// Fetch multiple members by their ids /// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> { async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find(doc! { .find(doc! {
"_id.server": server_id, "_id.server": server_id,
"pending_deletion_at": {"$exists": false},
"_id.user": { "_id.user": {
"$in": ids "$in": ids
} }
@@ -185,7 +224,8 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> { async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL) self.col::<Member>(COL)
.count_documents(doc! { .count_documents(doc! {
"_id.server": server_id "_id.server": server_id,
"pending_deletion_at": {"$exists": false}
}) })
.await .await
.map(|c| c as usize) .map(|c| c as usize)
@@ -196,7 +236,8 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> { async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL) self.col::<Member>(COL)
.count_documents(doc! { .count_documents(doc! {
"_id.user": user_id "_id.user": user_id,
"pending_deletion_at": {"$exists": false}
}) })
.await .await
.map(|c| c as usize) .map(|c| c as usize)
@@ -225,8 +266,42 @@ impl AbstractServerMembers for MongoDb {
.map(|_| ()) .map(|_| ())
} }
/// Marks a member for deletion.
/// This will remove the record if the user has no pending actions (eg. timeout),
/// otherwise will slate the record for deletion by revolt_crond once the actions expire.
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let member = self.fetch_member(&id.server, &id.user).await;
if let Ok(member) = member {
if member.in_timeout() {
self.col::<Document>(COL)
.update_many(
doc! {
"_id.server": &id.server,
"_id.user": &id.user,
},
doc! {
"$set": {"pending_deletion_at": format!("{}", member.timeout.unwrap().format())},
"$unset": {
"joined_at": "",
"avatar": "",
"nickname": "",
"roles": ""
}
},
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
} else {
self.force_delete_member(id).await
}
} else {
Err(create_database_error!("fetch_member", COL))
}
}
/// Delete a server member by their id /// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> { async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
query!( query!(
self, self,
delete_one, delete_one,
@@ -238,11 +313,25 @@ impl AbstractServerMembers for MongoDb {
) )
.map(|_| ()) .map(|_| ())
} }
async fn remove_dangling_members(&self) -> Result<()> {
let now = Timestamp::now_utc();
let date = bson::to_bson(&now).expect("Failed to serialize timestamp");
self.col::<Document>(COL)
.delete_many(doc! {
"pending_deletion_at": {"$lt": date}
})
.await
.map(|_| ())
.map_err(|_| create_database_error!("count_documents", COL))
}
} }
impl IntoDocumentPath for FieldsMember { impl IntoDocumentPath for FieldsMember {
fn as_path(&self) -> Option<&'static str> { fn as_path(&self) -> Option<&'static str> {
Some(match self { Some(match self {
FieldsMember::JoinedAt => "joined_at",
FieldsMember::Avatar => "avatar", FieldsMember::Avatar => "avatar",
FieldsMember::Nickname => "nickname", FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles", FieldsMember::Roles => "roles",
@@ -8,13 +8,13 @@ use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
#[async_trait] #[async_trait]
impl AbstractServerMembers for ReferenceDb { impl AbstractServerMembers for ReferenceDb {
/// Insert a new server member into the database /// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()> { async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
let mut server_members = self.server_members.lock().await; let mut server_members = self.server_members.lock().await;
if server_members.contains_key(&member.id) { if server_members.contains_key(&member.id) {
Err(create_database_error!("insert", "member")) Err(create_database_error!("insert", "member"))
} else { } else {
server_members.insert(member.id.clone(), member.clone()); server_members.insert(member.id.clone(), member.clone());
Ok(()) Ok(None)
} }
} }
@@ -31,7 +31,7 @@ impl AbstractServerMembers for ReferenceDb {
} }
/// Fetch all members in a server /// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> { async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await; let server_members = self.server_members.lock().await;
Ok(server_members Ok(server_members
.values() .values()
@@ -105,7 +105,7 @@ impl AbstractServerMembers for ReferenceDb {
} }
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> { async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await; let server_members = self.server_members.lock().await;
Ok(server_members Ok(server_members
.values() .values()
@@ -115,7 +115,7 @@ impl AbstractServerMembers for ReferenceDb {
} }
/// Fetch multiple members by their ids /// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> { async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await; let server_members = self.server_members.lock().await;
Ok(ids Ok(ids
.iter() .iter()
@@ -169,8 +169,26 @@ impl AbstractServerMembers for ReferenceDb {
} }
} }
/// Soft delete a member
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await;
let member = server_members.get_mut(id);
if let Some(member) = member {
if member.in_timeout() {
panic!("Soft deletion is not implemented.")
} else if server_members.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
/// Delete a server member by their id /// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> { async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await; let mut server_members = self.server_members.lock().await;
if server_members.remove(id).is_some() { if server_members.remove(id).is_some() {
Ok(()) Ok(())
@@ -178,4 +196,8 @@ impl AbstractServerMembers for ReferenceDb {
Err(create_error!(NotFound)) Err(create_error!(NotFound))
} }
} }
async fn remove_dangling_members(&self) -> Result<()> {
todo!()
}
} }
@@ -1,14 +1,20 @@
use axum::{extract::FromRequestParts, http::request::Parts}; use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts};
use revolt_result::{create_error, Error, Result}; use revolt_result::{create_error, Error, Result};
use crate::{Database, User}; use crate::{Database, User};
#[async_trait::async_trait] #[async_trait::async_trait]
impl FromRequestParts<Database> for User { impl<S> FromRequestParts<S> for User
where
Database: FromRef<S>,
S: Send + Sync
{
type Rejection = Error; type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> { async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> {
let db = Database::from_ref(state);
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) { if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
let bot = db.fetch_bot_by_token(bot_token).await?; let bot = db.fetch_bot_by_token(bot_token).await?;
db.fetch_user(&bot.id).await db.fetch_user(&bot.id).await
@@ -705,8 +705,6 @@ impl User {
duration_days: Option<usize>, duration_days: Option<usize>,
reason: Option<Vec<String>>, reason: Option<Vec<String>>,
) -> Result<()> { ) -> Result<()> {
// TODO: authifier Error should implement Error
let authifier = db.clone().to_authifier().await; let authifier = db.clone().to_authifier().await;
let mut account = authifier let mut account = authifier
.database .database
@@ -38,7 +38,6 @@ impl<'r> FromRequest<'r> for User {
if let Some(user) = user { if let Some(user) = user {
Outcome::Success(user.clone()) Outcome::Success(user.clone())
} else { } else {
request.local_cache(|| Some(create_error!(InvalidSession)));
Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession)) Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession))
} }
} }
@@ -707,6 +707,7 @@ impl From<crate::FieldsMember> for FieldsMember {
crate::FieldsMember::Nickname => FieldsMember::Nickname, crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles, crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout, crate::FieldsMember::Timeout => FieldsMember::Timeout,
crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt,
} }
} }
} }
@@ -718,6 +719,7 @@ impl From<FieldsMember> for crate::FieldsMember {
FieldsMember::Nickname => crate::FieldsMember::Nickname, FieldsMember::Nickname => crate::FieldsMember::Nickname,
FieldsMember::Roles => crate::FieldsMember::Roles, FieldsMember::Roles => crate::FieldsMember::Roles,
FieldsMember::Timeout => crate::FieldsMember::Timeout, FieldsMember::Timeout => crate::FieldsMember::Timeout,
FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt,
} }
} }
} }
@@ -113,8 +113,6 @@ impl<'r> FromRequest<'r> for IdempotencyKey {
let idempotency = IdempotencyKey { key }; 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() {
request.local_cache(|| Some(create_error!(DuplicateNonce)));
return Outcome::Error((Status::Conflict, create_error!(DuplicateNonce))); return Outcome::Error((Status::Conflict, create_error!(DuplicateNonce)));
} }
@@ -39,7 +39,9 @@ pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String>
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(), LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(), LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(), LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(), LoadedFixture::ServerMember(member) => {
db.insert_or_merge_member(&member).await.unwrap();
}
} }
} }
+4 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-files" name = "revolt-files"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -14,16 +14,16 @@ imagesize = "0.13.0"
tempfile = "3.12.0" tempfile = "3.12.0"
base64 = "0.22.1" base64 = "0.22.1"
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = "0.10.3"
typenum = "1.17.0" typenum = "1.17.0"
aws-config = "1.5.5" aws-config = "1.5.5"
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] } aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.8.8", path = "../config", features = [ revolt-config = { version = "0.8.9", path = "../config", features = [
"report-macros", "report-macros",
] } ] }
revolt-result = { version = "0.8.8", path = "../result", features = ["sentry"] } revolt-result = { version = "0.8.9", path = "../result" }
# image processing # image processing
jxl-oxide = "0.8.1" jxl-oxide = "0.8.1"
+34 -28
View File
@@ -6,7 +6,7 @@ use aes_gcm::{
}; };
use image::{DynamicImage, ImageBuffer}; use image::{DynamicImage, ImageBuffer};
use revolt_config::{config, report_internal_error, FilesS3}; use revolt_config::{config, report_internal_error, FilesS3};
use revolt_result::{create_error, Result, ToRevoltError}; use revolt_result::{create_error, Result};
use aws_sdk_s3::{ use aws_sdk_s3::{
config::{Credentials, Region}, config::{Credentials, Region},
@@ -55,12 +55,13 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
// Send a request for the file // Send a request for the file
let mut obj = let mut obj =
client.get_object().bucket(bucket_id).key(path).send().await.to_internal_error()?; report_internal_error!(client.get_object().bucket(bucket_id).key(path).send().await)?;
// Read the file from remote // Read the file from remote
let mut buf = vec![]; let mut buf = vec![];
while let Some(bytes) = obj.body.next().await { while let Some(bytes) = obj.body.next().await {
buf.write_all(&bytes.to_internal_error()?).to_internal_error()?; let data = report_internal_error!(bytes)?;
report_internal_error!(buf.write_all(&data))?;
// is there a more efficient way to do this? // is there a more efficient way to do this?
// we just want the Vec<u8> // we just want the Vec<u8>
} }
@@ -77,7 +78,10 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
// Decrypt the file // Decrypt the file
create_cipher(&config.files.encryption_key) create_cipher(&config.files.encryption_key)
.decrypt_in_place(nonce, b"", &mut buf) .decrypt_in_place(nonce, b"", &mut buf)
.to_internal_error()?; .map_err(|_| create_error!(InternalError))?;
// Remove the authentication tag bytes that were added during encryption
buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES);
Ok(buf) Ok(buf)
} }
@@ -96,17 +100,18 @@ pub async fn upload_to_s3(bucket_id: &str, path: &str, buf: &[u8]) -> Result<Str
// Encrypt the file in place // Encrypt the file in place
create_cipher(&config.files.encryption_key) create_cipher(&config.files.encryption_key)
.encrypt_in_place(&nonce, b"", &mut buf) .encrypt_in_place(&nonce, b"", &mut buf)
.to_internal_error()?; .map_err(|_| create_error!(InternalError))?;
// Upload the file to remote // Upload the file to remote
client report_internal_error!(
.put_object() client
.bucket(bucket_id) .put_object()
.key(path) .bucket(bucket_id)
.body(buf.into()) .key(path)
.send() .body(buf.into())
.await .send()
.to_internal_error()?; .await
)?;
Ok(BASE64_STANDARD.encode(nonce)) Ok(BASE64_STANDARD.encode(nonce))
} }
@@ -116,13 +121,14 @@ pub async fn delete_from_s3(bucket_id: &str, path: &str) -> Result<()> {
let config = config().await; let config = config().await;
let client = create_client(config.files.s3); let client = create_client(config.files.s3);
client report_internal_error!(
.delete_object() client
.bucket(bucket_id) .delete_object()
.key(path) .bucket(bucket_id)
.send() .key(path)
.await .send()
.to_internal_error()?; .await
)?;
Ok(()) Ok(())
} }
@@ -142,7 +148,8 @@ pub fn image_size(f: &NamedTempFile) -> Option<(usize, usize)> {
pub fn image_size_vec(v: &[u8], mime: &str) -> Option<(usize, usize)> { pub fn image_size_vec(v: &[u8], mime: &str) -> Option<(usize, usize)> {
match mime { match mime {
"image/svg+xml" => { "image/svg+xml" => {
let tree = usvg::Tree::from_data(v, &Default::default()).to_internal_error().ok()?; let tree =
report_internal_error!(usvg::Tree::from_data(v, &Default::default())).ok()?;
let size = tree.size(); let size = tree.size();
Some((size.width() as usize, size.height() as usize)) Some((size.width() as usize, size.height() as usize))
@@ -217,9 +224,9 @@ pub fn decode_image<R: Read + BufRead + Seek>(reader: &mut R, mime: &str) -> Res
"image/svg+xml" => { "image/svg+xml" => {
// usvg doesn't support Read trait so copy to buffer // usvg doesn't support Read trait so copy to buffer
let mut buf = Vec::new(); let mut buf = Vec::new();
reader.read_to_end(&mut buf).to_internal_error()?; report_internal_error!(reader.read_to_end(&mut buf))?;
let tree = usvg::Tree::from_data(&buf, &Default::default()).to_internal_error()?; let tree = report_internal_error!(usvg::Tree::from_data(&buf, &Default::default()))?;
let size = tree.size(); let size = tree.size();
let mut pixmap = Pixmap::new(size.width() as u32, size.height() as u32) let mut pixmap = Pixmap::new(size.width() as u32, size.height() as u32)
.ok_or_else(|| create_error!(ImageProcessingFailed))?; .ok_or_else(|| create_error!(ImageProcessingFailed))?;
@@ -237,11 +244,10 @@ pub fn decode_image<R: Read + BufRead + Seek>(reader: &mut R, mime: &str) -> Res
)) ))
} }
// Check if we can read using image-rs crate // Check if we can read using image-rs crate
_ => image::ImageReader::new(reader) _ => report_internal_error!(report_internal_error!(
.with_guessed_format() image::ImageReader::new(reader).with_guessed_format()
.to_internal_error()? )?
.decode() .decode()),
.to_internal_error()
} }
} }
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,8 +20,8 @@ default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.8", path = "../config" } revolt-config = { version = "0.8.9", path = "../config" }
revolt-permissions = { version = "0.8.8", path = "../permissions" } revolt-permissions = { version = "0.8.9", path = "../permissions" }
# Utility # Utility
regex = "1.11" regex = "1.11"
-2
View File
@@ -7,7 +7,6 @@ mod embeds;
mod emojis; mod emojis;
mod files; mod files;
mod messages; mod messages;
mod onboard;
mod policy_changes; mod policy_changes;
mod safety_reports; mod safety_reports;
mod server_bans; mod server_bans;
@@ -25,7 +24,6 @@ pub use embeds::*;
pub use emojis::*; pub use emojis::*;
pub use files::*; pub use files::*;
pub use messages::*; pub use messages::*;
pub use onboard::*;
pub use policy_changes::*; pub use policy_changes::*;
pub use safety_reports::*; pub use safety_reports::*;
pub use server_bans::*; pub use server_bans::*;
-15
View File
@@ -1,15 +0,0 @@
auto_derived!(
/// # New User Data
#[derive(validator::Validate)]
pub struct DataOnboard {
/// New username which will be used to identify the user on the platform
#[validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")]
pub username: String,
}
/// # Onboarding Status
pub struct DataHello {
/// Whether onboarding is required
pub onboarding: bool,
}
);
@@ -1,17 +1,6 @@
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
auto_derived!( auto_derived!(
/// # Report Data
#[derive(validator::Validate)]
pub struct DataReportContent {
/// Content being reported
pub content: ReportedContent,
/// Additional report description
#[validate(length(min = 0, max = 1000))]
#[serde(default)]
pub additional_context: String,
}
/// User-generated platform moderation report /// User-generated platform moderation report
pub struct Report { pub struct Report {
/// Unique Id /// Unique Id
@@ -77,6 +77,7 @@ auto_derived!(
Avatar, Avatar,
Roles, Roles,
Timeout, Timeout,
JoinedAt,
} }
/// Member removal intention /// Member removal intention
+1 -19
View File
@@ -1,4 +1,4 @@
use super::{Channel, File, Member, User, RE_COLOUR}; use super::{Channel, File, RE_COLOUR};
use revolt_permissions::{Override, OverrideField}; use revolt_permissions::{Override, OverrideField};
use std::collections::HashMap; use std::collections::HashMap;
@@ -293,22 +293,4 @@ auto_derived!(
pub struct DataEditRoleRanks { pub struct DataEditRoleRanks {
pub ranks: Vec<String>, pub ranks: Vec<String>,
} }
/// # Query Parameters
#[derive(FromForm)]
pub struct OptionsQueryMembers {
/// String to search for
pub query: String,
/// Discourage use of this API
pub experimental_api: bool,
}
/// # Query members by name
pub struct MemberQueryResponse {
/// List of members
pub members: Vec<Member>,
/// List of users
pub users: Vec<User>,
}
); );
+3 -12
View File
@@ -255,12 +255,14 @@ auto_derived!(
pub flags: i32, pub flags: i32,
} }
/// Mutual friends and servers response /// Mutual friends, servers, groups and DMs response
pub struct MutualResponse { pub struct MutualResponse {
/// Array of mutual user IDs that both users are friends with /// Array of mutual user IDs that both users are friends with
pub users: Vec<String>, pub users: Vec<String>,
/// Array of mutual server IDs that both users are in /// Array of mutual server IDs that both users are in
pub servers: Vec<String>, pub servers: Vec<String>,
/// Array of mutual group and dm IDs that both users are in
pub channels: Vec<String>,
} }
/// Bot information for if the user is a bot /// Bot information for if the user is a bot
@@ -275,17 +277,6 @@ auto_derived!(
/// Username and discriminator combo separated by # /// Username and discriminator combo separated by #
pub username: String, pub username: String,
} }
/// # Username Information
#[derive(Validate)]
pub struct DataChangeUsername {
/// New username
#[validate(length(min = 2, max = 32), regex = "super::RE_USERNAME")]
pub username: String,
/// Current account password
#[validate(length(min = 8, max = 1024))]
pub password: String,
}
); );
pub trait CheckRelationship { pub trait CheckRelationship {
+3 -2
View File
@@ -1,8 +1,9 @@
[package] [package]
name = "revolt-parser" name = "revolt-parser"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "MIT"
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Message Parser" description = "Revolt Backend: Message Parser"
[dependencies] [dependencies]
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+34 -23
View File
@@ -4,15 +4,18 @@ use logos::Logos;
#[derive(Debug, Clone, Logos, PartialEq)] #[derive(Debug, Clone, Logos, PartialEq)]
#[logos(skip "\n")] #[logos(skip "\n")]
pub enum MessageToken { #[logos(subpattern id="[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")]
pub enum MessageToken<'a> {
#[token("\\")] #[token("\\")]
Escape, Escape,
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())] #[regex("```[^`\n]*", |_| 3)]
#[regex("``", |_| 2)]
#[regex("`", |_| 1)]
CodeblockMarker(usize), CodeblockMarker(usize),
#[regex("<@[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())] #[regex("<@(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1])]
UserMention(String), UserMention(&'a str),
#[regex("<%[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())] #[regex("<%(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1],)]
RoleMention(String), RoleMention(&'a str),
#[token("@everyone")] #[token("@everyone")]
MentionEveryone, MentionEveryone,
#[token("@online")] #[token("@online")]
@@ -27,13 +30,13 @@ pub struct MessageResults {
pub mentions_online: bool pub mentions_online: bool
} }
struct MessageParserIterator<I> { struct MessageParserIterator<'a, I> {
inner: I, inner: I,
temp: VecDeque<MessageToken> temp: VecDeque<MessageToken<'a>>
} }
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> { impl<'a, I: Iterator<Item = MessageToken<'a>>> Iterator for MessageParserIterator<'a, I> {
type Item = MessageToken; type Item = MessageToken<'a>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if !self.temp.is_empty() { if !self.temp.is_empty() {
@@ -80,8 +83,8 @@ pub fn parse_message(text: &str) -> MessageResults {
match token { match token {
MessageToken::Escape => {} MessageToken::Escape => {}
MessageToken::CodeblockMarker(_) => {}, MessageToken::CodeblockMarker(_) => {},
MessageToken::UserMention(id) => { results.user_mentions.insert(id); }, MessageToken::UserMention(id) => { results.user_mentions.insert(id.to_string()); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); }, MessageToken::RoleMention(id) => { results.role_mentions.insert(id.to_string()); },
MessageToken::MentionEveryone => results.mentions_everyone = true, MessageToken::MentionEveryone => results.mentions_everyone = true,
MessageToken::MentionOnline => results.mentions_online = true, MessageToken::MentionOnline => results.mentions_online = true,
}; };
@@ -106,7 +109,7 @@ mod tests {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>(); let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1); assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
} }
#[test] #[test]
@@ -114,7 +117,7 @@ mod tests {
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>(); let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1); assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
} }
#[test] #[test]
@@ -138,8 +141,8 @@ mod tests {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>(); let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>();
assert_eq!(output.len(), 4); assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::MentionEveryone); assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline); assert_eq!(output[3], MessageToken::MentionOnline);
} }
@@ -149,8 +152,8 @@ mod tests {
let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>(); let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
assert_eq!(output.len(), 4); assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::MentionEveryone); assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline); assert_eq!(output[3], MessageToken::MentionOnline);
} }
@@ -170,15 +173,14 @@ mod tests {
assert_eq!(output.len(), 5); assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(3)); assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[3], MessageToken::MentionEveryone); assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline); assert_eq!(output[4], MessageToken::MentionOnline);
} }
#[test] #[test]
fn test_inline_codeblock_no_mentions() { fn test_inline_codeblock_no_mentions() {
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>(); let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>();
assert_eq!(output.len(), 2); assert_eq!(output.len(), 2);
@@ -192,12 +194,21 @@ mod tests {
assert_eq!(output.len(), 5); assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(1)); assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string())); assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[3], MessageToken::MentionEveryone); assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline); assert_eq!(output[4], MessageToken::MentionOnline);
} }
#[test]
fn test_codeblock_with_language_no_mentions() {
let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online```").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
}
#[test] #[test]
fn test_double_inline_codeblock() { fn test_double_inline_codeblock() {
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>(); let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.8.8", path = "../result" } revolt-result = { version = "0.8.9", path = "../result" }
# Utility # Utility
auto_ops = "0.3.0" auto_ops = "0.3.0"
+9 -1
View File
@@ -8,10 +8,18 @@ pub use server::*;
pub use user::*; pub use user::*;
/// Holds a permission value to manipulate. /// Holds a permission value to manipulate.
#[derive(Clone, Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PermissionValue(u64); pub struct PermissionValue(u64);
impl PermissionValue { impl PermissionValue {
pub fn from_raw(value: u64) -> Self {
Self(value)
}
pub fn into_raw(self) -> u64 {
self.0
}
/// Apply a given override to this value /// Apply a given override to this value
pub fn apply(&mut self, v: Override) { pub fn apply(&mut self, v: Override) {
self.allow(v.allow); self.allow(v.allow);
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -16,7 +16,7 @@ redis-is-patched = []
async-std = { version = "1.8.0", features = ["attributes"] } async-std = { version = "1.8.0", features = ["attributes"] }
# Config for loading Redis URI # Config for loading Redis URI
revolt-config = { version = "0.8.8", path = "../config" } revolt-config = { version = "0.8.9", path = "../config" }
[dependencies] [dependencies]
# Utility # Utility
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "revolt-ratelimits"
version = "0.8.9"
edition = "2024"
[features]
rocket = ["dep:rocket", "dep:revolt_rocket_okapi", "revolt-database/rocket-impl"]
axum = ["dep:axum", "revolt-database/axum-impl"]
default = ["rocket", "axum"]
[dependencies]
revolt-database = { version = "0.8.9", path = "../database"}
revolt-result = { version = "0.8.9", path = "../result" }
revolt-config = { version = "0.8.9", path = "../config" }
rocket = { version = "0.5.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
axum = { version = "0.7.5", optional = true, features = ["macros"] }
serde = { version = "1", features = ["derive"] }
authifier = { version = "1.0.15" }
dashmap = "5.2.0"
async-trait = "0.1.81"
log = "0.4"
+194
View File
@@ -0,0 +1,194 @@
use std::net::SocketAddr;
use async_trait::async_trait;
use axum::{
Json, RequestPartsExt, Router,
body::Body,
extract::{ConnectInfo, FromRef, FromRequestParts, State},
http::{HeaderValue, Request, StatusCode, request::Parts},
middleware::Next,
response::{IntoResponse, Response},
routing::get,
};
use revolt_database::{Database, User};
use revolt_config::config;
use crate::ratelimiter::{RatelimitInformation, Ratelimiter, RequestKind};
#[derive(Clone, Copy)]
pub struct AxumRequestKind;
impl RequestKind for AxumRequestKind {
type R<'a> = Parts;
}
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<AxumRequestKind>;
fn to_ip(parts: &Parts) -> String {
parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.map(|info| info.ip().to_string())
.unwrap_or_default()
}
async fn to_real_ip(parts: &Parts) -> String {
if config().await.api.security.trust_cloudflare {
parts
.headers
.get("CF-Connecting-IP")
.map(|x| x.to_str().unwrap().to_string())
.unwrap_or_else(|| to_ip(parts))
} else {
to_ip(parts)
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for Ratelimiter
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
type Rejection = Json<Ratelimiter>;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
if parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.is_none()
{
let storage = RatelimitStorage::from_ref(state);
let identifier = if let Ok(user) = parts.extract_with_state::<User, _>(state).await {
user.id
} else {
to_real_ip(parts).await
};
let (bucket, resource) = storage.resolver.resolve_bucket(parts);
let limit = storage.resolver.resolve_bucket_limit(bucket);
let ratelimiter =
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource));
parts.extensions.insert(ratelimiter.map_err(Json));
};
*parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.unwrap()
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for RatelimitInformation
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
type Rejection = Json<RatelimitInformation>;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
if parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.is_none()
{
let ratelimiter = parts.extract_with_state::<Ratelimiter, S>(state).await;
parts.extensions.insert(ratelimiter);
};
let ratelimiter = *parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.unwrap();
match ratelimiter {
Ok(ratelimter) => Ok(RatelimitInformation::Success(ratelimter)),
Err(ratelimiter) => Err(Json(RatelimitInformation::Failure {
retry_after: ratelimiter.reset,
})),
}
}
}
pub async fn ratelimit_middleware(
State(database): State<Database>,
State(ratelimit_storage): State<RatelimitStorage>,
request: Request<Body>,
next: Next,
) -> Response {
#[derive(axum::extract::FromRef)]
struct TempState {
database: Database,
ratelimit_storage: RatelimitStorage,
}
let state = TempState {
database,
ratelimit_storage,
};
let (mut parts, body) = request.into_parts();
let res = Ratelimiter::from_request_parts(&mut parts, &state).await;
let (Ok(ratelimiter) | Err(Json(ratelimiter))) = &res;
let mut response = if res.is_ok() {
let request = Request::from_parts(parts, body);
next.run(request).await
} else {
let ratelimit_info = RatelimitInformation::from_request_parts(&mut parts, &state).await;
ratelimit_info.map(Json).into_response()
};
let Ratelimiter {
key,
limit,
remaining,
reset,
} = ratelimiter;
let headers = response.headers_mut();
headers.insert(
"X-RateLimit-Limit",
HeaderValue::from_str(&limit.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Bucket",
HeaderValue::from_str(&key.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Remaining",
HeaderValue::from_str(&remaining.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Reset-After",
HeaderValue::from_str(&reset.to_string()).unwrap(),
);
if res.is_err() {
*response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
};
response
}
async fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
Json(info)
}
pub fn routes<S: Clone + Send + Sync + 'static>() -> Router<S>
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
Router::new().route("/ratelimit", get(ratelimit_info))
}
+7
View File
@@ -0,0 +1,7 @@
pub mod ratelimiter;
#[cfg(feature = "rocket")]
pub mod rocket;
#[cfg(feature = "axum")]
pub mod axum;
+145
View File
@@ -0,0 +1,145 @@
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
use std::ops::Add;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Serialize;
use dashmap::DashMap;
pub trait RequestKind {
type R<'a>;
}
pub trait RatelimitResolver<R>: Send + Sync {
fn resolve_bucket<'a>(&self, request: &'a R) -> (&'a str, Option<&'a str>);
fn resolve_bucket_limit(&self, bucket: &str) -> u32;
}
#[derive(Clone)]
pub struct RatelimitStorage<K: RequestKind> {
pub resolver: Arc<dyn for<'a> RatelimitResolver<K::R<'a>>>,
pub map: Arc<DashMap<u64, Entry>>,
}
impl<K: RequestKind> RatelimitStorage<K> {
pub fn new<R: for<'a> RatelimitResolver<K::R<'a>> + 'static>(resolver: R) -> Self {
Self {
resolver: Arc::new(resolver),
map: Arc::new(DashMap::new()),
}
}
}
/// Ratelimit Bucket
#[derive(Clone, Copy, Debug)]
pub struct Entry {
used: u32,
reset: u128,
}
/// Get the current time from Unix Epoch as a Duration
fn now() -> Duration {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards...")
}
impl Entry {
/// Find bucket by its key
pub fn from(map: &DashMap<u64, Entry>, key: u64) -> Entry {
map.get(&key).map(|x| *x).unwrap_or_else(|| Entry {
used: 0,
reset: now().add(Duration::from_secs(10)).as_millis(),
})
}
/// Deduct one unit from the bucket and save
pub fn deduct(&mut self) {
let current_time = now().as_millis();
if current_time > self.reset {
self.used = 1;
self.reset = now().add(Duration::from_secs(10)).as_millis();
} else {
self.used += 1;
}
}
/// Save information
pub fn save(self, map: &DashMap<u64, Entry>, key: u64) {
map.insert(key, self);
}
/// Get remaining units in the bucket
pub fn get_remaining(&self, limit: u32) -> u32 {
if now().as_millis() > self.reset {
limit
} else {
limit - self.used
}
}
/// Get how long bucket has until reset
pub fn left_until_reset(&self) -> u128 {
let current_time = now().as_millis();
self.reset.saturating_sub(current_time)
}
}
/// Ratelimit Guard
#[derive(Serialize, Clone, Copy, Debug)]
#[allow(dead_code)]
pub struct Ratelimiter {
pub key: u64,
pub limit: u32,
pub remaining: u32,
pub reset: u128,
}
impl Ratelimiter {
/// Generate guard from identifier and target bucket
pub fn from(
map: &DashMap<u64, Entry>,
identifier: &str,
limit: u32,
(bucket, resource): (&str, Option<&str>),
) -> Result<Ratelimiter, Ratelimiter> {
let mut key = DefaultHasher::new();
key.write(identifier.as_bytes());
key.write(bucket.as_bytes());
if let Some(id) = resource {
key.write(id.as_bytes());
}
let key = key.finish();
let mut entry = Entry::from(map, key);
let remaining = entry.get_remaining(limit);
let reset = entry.left_until_reset();
let mut ratelimiter = Ratelimiter {
key,
limit,
remaining,
reset,
};
if remaining == 0 {
return Err(ratelimiter);
}
entry.deduct();
entry.save(map, key);
ratelimiter.remaining -= 1;
ratelimiter.reset = entry.left_until_reset();
Ok(ratelimiter)
}
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum RatelimitInformation {
Success(Ratelimiter),
Failure { retry_after: u128 },
}
+163
View File
@@ -0,0 +1,163 @@
use async_trait::async_trait;
use log::info;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::uri::Origin;
use rocket::http::{Method, Status};
use rocket::request::{FromRequest, Outcome};
use rocket::serde::json::Json;
use rocket::{Data, Request, Response, State};
use revolt_config::config;
use revolt_rocket_okapi::r#gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use authifier::models::Session;
use crate::ratelimiter::RequestKind;
use crate::ratelimiter::{RatelimitInformation, Ratelimiter};
#[derive(Clone, Copy)]
pub struct RocketRequestKind;
impl RequestKind for RocketRequestKind {
type R<'a> = Request<'a>;
}
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<RocketRequestKind>;
/// Find the remote IP of the client
fn to_ip(request: &'_ rocket::Request<'_>) -> String {
request
.remote()
.map(|x| x.ip().to_string())
.unwrap_or_default()
}
/// Find the actual IP of the client
async fn to_real_ip(request: &'_ rocket::Request<'_>) -> String {
if config().await.api.security.trust_cloudflare {
request
.headers()
.get_one("CF-Connecting-IP")
.map(|x| x.to_string())
.unwrap_or_else(|| to_ip(request))
} else {
to_ip(request)
}
}
#[async_trait]
impl<'r> FromRequest<'r> for Ratelimiter {
type Error = Ratelimiter;
async fn from_request<'a>(request: &'r rocket::Request<'a>) -> Outcome<Self, Self::Error> {
let ratelimiter = request
.local_cache_async(async {
use rocket::outcome::Outcome;
let storage = request.guard::<&State<RatelimitStorage>>().await.unwrap();
let identifier = if let Outcome::Success(session) = request.guard::<Session>().await
{
session.id
} else {
to_real_ip(request).await
};
let (bucket, resource) = storage.resolver.resolve_bucket(request);
let limit = storage.resolver.resolve_bucket_limit(bucket);
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource))
})
.await;
match ratelimiter {
Ok(ratelimiter) => Outcome::Success(*ratelimiter),
Err(ratelimiter) => Outcome::Error((Status::TooManyRequests, *ratelimiter)),
}
}
}
impl OpenApiFromRequest<'_> for Ratelimiter {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::None)
}
}
/// Attach ratelimiter to the Rocket application
pub struct RatelimitFairing;
#[async_trait]
impl Fairing for RatelimitFairing {
fn info(&self) -> Info {
Info {
name: "Ratelimiter",
kind: Kind::Request | Kind::Response,
}
}
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
use rocket::outcome::Outcome;
if let Outcome::Error(_) = request.guard::<Ratelimiter>().await {
info!(
"User rate-limited on route {}! (IP = {:?})",
request.uri(),
to_real_ip(request).await
);
request.set_method(Method::Get);
request.set_uri(Origin::parse("/ratelimit").unwrap())
}
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
let guard = request.guard::<Ratelimiter>().await;
let (Outcome::Success(ratelimiter) | Outcome::Error((_, ratelimiter))) = guard else {
unreachable!()
};
let Ratelimiter {
key,
limit,
remaining,
reset,
} = ratelimiter;
response.set_raw_header("X-RateLimit-Limit", limit.to_string());
response.set_raw_header("X-RateLimit-Bucket", key.to_string());
response.set_raw_header("X-RateLimit-Remaining", remaining.to_string());
response.set_raw_header("X-RateLimit-Reset-After", reset.to_string());
if guard.is_error() {
response.set_status(Status::TooManyRequests);
}
}
}
#[async_trait]
impl<'r> FromRequest<'r> for RatelimitInformation {
type Error = u128;
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
let info = match request.guard::<Ratelimiter>().await {
Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter),
Outcome::Error((_, ratelimiter)) => RatelimitInformation::Failure {
retry_after: ratelimiter.reset,
},
_ => unreachable!(),
};
Outcome::Success(info)
}
}
#[rocket::get("/ratelimit")]
fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
Json(info)
}
pub fn routes() -> Vec<rocket::Route> {
rocket::routes![ratelimit_info]
}
+1 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -15,7 +15,6 @@ utoipa = ["dep:utoipa"]
rocket = ["dep:rocket", "dep:serde_json"] rocket = ["dep:rocket", "dep:serde_json"]
axum = ["dep:axum", "dep:serde_json"] axum = ["dep:axum", "dep:serde_json"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"] okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
sentry = ["dep:sentry"]
default = ["serde"] default = ["serde"]
@@ -35,6 +34,3 @@ revolt_okapi = { version = "0.9.1", optional = true }
# Axum # Axum
axum = { version = "0.7.5", optional = true } axum = { version = "0.7.5", optional = true }
# Sentry
sentry = { version = "0.31.5", optional = true }
+7 -15
View File
@@ -2,9 +2,10 @@ use axum::{http::StatusCode, response::IntoResponse, Json};
use crate::{Error, ErrorType}; use crate::{Error, ErrorType};
impl Error { /// HTTP response builder for Error enum
pub fn axum_status(&self) -> StatusCode { impl IntoResponse for Error {
match self.error_type { fn into_response(self) -> axum::response::Response {
let status = match self.error_type {
ErrorType::LabelMe => StatusCode::INTERNAL_SERVER_ERROR, ErrorType::LabelMe => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::AlreadyOnboarded => StatusCode::FORBIDDEN, ErrorType::AlreadyOnboarded => StatusCode::FORBIDDEN,
@@ -69,15 +70,11 @@ impl Error {
ErrorType::InvalidProperty => StatusCode::BAD_REQUEST, ErrorType::InvalidProperty => StatusCode::BAD_REQUEST,
ErrorType::InvalidSession => StatusCode::UNAUTHORIZED, ErrorType::InvalidSession => StatusCode::UNAUTHORIZED,
ErrorType::NotAuthenticated => StatusCode::UNAUTHORIZED, ErrorType::NotAuthenticated => StatusCode::UNAUTHORIZED,
ErrorType::Conflict => StatusCode::CONFLICT,
ErrorType::DuplicateNonce => StatusCode::CONFLICT, ErrorType::DuplicateNonce => StatusCode::CONFLICT,
ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST, ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST,
ErrorType::NotFound => StatusCode::NOT_FOUND, ErrorType::NotFound => StatusCode::NOT_FOUND,
ErrorType::NoEffect => StatusCode::OK, ErrorType::NoEffect => StatusCode::OK,
ErrorType::IOError => StatusCode::BAD_REQUEST, ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
ErrorType::UnprocessableEntity => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::DeserializationError { .. } => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::FailedValidation { .. } => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST, ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST, ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
@@ -87,13 +84,8 @@ impl Error {
ErrorType::FileTypeNotAllowed => StatusCode::BAD_REQUEST, ErrorType::FileTypeNotAllowed => StatusCode::BAD_REQUEST,
ErrorType::ImageProcessingFailed => StatusCode::INTERNAL_SERVER_ERROR, ErrorType::ImageProcessingFailed => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::NoEmbedData => StatusCode::BAD_REQUEST, ErrorType::NoEmbedData => StatusCode::BAD_REQUEST,
} };
}
}
/// HTTP response builder for Error enum (status, Json(&self)).into_response()
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
(self.axum_status(), Json(&self)).into_response()
} }
} }
+1 -64
View File
@@ -1,4 +1,3 @@
use std::panic::Location;
use std::fmt::Display; use std::fmt::Display;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
@@ -152,15 +151,9 @@ pub enum ErrorType {
InvalidSession, InvalidSession,
InvalidFlagValue, InvalidFlagValue,
NotAuthenticated, NotAuthenticated,
Conflict,
DuplicateNonce, DuplicateNonce,
NotFound, NotFound,
NoEffect, NoEffect,
IOError,
UnprocessableEntity,
DeserializationError {
error: String,
},
FailedValidation { FailedValidation {
error: String, error: String,
}, },
@@ -181,7 +174,7 @@ pub enum ErrorType {
// ? Feature flag disabled in the config // ? Feature flag disabled in the config
FeatureDisabled { FeatureDisabled {
feature: String, feature: String,
} },
} }
#[macro_export] #[macro_export]
@@ -204,62 +197,6 @@ macro_rules! create_database_error {
}; };
} }
pub trait ToRevoltError<T>: Sized {
fn capture_error(self) -> Self;
#[track_caller]
fn to_internal_error(self) -> Result<T, Error>;
}
impl<T, E: std::error::Error> ToRevoltError<T> for Result<T, E> {
fn capture_error(self) -> Self {
#[allow(unused_variables)]
self.inspect_err(|e| {
#[cfg(feature = "sentry")]
sentry::capture_error(e);
})
}
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self
.capture_error()
.map_err(|_| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
}
})
}
}
impl<T: std::error::Error> ToRevoltError<T> for Option<T> {
fn capture_error(self) -> Self {
#[allow(unused_variables)]
self.inspect(|e| {
#[cfg(feature = "sentry")]
sentry::capture_error(e);
})
}
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self
.capture_error()
.ok_or_else(|| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
}
})
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::ErrorType; use crate::ErrorType;
+7 -15
View File
@@ -8,9 +8,10 @@ use rocket::{
use crate::{Error, ErrorType}; use crate::{Error, ErrorType};
impl Error { /// HTTP response builder for Error enum
pub fn rocket_status(&self) -> Status { impl<'r> Responder<'r, 'static> for Error {
match self.error_type { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
let status = match self.error_type {
ErrorType::LabelMe => Status::InternalServerError, ErrorType::LabelMe => Status::InternalServerError,
ErrorType::AlreadyOnboarded => Status::Forbidden, ErrorType::AlreadyOnboarded => Status::Forbidden,
@@ -76,15 +77,11 @@ impl Error {
ErrorType::InvalidProperty => Status::BadRequest, ErrorType::InvalidProperty => Status::BadRequest,
ErrorType::InvalidSession => Status::Unauthorized, ErrorType::InvalidSession => Status::Unauthorized,
ErrorType::NotAuthenticated => Status::Unauthorized, ErrorType::NotAuthenticated => Status::Unauthorized,
ErrorType::Conflict => Status::Conflict,
ErrorType::DuplicateNonce => Status::Conflict, ErrorType::DuplicateNonce => Status::Conflict,
ErrorType::VosoUnavailable => Status::BadRequest, ErrorType::VosoUnavailable => Status::BadRequest,
ErrorType::NotFound => Status::NotFound, ErrorType::NotFound => Status::NotFound,
ErrorType::NoEffect => Status::Ok, ErrorType::NoEffect => Status::Ok,
ErrorType::IOError => Status::BadRequest, ErrorType::FailedValidation { .. } => Status::BadRequest,
ErrorType::UnprocessableEntity => Status::UnprocessableEntity,
ErrorType::DeserializationError { .. } => Status::UnprocessableEntity,
ErrorType::FailedValidation { .. } => Status::UnprocessableEntity,
ErrorType::FeatureDisabled { .. } => Status::BadRequest, ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest, ErrorType::ProxyError => Status::BadRequest,
@@ -93,13 +90,8 @@ impl Error {
ErrorType::FileTypeNotAllowed => Status::BadRequest, ErrorType::FileTypeNotAllowed => Status::BadRequest,
ErrorType::ImageProcessingFailed => Status::InternalServerError, ErrorType::ImageProcessingFailed => Status::InternalServerError,
ErrorType::NoEmbedData => Status::BadRequest, ErrorType::NoEmbedData => Status::BadRequest,
} };
}
}
/// HTTP response builder for Error enum
impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
// Serialize the error data structure into JSON. // Serialize the error data structure into JSON.
let string = serde_json::to_string(&self).unwrap(); let string = serde_json::to_string(&self).unwrap();
@@ -107,7 +99,7 @@ impl<'r> Responder<'r, 'static> for Error {
Response::build() Response::build()
.sized_body(string.len(), Cursor::new(string)) .sized_body(string.len(), Cursor::new(string))
.header(ContentType::new("application", "json")) .header(ContentType::new("application", "json"))
.status(self.rocket_status()) .status(status)
.ok() .ok()
} }
} }
+5 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-crond" name = "revolt-crond"
version = "0.8.8" version = "0.8.9"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
edition = "2021" edition = "2021"
@@ -16,7 +16,7 @@ log = "0.4"
tokio = { version = "1" } tokio = { version = "1" }
# Core # Core
revolt-database = { version = "0.8.8", path = "../../core/database" } revolt-database = { version = "0.8.9", path = "../../core/database" }
revolt-result = { version = "0.8.8", path = "../../core/result" } revolt-result = { version = "0.8.9", path = "../../core/result" }
revolt-config = { version = "0.8.8", path = "../../core/config" } revolt-config = { version = "0.8.9", path = "../../core/config" }
revolt-files = { version = "0.8.8", path = "../../core/files" } revolt-files = { version = "0.8.9", path = "../../core/files" }
+11 -23
View File
@@ -1,32 +1,20 @@
use std::{future::Future, time::Duration}; use revolt_config::configure;
use revolt_database::DatabaseInfo;
use revolt_config::{configure, capture_error};
use revolt_database::{Database, DatabaseInfo};
use revolt_result::Result; use revolt_result::Result;
use tasks::{file_deletion, prune_dangling_files}; use tasks::{file_deletion, prune_dangling_files, prune_members};
use tokio::{join, time::sleep}; use tokio::try_join;
pub mod tasks; pub mod tasks;
pub async fn cron_task_wrapper<Fut: Future<Output = Result<()>>>(func: fn(Database) -> Fut, db: Database) {
loop {
if let Err(error) = func(db.clone()).await {
log::error!("cron task failed unexpectidly: {error:?}\nRetrying after 60s");
capture_error(&error);
}
sleep(Duration::from_secs(60)).await;
}
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> Result<()> {
configure!(crond); configure!(crond);
let db = DatabaseInfo::Auto.connect().await.expect("database"); let db = DatabaseInfo::Auto.connect().await.expect("database");
try_join!(
join!( file_deletion::task(db.clone()),
cron_task_wrapper(file_deletion::task, db.clone()), prune_dangling_files::task(db.clone()),
cron_task_wrapper(prune_dangling_files::task, db.clone()), prune_members::task(db.clone())
); )
.map(|_| ())
} }
+1
View File
@@ -1,2 +1,3 @@
pub mod file_deletion; pub mod file_deletion;
pub mod prune_dangling_files; pub mod prune_dangling_files;
pub mod prune_members;
@@ -0,0 +1,18 @@
use std::time::Duration;
use log::warn;
use revolt_database::Database;
use revolt_result::Result;
use tokio::time::sleep;
pub async fn task(db: Database) -> Result<()> {
loop {
let success = db.remove_dangling_members().await;
if let Err(s) = success {
revolt_config::capture_error(&s);
warn!("Failed to prune dangling members: {:?}", &s);
}
sleep(Duration::from_secs(90)).await;
}
}
+6 -6
View File
@@ -1,20 +1,20 @@
[package] [package]
name = "revolt-pushd" name = "revolt-pushd"
version = "0.8.8" version = "0.8.9"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
revolt-result = { version = "0.8.8", path = "../../core/result" } revolt-result = { version = "0.8.9", path = "../../core/result" }
revolt-config = { version = "0.8.8", path = "../../core/config", features = [ revolt-config = { version = "0.8.9", path = "../../core/config", features = [
"report-macros", "report-macros",
"anyhow" "anyhow"
] } ] }
revolt-database = { version = "0.8.8", path = "../../core/database" } revolt-database = { version = "0.8.9", path = "../../core/database" }
revolt-models = { version = "0.8.8", path = "../../core/models", features = [ revolt-models = { version = "0.8.9", path = "../../core/models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.8.8", path = "../../core/presence", features = [ revolt-presence = { version = "0.8.9", path = "../../core/presence", features = [
"redis-is-patched", "redis-is-patched",
] } ] }
@@ -123,26 +123,24 @@ impl AsyncConsumer for AckConsumer {
token: session.subscription.as_ref().unwrap().auth.clone(), token: session.subscription.as_ref().unwrap().auth.clone(),
extras: Default::default(), extras: Default::default(),
}; };
let raw_service_payload = serde_json::to_string(&service_payload);
match serde_json::to_string(&service_payload) { if let Ok(p) = raw_service_payload {
Ok(p) => { let args = BasicPublishArguments::new(
let args = BasicPublishArguments::new( config.pushd.exchange.as_str(),
config.pushd.exchange.as_str(), config.pushd.apn.queue.as_str(),
config.pushd.apn.queue.as_str(), )
) .finish();
.finish();
log::debug!( log::debug!(
"Publishing ack to apn session {}", "Publishing ack to apn session {}",
session.subscription.as_ref().unwrap().auth session.subscription.as_ref().unwrap().auth
); );
publish_message(self, p.into(), args).await; publish_message(self, p.into(), args).await;
}, } else {
Err(e) => { log::warn!("Failed to serialize ack badge update payload!");
log::warn!("Failed to serialize ack badge update payload!"); revolt_config::capture_error(&raw_service_payload.unwrap_err());
revolt_config::capture_error(&e);
}
} }
} }
} }
+3 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.8.8" version = "0.8.9"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -79,8 +79,9 @@ revolt-models = { path = "../core/models", features = [
"rocket", "rocket",
] } ] }
revolt-presence = { path = "../core/presence" } revolt-presence = { path = "../core/presence" }
revolt-result = { path = "../core/result", features = ["rocket", "okapi", "sentry"] } revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] } revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] }
[build-dependencies] [build-dependencies]
vergen = "7.5.0" vergen = "7.5.0"
+7 -3
View File
@@ -11,6 +11,7 @@ pub mod util;
use revolt_config::config; use revolt_config::config;
use revolt_database::events::client::EventV1; use revolt_database::events::client::EventV1;
use revolt_database::AMQP; use revolt_database::AMQP;
use revolt_ratelimits::rocket as ratelimiter;
use rocket::{Build, Rocket}; use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions}; use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics; use rocket_prometheus::PrometheusMetrics;
@@ -122,20 +123,23 @@ pub async fn web() -> Rocket<Build> {
let rocket = rocket::build(); let rocket = rocket::build();
let prometheus = PrometheusMetrics::new(); let prometheus = PrometheusMetrics::new();
// Ratelimits
let ratelimits = ratelimiter::RatelimitStorage::new(util::ratelimits::DeltaRatelimits);
routes::mount(config, rocket) routes::mount(config, rocket)
.attach(prometheus.clone()) .attach(prometheus.clone())
.mount("/metrics", prometheus) .mount("/metrics", prometheus)
.mount("/", rocket_cors::catch_all_options_routes()) .mount("/", rocket_cors::catch_all_options_routes())
.mount("/", util::ratelimiter::routes()) .mount("/", ratelimiter::routes())
.mount("/swagger/", swagger) .mount("/swagger/", swagger)
.mount("/0.8/swagger/", swagger_0_8) .mount("/0.8/swagger/", swagger_0_8)
.manage(authifier) .manage(authifier)
.manage(db) .manage(db)
.manage(amqp) .manage(amqp)
.manage(cors.clone()) .manage(cors.clone())
.attach(util::ratelimiter::RatelimitFairing) .manage(ratelimits)
.attach(ratelimiter::RatelimitFairing)
.attach(cors) .attach(cors)
.register("/", util::catchers::all_catchers())
.configure(rocket::Config { .configure(rocket::Config {
limits: rocket::data::Limits::default().limit("string", 5.megabytes()), limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
address: Ipv4Addr::new(0, 0, 0, 0).into(), address: Ipv4Addr::new(0, 0, 0, 0).into(),
+10 -4
View File
@@ -1,8 +1,9 @@
use crate::util::json::{Json, Validate};
use revolt_database::{Bot, Database, User}; use revolt_database::{Bot, Database, User};
use revolt_models::v0; use revolt_models::v0;
use revolt_result::Result; use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use rocket::State; use rocket::State;
use validator::Validate;
/// # Create Bot /// # Create Bot
/// ///
@@ -12,9 +13,14 @@ use rocket::State;
pub async fn create_bot( pub async fn create_bot(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
info: Validate<Json<v0::DataCreateBot>>, info: Json<v0::DataCreateBot>,
) -> Result<Json<v0::BotWithUserResponse>> { ) -> Result<Json<v0::BotWithUserResponse>> {
let info = info.into_inner().into_inner(); let info = info.into_inner();
info.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let (bot, user) = Bot::create(db, info.name, &user, None).await?; let (bot, user) = Bot::create(db, info.name, &user, None).await?;
Ok(Json(v0::BotWithUserResponse { Ok(Json(v0::BotWithUserResponse {
+9 -3
View File
@@ -3,7 +3,8 @@ use revolt_models::v0::{self, DataEditBot};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::State;
use crate::util::json::{Json, Validate}; use rocket::serde::json::Json;
use validator::Validate;
/// # Edit Bot /// # Edit Bot
/// ///
@@ -14,9 +15,14 @@ pub async fn edit_bot(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
data: Validate<Json<DataEditBot>>, data: Json<DataEditBot>,
) -> Result<Json<v0::BotWithUserResponse>> { ) -> Result<Json<v0::BotWithUserResponse>> {
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut bot = target.as_bot(db).await?; let mut bot = target.as_bot(db).await?;
if bot.owner != user.id { if bot.owner != user.id {
+1 -2
View File
@@ -1,8 +1,7 @@
use revolt_database::{util::reference::Reference, Database, User}; use revolt_database::{util::reference::Reference, Database, User};
use revolt_models::v0::FetchBotResponse; use revolt_models::v0::FetchBotResponse;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Fetch Bot /// # Fetch Bot
/// ///
+1 -1
View File
@@ -2,7 +2,7 @@ use futures::future::join_all;
use revolt_database::{Database, User}; use revolt_database::{Database, User};
use revolt_models::v0::OwnedBotsResponse; use revolt_models::v0::OwnedBotsResponse;
use revolt_result::Result; use revolt_result::Result;
use crate::util::json::Json; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
/// # Fetch Owned Bots /// # Fetch Owned Bots
+1 -1
View File
@@ -2,7 +2,7 @@ use revolt_database::{util::reference::Reference, Database, User};
use revolt_models::v0::PublicBot; use revolt_models::v0::PublicBot;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use crate::util::json::Json; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
/// # Fetch Public Bot /// # Fetch Public Bot
+3 -3
View File
@@ -8,7 +8,7 @@ use revolt_permissions::{
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::State;
use crate::util::json::Json; use rocket::serde::json::Json;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
/// # Invite Bot /// # Invite Bot
@@ -168,8 +168,8 @@ mod test {
.await; .await;
match event { match event {
EventV1::ServerMemberJoin { user, .. } => { EventV1::ServerMemberJoin { member, .. } => {
assert_eq!(bot.id, user); assert_eq!(bot.id, member.id.user);
} }
_ => unreachable!(), _ => unreachable!(),
} }
@@ -5,8 +5,8 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::{Json, Validate}; use validator::Validate;
/// # Edit Channel /// # Edit Channel
/// ///
@@ -18,9 +18,14 @@ pub async fn edit(
amqp: &State<AMQP>, amqp: &State<AMQP>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
data: Validate<Json<v0::DataEditChannel>>, data: Json<v0::DataEditChannel>,
) -> Result<Json<v0::Channel>> { ) -> Result<Json<v0::Channel>> {
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut channel = target.as_channel(db).await?; let mut channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
@@ -6,8 +6,7 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result; use revolt_result::Result;
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Fetch Channel /// # Fetch Channel
/// ///
@@ -2,8 +2,9 @@ use revolt_database::{Channel, Database, RelationshipStatus, User};
use revolt_models::v0; use revolt_models::v0;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use crate::util::json::{Json, Validate}; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
use validator::Validate;
/// # Create Group /// # Create Group
/// ///
@@ -13,13 +14,18 @@ use rocket::State;
pub async fn create_group( pub async fn create_group(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
data: Validate<Json<v0::DataCreateGroup>>, data: Json<v0::DataCreateGroup>,
) -> Result<Json<v0::Channel>> { ) -> Result<Json<v0::Channel>> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(create_error!(IsBot)); return Err(create_error!(IsBot));
} }
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
for target in &data.users { for target in &data.users {
match user.relationship_with(target) { match user.relationship_with(target) {
@@ -6,8 +6,7 @@ use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Create Invite /// # Create Invite
/// ///
@@ -5,8 +5,7 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Fetch Group Members /// # Fetch Group Members
/// ///
@@ -6,9 +6,9 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
use crate::util::json::{Json, Validate}; use validator::Validate;
/// # Bulk Delete Messages /// # Bulk Delete Messages
/// ///
@@ -23,9 +23,14 @@ pub async fn bulk_delete_messages(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
options: Validate<Json<v0::OptionsBulkDelete>>, options: Json<v0::OptionsBulkDelete>,
) -> Result<EmptyResponse> { ) -> Result<EmptyResponse> {
let options = options.into_inner().into_inner(); let options = options.into_inner();
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
for id in &options.ids { for id in &options.ids {
if ulid::Ulid::from_string(id) if ulid::Ulid::from_string(id)
@@ -7,8 +7,8 @@ use revolt_database::{
use revolt_models::v0::{self, Embed}; use revolt_models::v0::{self, Embed};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::{Json, Validate}; use validator::Validate;
/// # Edit Message /// # Edit Message
/// ///
@@ -20,9 +20,14 @@ pub async fn edit(
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
msg: Reference<'_>, msg: Reference<'_>,
edit: Validate<Json<v0::DataEditMessage>>, edit: Json<v0::DataEditMessage>,
) -> Result<Json<v0::Message>> { ) -> Result<Json<v0::Message>> {
let edit = edit.into_inner().into_inner(); let edit = edit.into_inner();
edit.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
Message::validate_sum( Message::validate_sum(
&edit.content, &edit.content,
@@ -5,8 +5,7 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Fetch Message /// # Fetch Message
/// ///
@@ -5,8 +5,8 @@ use revolt_database::{
use revolt_models::v0::{self, MessageSort}; use revolt_models::v0::{self, MessageSort};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::{Json, Validate}; use validator::Validate;
/// # Fetch Messages /// # Fetch Messages
/// ///
@@ -17,8 +17,14 @@ pub async fn query(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
options: Validate<v0::OptionsQueryMessages>, options: v0::OptionsQueryMessages,
) -> Result<Json<v0::BulkMessageResponse>> { ) -> Result<Json<v0::BulkMessageResponse>> {
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
if let Some(MessageSort::Relevance) = options.sort { if let Some(MessageSort::Relevance) = options.sort {
return Err(create_error!(InvalidOperation)); return Err(create_error!(InvalidOperation));
} }
@@ -37,7 +43,7 @@ pub async fn query(
sort, sort,
nearby, nearby,
include_users, include_users,
} = options.into_inner(); } = options;
Message::fetch_with_users( Message::fetch_with_users(
db, db,
@@ -5,8 +5,8 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::{Json, Validate}; use validator::Validate;
/// # Search for Messages /// # Search for Messages
/// ///
@@ -17,13 +17,18 @@ pub async fn search(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
options: Validate<Json<v0::DataMessageSearch>>, options: Json<v0::DataMessageSearch>,
) -> Result<Json<v0::BulkMessageResponse>> { ) -> Result<Json<v0::BulkMessageResponse>> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(create_error!(IsBot)); return Err(create_error!(IsBot));
} }
let options = options.into_inner().into_inner(); let options = options.into_inner();
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
if options.query.is_some() && options.pinned.is_some() { if options.query.is_some() && options.pinned.is_some() {
return Err(create_error!(InvalidOperation)) return Err(create_error!(InvalidOperation))
@@ -7,9 +7,10 @@ use revolt_database::{Interactions, Message, AMQP};
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::PermissionQuery; use revolt_permissions::PermissionQuery;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result; use revolt_result::{create_error, Result};
use crate::util::json::{Json, Validate}; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
use validator::Validate;
/// # Send Message /// # Send Message
/// ///
@@ -21,10 +22,15 @@ pub async fn message_send(
amqp: &State<AMQP>, amqp: &State<AMQP>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
data: Validate<Json<v0::DataMessageSend>>, data: Json<v0::DataMessageSend>,
idempotency: IdempotencyKey, idempotency: IdempotencyKey,
) -> Result<Json<v0::Message>> { ) -> Result<Json<v0::Message>> {
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
// Ensure we have permissions to send a message // Ensure we have permissions to send a message
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
@@ -5,8 +5,7 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Set Role Permission /// # Set Role Permission
/// ///
@@ -5,8 +5,7 @@ use revolt_database::{
use revolt_models::v0::{self, DataDefaultChannelPermissions}; use revolt_models::v0::{self, DataDefaultChannelPermissions};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Set Default Permission /// # Set Default Permission
/// ///
@@ -6,8 +6,7 @@ use revolt_database::{
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Join Call /// # Join Call
/// ///
@@ -7,9 +7,9 @@ use revolt_permissions::{
calculate_channel_permissions, ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS, calculate_channel_permissions, ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS,
}; };
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::{Json, Validate};
use ulid::Ulid; use ulid::Ulid;
use validator::Validate;
/// # Creates a webhook /// # Creates a webhook
/// ///
@@ -20,9 +20,14 @@ pub async fn create_webhook(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
target: Reference<'_>, target: Reference<'_>,
data: Validate<Json<v0::CreateWebhookBody>>, data: Json<v0::CreateWebhookBody>,
) -> Result<Json<v0::Webhook>> { ) -> Result<Json<v0::Webhook>> {
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
@@ -5,8 +5,7 @@ use revolt_database::{
use revolt_models::v0::Webhook; use revolt_models::v0::Webhook;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result; use revolt_result::Result;
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Gets all webhooks /// # Gets all webhooks
/// ///
@@ -3,8 +3,9 @@ use revolt_database::{util::permissions::DatabasePermissionQuery, Database, Emoj
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use validator::Validate;
use crate::util::json::{Json, Validate};
use rocket::{serde::json::Json, State};
/// # Create New Emoji /// # Create New Emoji
/// ///
@@ -15,11 +16,16 @@ pub async fn create_emoji(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
id: String, id: String,
data: Validate<Json<v0::DataCreateEmoji>>, data: Json<v0::DataCreateEmoji>,
) -> Result<Json<v0::Emoji>> { ) -> Result<Json<v0::Emoji>> {
let config = config().await; let config = config().await;
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
// Validate we have permission to write into parent // Validate we have permission to write into parent
match &data.parent { match &data.parent {
@@ -2,8 +2,7 @@ use revolt_database::{util::reference::Reference, Database};
use revolt_models::v0; use revolt_models::v0;
use revolt_result::Result; use revolt_result::Result;
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Fetch Emoji /// # Fetch Emoji
/// ///
@@ -1,8 +1,7 @@
use revolt_database::{util::reference::Reference, Channel, Database, Invite}; use revolt_database::{util::reference::Reference, Channel, Database, Invite};
use revolt_models::v0; use revolt_models::v0;
use revolt_result::Result; use revolt_result::Result;
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Fetch Invite /// # Fetch Invite
/// ///
@@ -1,8 +1,7 @@
use revolt_database::{util::reference::Reference, Channel, Database, Invite, Member, User, AMQP}; use revolt_database::{util::reference::Reference, Channel, Database, Invite, Member, User, AMQP};
use revolt_models::v0::{self, InviteJoinResponse}; use revolt_models::v0::{self, InviteJoinResponse};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
/// # Join Invite /// # Join Invite
/// ///
+25 -4
View File
@@ -1,11 +1,27 @@
use authifier::models::Session; use authifier::models::Session;
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_database::{Database, User}; use revolt_database::{Database, User};
use revolt_models::v0; use revolt_models::v0;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::{Json, Validate}; use serde::{Deserialize, Serialize};
use validator::Validate;
/// Regex for valid usernames
///
/// Block zero width space
/// Block lookalike characters
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
/// # New User Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataOnboard {
/// New username which will be used to identify the user on the platform
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
username: String,
}
/// # Complete Onboarding /// # Complete Onboarding
/// ///
@@ -16,13 +32,18 @@ pub async fn complete(
db: &State<Database>, db: &State<Database>,
session: Session, session: Session,
user: Option<User>, user: Option<User>,
data: Validate<Json<v0::DataOnboard>>, data: Json<DataOnboard>,
) -> Result<Json<v0::User>> { ) -> Result<Json<v0::User>> {
if user.is_some() { if user.is_some() {
return Err(create_error!(AlreadyOnboarded)); return Err(create_error!(AlreadyOnboarded));
} }
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
Ok(Json( Ok(Json(
User::create(db, data.username, session.user_id, None) User::create(db, data.username, session.user_id, None)
+11 -4
View File
@@ -1,16 +1,23 @@
use authifier::models::Session; use authifier::models::Session;
use revolt_database::User; use revolt_database::User;
use revolt_models::v0;
use crate::util::json::Json; use rocket::serde::json::Json;
use serde::Serialize;
/// # Onboarding Status
#[derive(Serialize, JsonSchema)]
pub struct DataHello {
/// Whether onboarding is required
onboarding: bool,
}
/// # Check Onboarding Status /// # Check Onboarding Status
/// ///
/// This will tell you whether the current account requires onboarding or whether you can continue to send requests as usual. You may skip calling this if you're restoring an existing session. /// This will tell you whether the current account requires onboarding or whether you can continue to send requests as usual. You may skip calling this if you're restoring an existing session.
#[openapi(tag = "Onboarding")] #[openapi(tag = "Onboarding")]
#[get("/hello")] #[get("/hello")]
pub async fn hello(_session: Session, user: Option<User>) -> Json<v0::DataHello> { pub async fn hello(_session: Session, user: Option<User>) -> Json<DataHello> {
Json(v0::DataHello { Json(DataHello {
onboarding: user.is_none(), onboarding: user.is_none(),
}) })
} }
+1 -2
View File
@@ -3,8 +3,7 @@ use authifier::{
Authifier, Authifier,
}; };
use revolt_result::{create_database_error, Result}; use revolt_result::{create_database_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::Json;
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
/// # Push Subscribe /// # Push Subscribe
+1 -1
View File
@@ -1,6 +1,6 @@
use crate::util::json::Json;
use revolt_config::config; use revolt_config::config;
use revolt_result::Result; use revolt_result::Result;
use rocket::serde::json::Json;
use serde::Serialize; use serde::Serialize;
/// # hCaptcha Configuration /// # hCaptcha Configuration
@@ -1,9 +1,22 @@
use revolt_database::{events::client::EventV1, Database, Report, Snapshot, SnapshotContent, User}; use revolt_database::{events::client::EventV1, Database, Report, Snapshot, SnapshotContent, User};
use revolt_models::v0::{DataReportContent, ReportStatus, ReportedContent}; use revolt_models::v0::{ReportStatus, ReportedContent};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use serde::Deserialize;
use ulid::Ulid; use ulid::Ulid;
use rocket::State; use validator::Validate;
use crate::util::json::{Json, Validate};
use rocket::{serde::json::Json, State};
/// # Report Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataReportContent {
/// Content being reported
content: ReportedContent,
/// Additional report description
#[validate(length(min = 0, max = 1000))]
#[serde(default)]
additional_context: String,
}
/// # Report Content /// # Report Content
/// ///
@@ -13,9 +26,14 @@ use crate::util::json::{Json, Validate};
pub async fn report_content( pub async fn report_content(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
data: Validate<Json<DataReportContent>>, data: Json<DataReportContent>,
) -> Result<()> { ) -> Result<()> {
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
// Bots cannot create reports // Bots cannot create reports
if user.bot.is_some() { if user.bot.is_some() {
@@ -6,8 +6,8 @@ use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::State; use rocket::{serde::json::Json, State};
use crate::util::json::{Json, Validate}; use validator::Validate;
/// # Ban User /// # Ban User
/// ///
@@ -19,9 +19,14 @@ pub async fn ban(
user: User, user: User,
server: Reference<'_>, server: Reference<'_>,
target: Reference<'_>, target: Reference<'_>,
data: Validate<Json<v0::DataBanCreate>>, data: Json<v0::DataBanCreate>,
) -> Result<Json<v0::ServerBan>> { ) -> Result<Json<v0::ServerBan>> {
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let server = server.as_server(db).await?; let server = server.as_server(db).await?;
+1 -1
View File
@@ -6,7 +6,7 @@ use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::Result; use revolt_result::Result;
use crate::util::json::Json; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
/// # Fetch Bans /// # Fetch Bans
@@ -2,10 +2,11 @@ use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::{util::reference::Reference, Channel, Database, User}; use revolt_database::{util::reference::Reference, Channel, Database, User};
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::Result; use revolt_result::{create_error, Result};
use crate::util::json::{Json, Validate}; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
use validator::Validate;
/// # Create Channel /// # Create Channel
/// ///
@@ -16,9 +17,14 @@ pub async fn create_server_channel(
db: &State<Database>, db: &State<Database>,
user: User, user: User,
server: Reference<'_>, server: Reference<'_>,
data: Validate<Json<v0::DataCreateServerChannel>>, data: Json<v0::DataCreateServerChannel>,
) -> Result<Json<v0::Channel>> { ) -> Result<Json<v0::Channel>> {
let data = data.into_inner().into_inner(); let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut server = server.as_server(db).await?; let mut server = server.as_server(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server); let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
Loaded 100 of 174 files, more files were not shown because too many files have changed in this diff. Show more