From 32e6600272b885c595c094f0bc69459250220dcb Mon Sep 17 00:00:00 2001 From: ElfFlu <228066338+ElfFlu@users.noreply.github.com> Date: Sat, 23 Aug 2025 19:05:32 +0200 Subject: [PATCH 01/14] fix: remove authentication tag bytes from attachment download Signed-off-by: ElfFlu <228066338+ElfFlu@users.noreply.github.com> --- crates/core/files/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/core/files/src/lib.rs b/crates/core/files/src/lib.rs index dacc988e..f9db7810 100644 --- a/crates/core/files/src/lib.rs +++ b/crates/core/files/src/lib.rs @@ -80,6 +80,9 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result Date: Mon, 5 May 2025 22:14:10 +0100 Subject: [PATCH 02/14] feat: ready payload field customisation --- Cargo.lock | 1 + crates/bonfire/Cargo.toml | 1 + crates/bonfire/src/config.rs | 40 ++++++++++++++++++----- crates/bonfire/src/events/impl.rs | 21 +++++------- crates/core/database/src/events/client.rs | 31 +++++++++++++----- 5 files changed, 65 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d310e03..a3c3b024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6294,6 +6294,7 @@ dependencies = [ "once_cell", "querystring", "redis-kiss", + "regex", "revolt-config", "revolt-database", "revolt-models", diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index e447825d..c0160270 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -19,6 +19,7 @@ async-channel = "2.3.1" # parsing querystring = "1.1.0" +regex = "1.11.1" # serde bincode = "1.3.3" diff --git a/crates/bonfire/src/config.rs b/crates/bonfire/src/config.rs index 5642d3ba..ea02092e 100644 --- a/crates/bonfire/src/config.rs +++ b/crates/bonfire/src/config.rs @@ -1,8 +1,13 @@ use async_tungstenite::tungstenite::{handshake, Message}; use futures::channel::oneshot::Sender; +use once_cell::sync::Lazy; use revolt_database::events::client::ReadyPayloadFields; use revolt_result::{create_error, Result}; use serde::{Deserialize, Serialize}; +use regex::Regex; + +/// matches either a single word ie "users" or a key and value ie "settings[notifications]" +static READY_PAYLOAD_FIELD_REGEX: Lazy = Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\w+)\])?$"#).unwrap()); /// Enumeration of supported protocol formats #[derive(Debug)] @@ -17,6 +22,7 @@ pub struct ProtocolConfiguration { protocol_version: i32, format: ProtocolFormat, session_token: Option, + ready_payload_fields: ReadyPayloadFields } impl ProtocolConfiguration { @@ -25,11 +31,13 @@ impl ProtocolConfiguration { protocol_version: i32, format: ProtocolFormat, session_token: Option, + ready_payload_fields: ReadyPayloadFields ) -> Self { Self { protocol_version, format, session_token, + ready_payload_fields } } @@ -86,14 +94,8 @@ impl ProtocolConfiguration { } /// Get ready payload fields - pub fn get_ready_payload_fields(&self) -> Vec { - vec![ - ReadyPayloadFields::Users, - ReadyPayloadFields::Servers, - ReadyPayloadFields::Channels, - ReadyPayloadFields::Members, - ReadyPayloadFields::Emoji, - ] + pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields { + &self.ready_payload_fields } } @@ -124,6 +126,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback { let mut protocol_version = 1; let mut format = ProtocolFormat::Json; let mut session_token = None; + let mut ready_payload_fields = ReadyPayloadFields::default(); // Parse and map parameters from key-value to known variables. for (key, value) in params { @@ -139,6 +142,26 @@ impl handshake::server::Callback for WebsocketHandshakeCallback { _ => {} }, "token" => session_token = Some(value.into()), + "ready" => { + 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()); + } + }, + _ => {} + } + } + } + } _ => {} } } @@ -151,6 +174,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback { protocol_version, format, session_token, + ready_payload_fields }) .is_ok() { diff --git a/crates/bonfire/src/events/impl.rs b/crates/bonfire/src/events/impl.rs index 160e7c6c..49b30d51 100644 --- a/crates/bonfire/src/events/impl.rs +++ b/crates/bonfire/src/events/impl.rs @@ -95,7 +95,7 @@ impl State { pub async fn generate_ready_payload( &mut self, db: &Database, - fields: Vec, + fields: &ReadyPayloadFields, ) -> Result { let user = self.clone_user(); self.cache.is_bot = user.bot.is_some(); @@ -168,7 +168,7 @@ impl State { .await?; // Fetch customisations. - let emojis = if fields.contains(&ReadyPayloadFields::Emoji) { + let emojis = if fields.emojis { Some( db.fetch_emoji_by_parent_ids( &servers @@ -183,17 +183,14 @@ impl State { }; // Fetch user settings - let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields - .iter() - .find(|e| matches!(e, ReadyPayloadFields::UserSettings(_))) - { - Some(db.fetch_user_settings(&user.id, keys).await?) + let user_settings = if !fields.user_settings.is_empty() { + Some(db.fetch_user_settings(&user.id, &fields.user_settings).await?) } else { None }; // 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?) } else { None @@ -241,22 +238,22 @@ impl State { } Ok(EventV1::Ready { - users: if fields.contains(&ReadyPayloadFields::Users) { + users: if fields.users { Some(users) } else { None }, - servers: if fields.contains(&ReadyPayloadFields::Servers) { + servers: if fields.servers { Some(servers.into_iter().map(Into::into).collect()) } else { None }, - channels: if fields.contains(&ReadyPayloadFields::Channels) { + channels: if fields.channels { Some(channels.into_iter().map(Into::into).collect()) } else { None }, - members: if fields.contains(&ReadyPayloadFields::Members) { + members: if fields.members { Some(members.into_iter().map(Into::into).collect()) } else { None diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index aa9fb0b9..b64a02ad 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -20,16 +20,29 @@ pub enum Ping { } /// Fields provided in Ready payload -#[derive(PartialEq)] -pub enum ReadyPayloadFields { - Users, - Servers, - Channels, - Members, - Emoji, +#[derive(PartialEq, Debug, Clone, Deserialize)] +pub struct ReadyPayloadFields { + pub users: bool, + pub servers: bool, + pub channels: bool, + pub members: bool, + pub emojis: bool, + pub user_settings: Vec, + pub channel_unreads: bool +} - UserSettings(Vec), - ChannelUnreads, +impl Default for ReadyPayloadFields { + fn default() -> Self { + Self { + users: true, + servers: true, + channels: true, + members: true, + emojis: true, + user_settings: Vec::new(), + channel_unreads: false + } + } } /// Protocol Events From 964884a5de81f8b459bbc09068c67889bb827cd4 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Tue, 16 Sep 2025 20:46:05 +0100 Subject: [PATCH 03/14] chore: add policy changes --- crates/bonfire/src/config.rs | 38 +++++++++++++----- crates/bonfire/src/events/impl.rs | 47 ++++++++++++++--------- crates/core/database/src/events/client.rs | 9 +++-- 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/crates/bonfire/src/config.rs b/crates/bonfire/src/config.rs index ea02092e..5cd87b8b 100644 --- a/crates/bonfire/src/config.rs +++ b/crates/bonfire/src/config.rs @@ -1,13 +1,14 @@ use async_tungstenite::tungstenite::{handshake, Message}; use futures::channel::oneshot::Sender; use once_cell::sync::Lazy; +use regex::Regex; use revolt_database::events::client::ReadyPayloadFields; use revolt_result::{create_error, Result}; use serde::{Deserialize, Serialize}; -use regex::Regex; /// matches either a single word ie "users" or a key and value ie "settings[notifications]" -static READY_PAYLOAD_FIELD_REGEX: Lazy = Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\w+)\])?$"#).unwrap()); +static READY_PAYLOAD_FIELD_REGEX: Lazy = + Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\w+)\])?$"#).unwrap()); /// Enumeration of supported protocol formats #[derive(Debug)] @@ -22,7 +23,7 @@ pub struct ProtocolConfiguration { protocol_version: i32, format: ProtocolFormat, session_token: Option, - ready_payload_fields: ReadyPayloadFields + ready_payload_fields: ReadyPayloadFields, } impl ProtocolConfiguration { @@ -31,13 +32,13 @@ impl ProtocolConfiguration { protocol_version: i32, format: ProtocolFormat, session_token: Option, - ready_payload_fields: ReadyPayloadFields + ready_payload_fields: ReadyPayloadFields, ) -> Self { Self { protocol_version, format, session_token, - ready_payload_fields + ready_payload_fields, } } @@ -126,7 +127,22 @@ impl handshake::server::Callback for WebsocketHandshakeCallback { let mut protocol_version = 1; let mut format = ProtocolFormat::Json; let mut session_token = None; - let mut ready_payload_fields = ReadyPayloadFields::default(); + 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. for (key, value) in params { @@ -143,6 +159,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback { }, "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() { @@ -154,9 +171,12 @@ impl handshake::server::Callback for WebsocketHandshakeCallback { "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()); + ready_payload_fields + .user_settings + .push(subkey.as_str().to_string()); } - }, + } + "policy_changes" => ready_payload_fields.policy_changes = true, _ => {} } } @@ -174,7 +194,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback { protocol_version, format, session_token, - ready_payload_fields + ready_payload_fields, }) .is_ok() { diff --git a/crates/bonfire/src/events/impl.rs b/crates/bonfire/src/events/impl.rs index 49b30d51..83b0d4c6 100644 --- a/crates/bonfire/src/events/impl.rs +++ b/crates/bonfire/src/events/impl.rs @@ -101,15 +101,17 @@ impl State { self.cache.is_bot = user.bot.is_some(); // Fetch pending policy changes. - let policy_changes = if user.bot.is_some() { - vec![] + let policy_changes = if user.bot.is_some() || !fields.policy_changes { + None } else { - db.fetch_policy_changes() - .await? - .into_iter() - .filter(|policy| policy.created_time > user.last_acknowledged_policy_change) - .map(Into::into) - .collect() + Some( + db.fetch_policy_changes() + .await? + .into_iter() + .filter(|policy| policy.created_time > user.last_acknowledged_policy_change) + .map(Into::into) + .collect(), + ) }; // Find all relationships to the user. @@ -176,7 +178,10 @@ impl State { .map(|x| x.id.to_string()) .collect::>(), ) - .await?, + .await? + .into_iter() + .map(|emoji| emoji.into()) + .collect(), ) } else { None @@ -184,14 +189,23 @@ impl State { // Fetch user settings let user_settings = if !fields.user_settings.is_empty() { - Some(db.fetch_user_settings(&user.id, &fields.user_settings).await?) + Some( + db.fetch_user_settings(&user.id, &fields.user_settings) + .await?, + ) } else { None }; // Fetch channel unreads 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 { None }; @@ -238,11 +252,7 @@ impl State { } Ok(EventV1::Ready { - users: if fields.users { - Some(users) - } else { - None - }, + users: if fields.users { Some(users) } else { None }, servers: if fields.servers { Some(servers.into_iter().map(Into::into).collect()) } else { @@ -258,10 +268,9 @@ impl State { } else { None }, - emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()), - + emojis, user_settings, - channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()), + channel_unreads, policy_changes, }) diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index b64a02ad..996347ca 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -28,7 +28,8 @@ pub struct ReadyPayloadFields { pub members: bool, pub emojis: bool, pub user_settings: Vec, - pub channel_unreads: bool + pub channel_unreads: bool, + pub policy_changes: bool, } impl Default for ReadyPayloadFields { @@ -40,7 +41,8 @@ impl Default for ReadyPayloadFields { members: true, emojis: true, user_settings: Vec::new(), - channel_unreads: false + channel_unreads: false, + policy_changes: true, } } } @@ -76,7 +78,8 @@ pub enum EventV1 { #[serde(skip_serializing_if = "Option::is_none")] channel_unreads: Option>, - policy_changes: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + policy_changes: Option>, }, /// Ping response From 3a3415915f0d0fdce1499d47a2b7fa097f5946ea Mon Sep 17 00:00:00 2001 From: Zomatree Date: Tue, 16 Sep 2025 20:50:34 +0100 Subject: [PATCH 04/14] fix: relax settings name regex --- crates/bonfire/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bonfire/src/config.rs b/crates/bonfire/src/config.rs index 5cd87b8b..05f7cf5a 100644 --- a/crates/bonfire/src/config.rs +++ b/crates/bonfire/src/config.rs @@ -8,7 +8,7 @@ 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 = - Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\w+)\])?$"#).unwrap()); + Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap()); /// Enumeration of supported protocol formats #[derive(Debug)] From fb4011084d6a5fdf164a6569bd8a9c5a91aed80c Mon Sep 17 00:00:00 2001 From: Zomatree Date: Tue, 16 Sep 2025 19:15:34 +0100 Subject: [PATCH 05/14] refactor: move ratelimits to a generic system for all web servers --- Cargo.lock | 19 + crates/core/database/src/models/users/axum.rs | 13 +- crates/core/ratelimits/Cargo.toml | 25 ++ crates/core/ratelimits/src/axum.rs | 193 ++++++++++ crates/core/ratelimits/src/lib.rs | 7 + crates/core/ratelimits/src/ratelimiter.rs | 145 ++++++++ crates/core/ratelimits/src/rocket.rs | 162 ++++++++ crates/delta/Cargo.toml | 1 + crates/delta/src/main.rs | 9 +- crates/delta/src/util/mod.rs | 2 +- crates/delta/src/util/ratelimiter.rs | 347 ------------------ crates/delta/src/util/ratelimits.rs | 81 ++++ crates/services/autumn/Cargo.toml | 1 + crates/services/autumn/src/api.rs | 4 +- crates/services/autumn/src/main.rs | 26 +- crates/services/autumn/src/ratelimits.rs | 28 ++ 16 files changed, 705 insertions(+), 358 deletions(-) create mode 100644 crates/core/ratelimits/Cargo.toml create mode 100644 crates/core/ratelimits/src/axum.rs create mode 100644 crates/core/ratelimits/src/lib.rs create mode 100644 crates/core/ratelimits/src/ratelimiter.rs create mode 100644 crates/core/ratelimits/src/rocket.rs delete mode 100644 crates/delta/src/util/ratelimiter.rs create mode 100644 crates/delta/src/util/ratelimits.rs create mode 100644 crates/services/autumn/src/ratelimits.rs diff --git a/Cargo.lock b/Cargo.lock index a3c3b024..ca101167 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -962,6 +962,7 @@ checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes 1.10.1", "futures-util", "http 1.3.1", @@ -6259,6 +6260,7 @@ dependencies = [ "revolt-config", "revolt-database", "revolt-files", + "revolt-ratelimits", "revolt-result", "revolt_clamav-client", "serde", @@ -6415,6 +6417,7 @@ dependencies = [ "revolt-models", "revolt-permissions", "revolt-presence", + "revolt-ratelimits", "revolt-result", "revolt_rocket_okapi", "rocket", @@ -6563,6 +6566,22 @@ dependencies = [ "web-push", ] +[[package]] +name = "revolt-ratelimits" +version = "0.8.8" +dependencies = [ + "async-trait", + "authifier", + "axum", + "dashmap", + "log", + "revolt-database", + "revolt-result", + "revolt_rocket_okapi", + "rocket", + "serde", +] + [[package]] name = "revolt-result" version = "0.8.8" diff --git a/crates/core/database/src/models/users/axum.rs b/crates/core/database/src/models/users/axum.rs index da83c5b1..a6d8b5a3 100644 --- a/crates/core/database/src/models/users/axum.rs +++ b/crates/core/database/src/models/users/axum.rs @@ -1,14 +1,21 @@ -use axum::{extract::FromRequestParts, http::request::Parts}; +use axum::{ + extract::{FromRef, FromRequestParts}, + http::request::Parts, +}; use revolt_result::{create_error, Error, Result}; use crate::{Database, User}; #[async_trait::async_trait] -impl FromRequestParts for User { +impl FromRequestParts for User +where + Database: FromRef, +{ type Rejection = Error; - async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result { + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let db = Database::from_ref(state); 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?; db.fetch_user(&bot.id).await diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml new file mode 100644 index 00000000..49110e1e --- /dev/null +++ b/crates/core/ratelimits/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "revolt-ratelimits" +version = "0.8.8" +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.8", path = "../database"} +revolt-result = { version = "0.8.8", path = "../result" } + +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" diff --git a/crates/core/ratelimits/src/axum.rs b/crates/core/ratelimits/src/axum.rs new file mode 100644 index 00000000..39b0c3bb --- /dev/null +++ b/crates/core/ratelimits/src/axum.rs @@ -0,0 +1,193 @@ +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 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; + +fn to_ip(parts: &Parts) -> String { + parts + .extensions + .get::>() + .map(|info| info.ip().to_string()) + .unwrap_or_default() +} + +fn to_real_ip(parts: &Parts) -> String { + if let Ok(true) = std::env::var("TRUST_CLOUDFLARE").map(|x| x == "1") { + 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 FromRequestParts for Ratelimiter +where + Database: FromRef, + RatelimitStorage: FromRef, +{ + type Rejection = Json; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + if parts + .extensions + .get::>>() + .is_none() + { + let storage = RatelimitStorage::from_ref(state); + + let identifier = if let Ok(user) = parts.extract_with_state::(state).await { + user.id + } else { + to_real_ip(parts) + }; + + 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::>>() + .unwrap() + } +} + +#[async_trait] +impl FromRequestParts for RatelimitInformation +where + Database: FromRef, + RatelimitStorage: FromRef, +{ + type Rejection = Json; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + if parts + .extensions + .get::>>() + .is_none() + { + let ratelimiter = parts.extract_with_state::(state).await; + + parts.extensions.insert(ratelimiter); + }; + + let ratelimiter = *parts + .extensions + .get::>>() + .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, + State(ratelimit_storage): State, + request: Request, + 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 { + Json(info) +} + +pub fn routes() -> Router +where + Database: FromRef, + RatelimitStorage: FromRef, +{ + Router::new().route("/ratelimit", get(ratelimit_info)) +} diff --git a/crates/core/ratelimits/src/lib.rs b/crates/core/ratelimits/src/lib.rs new file mode 100644 index 00000000..5eaa8108 --- /dev/null +++ b/crates/core/ratelimits/src/lib.rs @@ -0,0 +1,7 @@ +pub mod ratelimiter; + +#[cfg(feature = "rocket")] +pub mod rocket; + +#[cfg(feature = "axum")] +pub mod axum; diff --git a/crates/core/ratelimits/src/ratelimiter.rs b/crates/core/ratelimits/src/ratelimiter.rs new file mode 100644 index 00000000..a232feb3 --- /dev/null +++ b/crates/core/ratelimits/src/ratelimiter.rs @@ -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: 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 { + pub resolver: Arc RatelimitResolver>>, + pub map: Arc>, +} + +impl RatelimitStorage { + pub fn new RatelimitResolver> + '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, 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, 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, + identifier: &str, + limit: u32, + (bucket, resource): (&str, Option<&str>), + ) -> Result { + 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 }, +} diff --git a/crates/core/ratelimits/src/rocket.rs b/crates/core/ratelimits/src/rocket.rs new file mode 100644 index 00000000..6e7a0c9d --- /dev/null +++ b/crates/core/ratelimits/src/rocket.rs @@ -0,0 +1,162 @@ +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_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; + +/// 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 +fn to_real_ip(request: &'_ rocket::Request<'_>) -> String { + if let Ok(true) = std::env::var("TRUST_CLOUDFLARE").map(|x| x == "1") { + 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 { + let ratelimiter = request + .local_cache_async(async { + use rocket::outcome::Outcome; + + let storage = request.guard::<&State>().await.unwrap(); + + let identifier = if let Outcome::Success(session) = request.guard::().await + { + session.id + } else { + to_real_ip(request) + }; + + 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 { + 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::().await { + info!( + "User rate-limited on route {}! (IP = {:?})", + request.uri(), + to_real_ip(request) + ); + + 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::().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 { + let info = match request.guard::().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 { + Json(info) +} + +pub fn routes() -> Vec { + rocket::routes![ratelimit_info] +} diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index d07b44bb..889b1167 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -81,6 +81,7 @@ revolt-models = { path = "../core/models", features = [ revolt-presence = { path = "../core/presence" } revolt-result = { path = "../core/result", features = ["rocket", "okapi"] } revolt-permissions = { path = "../core/permissions", features = ["schemas"] } +revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] } [build-dependencies] vergen = "7.5.0" diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 9cce39ce..6d0eaa60 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -11,6 +11,7 @@ pub mod util; use revolt_config::config; use revolt_database::events::client::EventV1; use revolt_database::AMQP; +use revolt_ratelimits::rocket as ratelimiter; use rocket::{Build, Rocket}; use rocket_cors::{AllowedOrigins, CorsOptions}; use rocket_prometheus::PrometheusMetrics; @@ -122,18 +123,22 @@ pub async fn web() -> Rocket { let rocket = rocket::build(); let prometheus = PrometheusMetrics::new(); + // Ratelimits + let ratelimits = ratelimiter::RatelimitStorage::new(util::ratelimits::DeltaRatelimits); + routes::mount(config, rocket) .attach(prometheus.clone()) .mount("/metrics", prometheus) .mount("/", rocket_cors::catch_all_options_routes()) - .mount("/", util::ratelimiter::routes()) + .mount("/", ratelimiter::routes()) .mount("/swagger/", swagger) .mount("/0.8/swagger/", swagger_0_8) .manage(authifier) .manage(db) .manage(amqp) .manage(cors.clone()) - .attach(util::ratelimiter::RatelimitFairing) + .manage(ratelimits) + .attach(ratelimiter::RatelimitFairing) .attach(cors) .configure(rocket::Config { limits: rocket::data::Limits::default().limit("string", 5.megabytes()), diff --git a/crates/delta/src/util/mod.rs b/crates/delta/src/util/mod.rs index 6ee3a7f6..61e6d0da 100644 --- a/crates/delta/src/util/mod.rs +++ b/crates/delta/src/util/mod.rs @@ -1,2 +1,2 @@ -pub mod ratelimiter; +pub mod ratelimits; pub mod test; diff --git a/crates/delta/src/util/ratelimiter.rs b/crates/delta/src/util/ratelimiter.rs deleted file mode 100644 index 7d00e96d..00000000 --- a/crates/delta/src/util/ratelimiter.rs +++ /dev/null @@ -1,347 +0,0 @@ -use std::collections::hash_map::DefaultHasher; -use std::hash::Hasher; -use std::ops::Add; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use authifier::models::Session; -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}; - -use revolt_rocket_okapi::gen::OpenApiGenerator; -use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; - -use serde::Serialize; - -use dashmap::DashMap; -use once_cell::sync::Lazy; - -/// Ratelimit Bucket -#[derive(Clone, Copy, Debug)] -struct Entry { - used: u8, - reset: u128, -} - -static MAP: Lazy> = Lazy::new(DashMap::new); - -/// 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(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, key: u64) { - MAP.insert(key, self); - } - - /// Get remaining units in the bucket - pub fn get_remaining(&self, limit: u8) -> u8 { - 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(); - if current_time > self.reset { - 0 - } else { - self.reset - current_time - } - } -} - -/// Ratelimit Guard -#[derive(Serialize, Clone, Copy, Debug)] -#[allow(dead_code)] -pub struct Ratelimiter { - key: u64, - limit: u8, - remaining: u8, - reset: u128, -} - -/// Find bucket from given request -/// -/// Optionally, include a resource id to hash against. -fn resolve_bucket<'r>(request: &'r rocket::Request<'_>) -> (&'r str, Option<&'r str>) { - let (segment, resource, extra) = if matches!(request.routed_segment(0), Some("0.8")) { - ( - request.routed_segment(1), - request.routed_segment(2), - request.routed_segment(3), - ) - } else { - ( - request.routed_segment(0), - request.routed_segment(1), - request.routed_segment(2), - ) - }; - - if let Some(segment) = segment { - #[allow(clippy::redundant_locals)] - let resource = resource; - - let method = request.method(); - match (segment, resource, method) { - ("users", target, Method::Patch) => ("user_edit", target), - ("users", _, _) => { - if let Some("default_avatar") = extra { - return ("default_avatar", None); - } - - ("users", None) - } - ("bots", _, _) => ("bots", None), - ("channels", Some(id), _) => { - if request.method() == Method::Post { - if let Some("messages") = extra { - return ("messaging", Some(id)); - } - } - - ("channels", Some(id)) - } - ("servers", Some(id), _) => ("servers", Some(id)), - ("auth", _, _) => { - if request.method() == Method::Delete { - ("auth_delete", None) - } else { - ("auth", None) - } - } - ("swagger", _, _) => ("swagger", None), - ("safety", Some("report"), _) => ("safety_report", Some("report")), - ("safety", _, _) => ("safety", None), - _ => ("any", None), - } - } else { - ("any", None) - } -} - -/// Resolve per-bucket limits -fn resolve_bucket_limit(bucket: &str) -> u8 { - match bucket { - "user_edit" => 2, - "users" => 20, - "bots" => 10, - "messaging" => 10, - "channels" => 15, - "servers" => 5, - "auth" => 15, - "auth_delete" => 255, - "default_avatar" => 255, - "swagger" => 100, - "safety" => 15, - "safety_report" => 3, - _ => 20, - } -} - -/// 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 -fn to_real_ip(request: &'_ rocket::Request<'_>) -> String { - if let Ok(true) = std::env::var("TRUST_CLOUDFLARE").map(|x| x == "1") { - request - .headers() - .get_one("CF-Connecting-IP") - .map(|x| x.to_string()) - .unwrap_or_else(|| to_ip(request)) - } else { - to_ip(request) - } -} - -impl Ratelimiter { - /// Generate guard from identifier and target bucket - pub fn from( - identifier: &str, - (bucket, resource): (&str, Option<&str>), - ) -> Result { - 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 limit = resolve_bucket_limit(bucket); - let mut entry = Entry::from(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(key); - ratelimiter.remaining -= 1; - ratelimiter.reset = entry.left_until_reset(); - - Ok(ratelimiter) - } -} - -#[async_trait] -impl<'r> FromRequest<'r> for Ratelimiter { - type Error = Ratelimiter; - - async fn from_request(request: &'r rocket::Request<'_>) -> Outcome { - let ratelimiter = request - .local_cache_async(async { - use rocket::outcome::Outcome; - let identifier = if let Outcome::Success(session) = request.guard::().await - { - session.id - } else { - to_real_ip(request) - }; - - Ratelimiter::from(&identifier, resolve_bucket(request)) - }) - .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 { - Ok(RequestHeaderInput::None) - } -} - -/// Attach ratelimiter to the Rocket application -pub struct RatelimitFairing; - -#[rocket::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::().await { - info!( - "User rate-limited on route {}! (IP = {:?})", - request.uri(), - to_real_ip(request) - ); - - 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::().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); - } - } -} - -#[derive(Serialize)] -#[serde(untagged)] -pub enum RatelimitInformation { - Success(Ratelimiter), - Failure { retry_after: u128 }, -} - -#[async_trait] -impl<'r> FromRequest<'r> for RatelimitInformation { - type Error = u128; - - async fn from_request(request: &'r rocket::Request<'_>) -> Outcome { - let info = match request.guard::().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 { - Json(info) -} - -pub fn routes() -> Vec { - rocket::routes![ratelimit_info] -} diff --git a/crates/delta/src/util/ratelimits.rs b/crates/delta/src/util/ratelimits.rs new file mode 100644 index 00000000..178d15d2 --- /dev/null +++ b/crates/delta/src/util/ratelimits.rs @@ -0,0 +1,81 @@ +use revolt_ratelimits::ratelimiter::RatelimitResolver; +use rocket::{http::Method, Request}; + +pub struct DeltaRatelimits; + +impl<'a> RatelimitResolver> for DeltaRatelimits { + fn resolve_bucket<'r>(&self, request: &'r Request<'_>) -> (&'r str, Option<&'r str>) { + let (segment, resource, extra) = if request.routed_segment(0) == Some("0.8") { + ( + request.routed_segment(1), + request.routed_segment(2), + request.routed_segment(3), + ) + } else { + ( + request.routed_segment(0), + request.routed_segment(1), + request.routed_segment(2), + ) + }; + + if let Some(segment) = segment { + #[allow(clippy::redundant_locals)] + let resource = resource; + + let method = request.method(); + match (segment, resource, method) { + ("users", target, Method::Patch) => ("user_edit", target), + ("users", _, _) => { + if let Some("default_avatar") = extra { + return ("default_avatar", None); + } + + ("users", None) + } + ("bots", _, _) => ("bots", None), + ("channels", Some(id), _) => { + if request.method() == Method::Post { + if let Some("messages") = extra { + return ("messaging", Some(id)); + } + } + + ("channels", Some(id)) + } + ("servers", Some(id), _) => ("servers", Some(id)), + ("auth", _, _) => { + if request.method() == Method::Delete { + ("auth_delete", None) + } else { + ("auth", None) + } + } + ("swagger", _, _) => ("swagger", None), + ("safety", Some("report"), _) => ("safety_report", Some("report")), + ("safety", _, _) => ("safety", None), + _ => ("any", None), + } + } else { + ("any", None) + } + } + + fn resolve_bucket_limit(&self, bucket: &str) -> u32 { + match bucket { + "user_edit" => 2, + "users" => 20, + "bots" => 10, + "messaging" => 10, + "channels" => 15, + "servers" => 5, + "auth" => 15, + "auth_delete" => 255, + "default_avatar" => 255, + "swagger" => 100, + "safety" => 15, + "safety_report" => 3, + _ => 20, + } + } +} diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index 464da056..3514f501 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -52,6 +52,7 @@ revolt-result = { version = "0.8.8", path = "../../core/result", features = [ "utoipa", "axum", ] } +revolt-ratelimits = { version = "0.8.8", path = "../../core/ratelimits", features = ["axum"] } # Axum / web server tempfile = "3.12.0" diff --git a/crates/services/autumn/src/api.rs b/crates/services/autumn/src/api.rs index d068c2dc..5c567146 100644 --- a/crates/services/autumn/src/api.rs +++ b/crates/services/autumn/src/api.rs @@ -25,10 +25,10 @@ use tokio::time::Instant; use tower_http::cors::{AllowHeaders, Any, CorsLayer}; use utoipa::ToSchema; -use crate::{exif::strip_metadata, metadata::generate_metadata, mime_type::determine_mime_type}; +use crate::{exif::strip_metadata, metadata::generate_metadata, mime_type::determine_mime_type, AppState}; /// Build the API router -pub async fn router() -> Router { +pub async fn router() -> Router { let config = config().await; let cors = CorsLayer::new() diff --git a/crates/services/autumn/src/main.rs b/crates/services/autumn/src/main.rs index ff29b2e3..cb936e0c 100644 --- a/crates/services/autumn/src/main.rs +++ b/crates/services/autumn/src/main.rs @@ -1,8 +1,10 @@ use std::net::{Ipv4Addr, SocketAddr}; -use axum::Router; +use axum::{middleware::from_fn_with_state, Router}; -use revolt_database::DatabaseInfo; +use axum_macros::FromRef; +use revolt_database::{Database, DatabaseInfo}; +use revolt_ratelimits::axum as ratelimiter; use tokio::net::TcpListener; use utoipa::{ openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, @@ -15,6 +17,13 @@ pub mod clamav; pub mod exif; pub mod metadata; pub mod mime_type; +mod ratelimits; + +#[derive(FromRef, Clone)] +struct AppState { + database: Database, + ratelimit_storage: ratelimiter::RatelimitStorage, +} #[tokio::main] async fn main() -> Result<(), std::io::Error> { @@ -69,12 +78,23 @@ async fn main() -> Result<(), std::io::Error> { // Connect to the database let db = DatabaseInfo::Auto.connect().await.unwrap(); + let ratelimits = ratelimiter::RatelimitStorage::new(ratelimits::AutumnRatelimits); + + let state = AppState { + database: db, + ratelimit_storage: ratelimits, + }; // Configure Axum and router let app = Router::new() .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) .nest("/", api::router().await) - .with_state(db); + .nest("/", ratelimiter::routes()) + .layer(from_fn_with_state( + state.clone(), + ratelimiter::ratelimit_middleware, + )) + .with_state(state); // Configure TCP listener and bind let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 14704)); diff --git a/crates/services/autumn/src/ratelimits.rs b/crates/services/autumn/src/ratelimits.rs new file mode 100644 index 00000000..609d8e1e --- /dev/null +++ b/crates/services/autumn/src/ratelimits.rs @@ -0,0 +1,28 @@ +use axum::http::{request::Parts, Method}; +use revolt_ratelimits::ratelimiter::RatelimitResolver; + +pub struct AutumnRatelimits; + +impl RatelimitResolver for AutumnRatelimits { + fn resolve_bucket<'a>(&self, parts: &'a Parts) -> (&'a str, Option<&'a str>) { + let path = parts + .uri + .path() + .trim_matches('/') + .split_terminator("/") + .collect::>(); + + match (&parts.method, path.as_slice()) { + (&Method::POST, &[tag]) => ("upload", Some(tag)), + _ => ("any", None), + } + } + + fn resolve_bucket_limit(&self, bucket: &str) -> u32 { + match bucket { + "upload" => 10, + "any" => u32::MAX, + _ => unreachable!("Bucket defined but no limit set"), + } + } +} From cc7a7962a882e1627fcd0bc75858a017415e8cfc Mon Sep 17 00:00:00 2001 From: Zomatree Date: Thu, 18 Sep 2025 21:09:53 +0100 Subject: [PATCH 06/14] fix: use `trust_cloudflare` config value instead of env var --- Cargo.lock | 1 + crates/core/ratelimits/Cargo.toml | 1 + crates/core/ratelimits/src/axum.rs | 7 ++++--- crates/core/ratelimits/src/rocket.rs | 9 +++++---- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca101167..96b91384 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6575,6 +6575,7 @@ dependencies = [ "axum", "dashmap", "log", + "revolt-config", "revolt-database", "revolt-result", "revolt_rocket_okapi", diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index 49110e1e..05e1e362 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -12,6 +12,7 @@ default = ["rocket", "axum"] [dependencies] revolt-database = { version = "0.8.8", path = "../database"} revolt-result = { version = "0.8.8", path = "../result" } +revolt-config = { version = "0.8.8", path = "../config" } rocket = { version = "0.5.1", optional = true } revolt_rocket_okapi = { version = "0.10.0", optional = true } diff --git a/crates/core/ratelimits/src/axum.rs b/crates/core/ratelimits/src/axum.rs index 39b0c3bb..f50cf867 100644 --- a/crates/core/ratelimits/src/axum.rs +++ b/crates/core/ratelimits/src/axum.rs @@ -11,6 +11,7 @@ use axum::{ routing::get, }; use revolt_database::{Database, User}; +use revolt_config::config; use crate::ratelimiter::{RatelimitInformation, Ratelimiter, RequestKind}; @@ -31,8 +32,8 @@ fn to_ip(parts: &Parts) -> String { .unwrap_or_default() } -fn to_real_ip(parts: &Parts) -> String { - if let Ok(true) = std::env::var("TRUST_CLOUDFLARE").map(|x| x == "1") { +async fn to_real_ip(parts: &Parts) -> String { + if config().await.api.security.trust_cloudflare { parts .headers .get("CF-Connecting-IP") @@ -62,7 +63,7 @@ where let identifier = if let Ok(user) = parts.extract_with_state::(state).await { user.id } else { - to_real_ip(parts) + to_real_ip(parts).await }; let (bucket, resource) = storage.resolver.resolve_bucket(parts); diff --git a/crates/core/ratelimits/src/rocket.rs b/crates/core/ratelimits/src/rocket.rs index 6e7a0c9d..046317bd 100644 --- a/crates/core/ratelimits/src/rocket.rs +++ b/crates/core/ratelimits/src/rocket.rs @@ -6,6 +6,7 @@ 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}; @@ -33,8 +34,8 @@ fn to_ip(request: &'_ rocket::Request<'_>) -> String { } /// Find the actual IP of the client -fn to_real_ip(request: &'_ rocket::Request<'_>) -> String { - if let Ok(true) = std::env::var("TRUST_CLOUDFLARE").map(|x| x == "1") { +async fn to_real_ip(request: &'_ rocket::Request<'_>) -> String { + if config().await.api.security.trust_cloudflare { request .headers() .get_one("CF-Connecting-IP") @@ -60,7 +61,7 @@ impl<'r> FromRequest<'r> for Ratelimiter { { session.id } else { - to_real_ip(request) + to_real_ip(request).await }; let (bucket, resource) = storage.resolver.resolve_bucket(request); @@ -105,7 +106,7 @@ impl Fairing for RatelimitFairing { info!( "User rate-limited on route {}! (IP = {:?})", request.uri(), - to_real_ip(request) + to_real_ip(request).await ); request.set_method(Method::Get); From bfe4018e436a4075bae780dd4d35a9b58315e12f Mon Sep 17 00:00:00 2001 From: Zomatree Date: Fri, 19 Sep 2025 02:41:53 +0100 Subject: [PATCH 07/14] 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. --- crates/services/january/src/website_embed.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/services/january/src/website_embed.rs b/crates/services/january/src/website_embed.rs index 49ac2846..25de1537 100644 --- a/crates/services/january/src/website_embed.rs +++ b/crates/services/january/src/website_embed.rs @@ -312,7 +312,7 @@ pub async fn populate_special(original_url: String, metadata: &mut WebsiteMetada Some(Special::GIF) } else { RE_APPLE_MUSIC - .captures_iter(url) + .captures_iter(&original_url) .next() .map(|captures| Special::AppleMusic { album_id: captures[1].to_string(), From b0c977b324b8144c1152589546eb8fec5954c3e7 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Fri, 8 Aug 2025 00:16:20 +0100 Subject: [PATCH 08/14] feat: initial work on tenor gif searching --- .github/workflows/docker.yaml | 20 + Cargo.lock | 1150 +++++++++++---------- README.md | 5 + crates/core/coalesced/Cargo.toml | 22 + crates/core/coalesced/LICENSE | 9 + crates/core/coalesced/src/config.rs | 23 + crates/core/coalesced/src/error.rs | 20 + crates/core/coalesced/src/lib.rs | 7 + crates/core/coalesced/src/service.rs | 168 +++ crates/core/config/Revolt.toml | 3 + crates/core/config/src/lib.rs | 2 + crates/services/gifbox/Cargo.toml | 40 + crates/services/gifbox/Dockerfile | 12 + crates/services/gifbox/src/api.rs | 64 ++ crates/services/gifbox/src/main.rs | 65 ++ crates/services/gifbox/src/tenor/mod.rs | 129 +++ crates/services/gifbox/src/tenor/types.rs | 36 + scripts/build-image-layer.sh | 9 +- scripts/publish-debug-image.sh | 2 + scripts/start.sh | 6 +- 20 files changed, 1268 insertions(+), 524 deletions(-) create mode 100644 crates/core/coalesced/Cargo.toml create mode 100644 crates/core/coalesced/LICENSE create mode 100644 crates/core/coalesced/src/config.rs create mode 100644 crates/core/coalesced/src/error.rs create mode 100644 crates/core/coalesced/src/lib.rs create mode 100644 crates/core/coalesced/src/service.rs create mode 100644 crates/services/gifbox/Cargo.toml create mode 100644 crates/services/gifbox/Dockerfile create mode 100644 crates/services/gifbox/src/api.rs create mode 100644 crates/services/gifbox/src/main.rs create mode 100644 crates/services/gifbox/src/tenor/mod.rs create mode 100644 crates/services/gifbox/src/tenor/types.rs diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index fccaf634..f0a79f53 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -152,6 +152,26 @@ jobs: BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest 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 - name: Docker meta id: meta-crond diff --git a/Cargo.lock b/Cargo.lock index 96b91384..efcf2cb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,12 +125,6 @@ dependencies = [ "tokio 1.47.1", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -142,18 +136,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" dependencies = [ "backtrace", ] [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] name = "arc-swap" @@ -169,7 +163,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -219,9 +213,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", @@ -250,20 +244,20 @@ dependencies = [ [[package]] name = "async-io" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock 3.4.1", + "autocfg 1.5.0", "cfg-if", "concurrent-queue", "futures-io", "futures-lite 2.6.1", "parking", - "polling 3.10.0", - "rustix 1.0.8", + "polling 3.11.0", + "rustix", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -288,9 +282,9 @@ dependencies = [ [[package]] name = "async-process" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65daa13722ad51e6ab1a1b9c01299142bc75135b337923cfa10e79bbbd669f00" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel 2.5.0", "async-io", @@ -301,7 +295,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite 2.6.1", - "rustix 1.0.8", + "rustix", ] [[package]] @@ -312,14 +306,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "async-signal" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f567af260ef69e1d52c2b560ce0ea230763e6fbb9214a85d768760a920e3e3c1" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock 3.4.1", @@ -327,17 +321,17 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.0.8", + "rustix", "signal-hook-registry", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] name = "async-std" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ "async-attributes", "async-channel 1.9.0", @@ -380,7 +374,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -391,13 +385,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -518,18 +512,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" dependencies = [ "arrayvec", ] [[package]] name = "aws-config" -version = "1.8.4" +version = "1.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483020b893cdef3d89637e428d588650c71cfae7ea2e6ecbaee4de4ff99fb2dd" +checksum = "8bc1b40fb26027769f16960d2f4a6bc20c4bb755d403e552c8c1a73af433c246" dependencies = [ "aws-credential-types", "aws-runtime", @@ -557,9 +551,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1541072f81945fa1251f8795ef6c92c4282d74d59f88498ae7d4bf00f0ebdad9" +checksum = "d025db5d9f52cbc413b167136afb3d8aeea708c0d8884783cf6253be5e22f6f2" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -569,9 +563,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" dependencies = [ "aws-lc-sys", "zeroize", @@ -579,9 +573,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" dependencies = [ "bindgen", "cc", @@ -617,9 +611,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.101.0" +version = "1.106.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b16efa59a199f5271bf21ab3e570c5297d819ce4f240e6cf0096d1dc0049c44" +checksum = "2c230530df49ed3f2b7b4d9c8613b72a04cdac6452eede16d587fc62addfabac" dependencies = [ "aws-credential-types", "aws-runtime", @@ -651,9 +645,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.79.0" +version = "1.84.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a847168f15b46329fa32c7aca4e4f1a2e072f9b422f0adb19756f2e1457f111" +checksum = "357a841807f6b52cb26123878b3326921e2a25faca412fabdd32bd35b7edd5d3" dependencies = [ "aws-credential-types", "aws-runtime", @@ -673,9 +667,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.80.0" +version = "1.85.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b654dd24d65568738593e8239aef279a86a15374ec926ae8714e2d7245f34149" +checksum = "67e05f33b6c9026fecfe9b3b6740f34d41bc6ff641a6a32dabaab60209245b75" dependencies = [ "aws-credential-types", "aws-runtime", @@ -695,9 +689,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.81.0" +version = "1.86.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92ea8a7602321c83615c82b408820ad54280fb026e92de0eeea937342fafa24" +checksum = "e7d835f123f307cafffca7b9027c14979f1d403b417d8541d67cf252e8a21e35" dependencies = [ "aws-credential-types", "aws-runtime", @@ -757,9 +751,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.6" +version = "0.63.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9054b4cc5eda331cde3096b1576dec45365c5cbbca61d1fffa5f236e251dfce7" +checksum = "56d2df0314b8e307995a3b86d44565dfe9de41f876901a7d71886c756a25979f" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -777,9 +771,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.10" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604c7aec361252b8f1c871a7641d5e0ba3a7f5a586e51b66bc9510a5519594d9" +checksum = "182b03393e8c677347fb5705a04a9392695d47d20ef0a2f8cfe28c8e6b9b9778" dependencies = [ "aws-smithy-types", "bytes 1.10.1", @@ -809,20 +803,20 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.0.6" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" +checksum = "147e8eea63a40315d704b97bf9bc9b8c1402ae94f89d5ad6f7550d963309da1b" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", "h2 0.3.27", - "h2 0.4.11", + "h2 0.4.12", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.24.2", "hyper-rustls 0.27.7", "hyper-util", @@ -832,15 +826,16 @@ dependencies = [ "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio 1.47.1", + "tokio-rustls 0.26.2", "tower", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.61.4" +version = "0.61.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9" +checksum = "eaa31b350998e703e9826b2104dd6f63be0508666e1aba88137af060e8944047" dependencies = [ "aws-smithy-types", ] @@ -866,9 +861,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.6" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796" +checksum = "4fa63ad37685ceb7762fa4d73d06f1d5493feb88e3f27259b9ed277f4c01b185" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -890,9 +885,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.8.7" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75d52251ed4b9776a3e8487b2a01ac915f73b2da3af8fc1e77e0fce697a550d4" +checksum = "07f5e0fc8a6b3f2303f331b94504bbf754d85488f402d6f1dd7a6080f99afe56" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -968,7 +963,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "itoa", "matchit", @@ -1043,7 +1038,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1075,7 +1070,7 @@ dependencies = [ "heck", "proc-macro-error", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", "ubyte", ] @@ -1169,16 +1164,14 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.5" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools", - "lazy_static", - "lazycell", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -1186,8 +1179,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.104", - "which", + "syn 2.0.106", ] [[package]] @@ -1198,9 +1190,9 @@ checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" [[package]] name = "bit_field" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitfield" @@ -1216,9 +1208,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "bitstream-io" @@ -1283,7 +1275,7 @@ dependencies = [ "getrandom 0.2.16", "getrandom 0.3.3", "hex", - "indexmap 2.10.0", + "indexmap 2.11.1", "js-sys", "once_cell", "rand 0.9.2", @@ -1308,9 +1300,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" @@ -1390,10 +1382,11 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.2.31" +version = "1.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -1431,23 +1424,22 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -1803,7 +1795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1823,24 +1815,24 @@ dependencies = [ [[package]] name = "curl" -version = "0.4.48" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2d5c8f48d9c0c23250e52b55e82a6ab4fdba6650c931f5a0a57a43abda812b" +checksum = "79fc3b6dd0b87ba36e565715bf9a2ced221311db47bd18011676f24a6066edbc" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", - "socket2 0.5.10", + "socket2 0.6.0", "windows-sys 0.59.0", ] [[package]] name = "curl-sys" -version = "0.4.82+curl-8.14.1" +version = "0.4.83+curl-8.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4d63638b5ec65f1a4ae945287b3fd035be4554bbaf211901159c9a2a74fb5be" +checksum = "5830daf304027db10c82632a464879d46a3f7c4ba17a31592657ad16c719b483" dependencies = [ "cc", "libc", @@ -1921,7 +1913,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "strsim 0.11.1", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1954,7 +1946,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1978,9 +1970,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-url" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "deadqueue" @@ -2054,9 +2046,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", "serde", @@ -2070,18 +2062,18 @@ checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "derive-where" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510c292c8cf384b1a340b816a9a6cf2599eb8f566a44949024af88418000c50b" +checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2094,7 +2086,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2123,11 +2115,11 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "proc-macro2", "proc-macro2-diagnostics", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2150,7 +2142,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2327,7 +2319,7 @@ dependencies = [ "heck", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2341,13 +2333,13 @@ dependencies = [ [[package]] name = "enum-iterator-derive" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2380,7 +2372,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2400,12 +2392,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -2474,6 +2466,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote 1.0.40", + "syn 2.0.106", +] + [[package]] name = "fcm_v1" version = "0.3.0" @@ -2540,6 +2552,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "findshlibs" version = "0.10.2" @@ -2623,9 +2641,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -2779,7 +2797,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2842,9 +2860,9 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" +checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" dependencies = [ "cc", "cfg-if", @@ -2876,9 +2894,9 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] @@ -2906,7 +2924,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.5+wasi-0.2.4", "wasm-bindgen", ] @@ -2919,7 +2937,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2963,9 +2981,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gloo-timers" @@ -3013,7 +3031,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.10.0", + "indexmap 2.11.1", "slab", "tokio 1.47.1", "tokio-util", @@ -3022,9 +3040,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes 1.10.1", @@ -3032,7 +3050,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.10.0", + "indexmap 2.11.1", "slab", "tokio 1.47.1", "tokio-util", @@ -3090,9 +3108,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -3163,7 +3181,7 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna 1.0.3", + "idna 1.1.0", "ipnet", "once_cell", "rand 0.8.5", @@ -3237,15 +3255,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "hostname" version = "0.3.1" @@ -3268,7 +3277,7 @@ dependencies = [ "markup5ever", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3374,20 +3383,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes 1.10.1", "futures-channel", - "futures-util", - "h2 0.4.11", + "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", "httpdate", "itoa", "pin-project-lite 0.2.16", + "pin-utils", "smallvec", "tokio 1.47.1", "want", @@ -3417,7 +3428,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "rustls 0.22.4", "rustls-pki-types", @@ -3434,7 +3445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "rustls 0.23.31", "rustls-native-certs 0.8.1", @@ -3465,7 +3476,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes 1.10.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio 1.47.1", @@ -3486,7 +3497,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", "ipnet", "libc", "percent-encoding", @@ -3501,9 +3512,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3511,7 +3522,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.0", ] [[package]] @@ -3648,9 +3659,9 @@ dependencies = [ [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -3669,24 +3680,25 @@ dependencies = [ [[package]] name = "if_chain" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb" [[package]] name = "image" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", "color_quant", "exr", "gif", - "image-webp 0.2.3", + "image-webp 0.2.4", + "moxcms", "num-traits", - "png", + "png 0.18.0", "qoi", "ravif", "rayon", @@ -3708,9 +3720,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", "quick-error 2.0.1", @@ -3747,12 +3759,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "serde", ] @@ -3797,16 +3809,16 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -3900,6 +3912,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -3908,25 +3929,19 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.3", "libc", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" - [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -4162,17 +4177,11 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "lebe" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "lettre" @@ -4199,9 +4208,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libfuzzer-sys" @@ -4299,15 +4308,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -4327,43 +4330,43 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "value-bag", ] [[package]] name = "logos" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6f536c1af4c7cc81edf73da1f8029896e7e1e16a219ef09b184e76a296f3db" +checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" dependencies = [ "logos-derive", ] [[package]] name = "logos-codegen" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189bbfd0b61330abea797e5e9276408f2edbe4f822d7ad08685d67419aafb34e" +checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" dependencies = [ "beef", "fnv", "lazy_static", "proc-macro2", "quote 1.0.40", - "regex-syntax 0.8.5", + "regex-syntax", "rustc_version", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "logos-derive" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfe8e1a19049ddbfccbd14ac834b215e11b85b90bab0c2dba7c7b92fb5d5cba" +checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" dependencies = [ "logos-codegen", ] @@ -4390,7 +4393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ "cfg-if", - "generator 0.8.5", + "generator 0.8.7", "scoped-tls", "tracing", "tracing-subscriber", @@ -4429,7 +4432,16 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe949189f46fabb938b3a9a0be30fdd93fd8a09260da863399a8cf3db756ec8" +dependencies = [ + "hashbrown 0.15.5", ] [[package]] @@ -4462,7 +4474,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4476,7 +4488,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4487,7 +4499,7 @@ checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4498,7 +4510,7 @@ checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" dependencies = [ "macro_magic_core", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4523,11 +4535,11 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4570,9 +4582,9 @@ checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memmap2" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", ] @@ -4673,10 +4685,28 @@ dependencies = [ ] [[package]] -name = "mongodb" -version = "3.2.4" +name = "mongocrypt" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f8c69f13acf07eae386a2974f48ffd9187ea2aba8defbea9aa34e7e272c5f3" +checksum = "22426d6318d19c5c0773f783f85375265d6a8f0fa76a733da8dc4355516ec63d" +dependencies = [ + "bson", + "mongocrypt-sys", + "once_cell", + "serde", +] + +[[package]] +name = "mongocrypt-sys" +version = "0.1.4+1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda42df21d035f88030aad8e877492fac814680e1d7336a57b2a091b989ae388" + +[[package]] +name = "mongodb" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f272c59e54a3c85f5902c6b8e7b1653a6b6681f45e4c42d6581301119a4b8" dependencies = [ "async-trait", "base64 0.13.1", @@ -4695,14 +4725,15 @@ dependencies = [ "hmac", "macro_magic", "md-5", + "mongocrypt", "mongodb-internal-macros", "once_cell", "pbkdf2", "percent-encoding", "rand 0.8.5", "rustc_version_runtime", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "rustls 0.23.31", + "rustversion", "serde", "serde_bytes", "serde_with", @@ -4714,23 +4745,33 @@ dependencies = [ "take_mut", "thiserror 1.0.69", "tokio 1.47.1", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.2", "tokio-util", "typed-builder", "uuid", - "webpki-roots 0.25.4", + "webpki-roots 0.26.11", ] [[package]] name = "mongodb-internal-macros" -version = "3.2.4" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9202de265a3a8bbb43f9fe56db27c93137d4f9fb04c093f47e9c7de0c61ac7d" +checksum = "63981427a0f26b89632fd2574280e069d09fb2912a3138da15de0174d11dd077" dependencies = [ "macro_magic", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", +] + +[[package]] +name = "moxcms" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +dependencies = [ + "num-traits", + "pxfm", ] [[package]] @@ -4754,9 +4795,9 @@ dependencies = [ [[package]] name = "mutate_once" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" +checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af" [[package]] name = "nanoid" @@ -4817,12 +4858,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -4866,7 +4906,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4959,7 +4999,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4998,7 +5038,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -5015,7 +5055,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5064,12 +5104,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.11.1" @@ -5175,7 +5209,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5219,26 +5253,26 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.12", + "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" dependencies = [ "pest", "pest_generator", @@ -5246,22 +5280,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", "sha2", @@ -5336,7 +5370,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5380,7 +5414,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5457,7 +5491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64 0.22.1", - "indexmap 2.10.0", + "indexmap 2.11.1", "quick-xml", "serde", "time", @@ -5476,6 +5510,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.9.4", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "2.8.0" @@ -5494,16 +5541,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi 0.5.2", "pin-project-lite 0.2.16", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix", + "windows-sys 0.61.0", ] [[package]] @@ -5526,9 +5573,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -5566,12 +5613,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5636,14 +5683,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -5656,7 +5703,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", "version_check", "yansi", ] @@ -5677,7 +5724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -5694,6 +5741,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pxfm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55f4fedc84ed39cb7a489322318976425e42a147e2be79d8f878e2884f94e84" +dependencies = [ + "num-traits", +] + [[package]] name = "qoi" version = "0.4.1" @@ -5723,9 +5779,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.1" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", ] @@ -5943,7 +5999,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.12.1", "libc", "libfuzzer-sys", "log", @@ -5981,9 +6037,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -5991,9 +6047,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -6065,7 +6121,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] @@ -6085,58 +6141,43 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" @@ -6180,19 +6221,19 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes 1.10.1", "encoding_rs", "futures-core", - "h2 0.4.11", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.27.7", "hyper-tls 0.6.0", "hyper-util", @@ -6220,9 +6261,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" [[package]] name = "resvg" @@ -6310,6 +6351,15 @@ dependencies = [ "ulid 0.5.0", ] +[[package]] +name = "revolt-coalesced" +version = "0.8.8" +dependencies = [ + "indexmap 2.11.1", + "lru 0.16.1", + "tokio 1.47.1", +] + [[package]] name = "revolt-config" version = "0.8.8" @@ -6457,6 +6507,27 @@ dependencies = [ "webp", ] +[[package]] +name = "revolt-gifbox" +version = "0.8.8" +dependencies = [ + "axum", + "axum-extra", + "lru_time_cache", + "reqwest 0.12.23", + "revolt-coalesced", + "revolt-config", + "revolt-models", + "revolt-result", + "serde", + "serde_json", + "tokio 1.47.1", + "tracing", + "urlencoding", + "utoipa", + "utoipa-scalar", +] + [[package]] name = "revolt-january" version = "0.8.8" @@ -6469,7 +6540,7 @@ dependencies = [ "mime", "moka", "regex", - "reqwest 0.12.22", + "reqwest 0.12.23", "revolt-config", "revolt-files", "revolt-models", @@ -6607,7 +6678,7 @@ dependencies = [ "erased-serde", "http 1.3.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.26.0", "hyper-util", "parking_lot", @@ -6758,7 +6829,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap 2.10.0", + "indexmap 2.11.1", "log", "memchr", "multer", @@ -6806,11 +6877,11 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap 2.10.0", + "indexmap 2.11.1", "proc-macro2", "quote 1.0.40", "rocket_http", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid 0.2.6", "version_check", ] @@ -6853,7 +6924,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.32", - "indexmap 2.10.0", + "indexmap 2.11.1", "log", "memchr", "pear", @@ -6947,9 +7018,9 @@ checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -6972,28 +7043,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" -dependencies = [ - "bitflags 2.9.1", - "errno", - "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys", + "windows-sys 0.61.0", ] [[package]] @@ -7029,9 +7087,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.5", "subtle", "zeroize", ] @@ -7057,7 +7117,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.4.0", ] [[package]] @@ -7110,9 +7170,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "b5a37813727b78798e53c2bec3f5e8fe12a6d6f8389bf9ca7802add4c9905ad8" dependencies = [ "aws-lc-rs", "ring", @@ -7122,9 +7182,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" @@ -7132,7 +7192,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytemuck", "core_maths", "log", @@ -7152,11 +7212,11 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -7205,7 +7265,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "serde_derive_internals", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7297,7 +7357,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -7306,11 +7366,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -7319,9 +7379,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -7333,7 +7393,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cssparser", "derive_more", "fxhash", @@ -7348,9 +7408,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "sentry" @@ -7504,7 +7564,7 @@ dependencies = [ "rand 0.9.2", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.16", "time", "url", "uuid", @@ -7512,20 +7572,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac" dependencies = [ + "serde_core", "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.17" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +checksum = "fe07b5d88710e3b807c16a06ccbc9dfecd5fff6a4d2745c59e3e26774f10de6a" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -7538,14 +7600,23 @@ dependencies = [ ] [[package]] -name = "serde_derive" -version = "1.0.219" +name = "serde_core" +version = "1.0.223" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.223" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7556,30 +7627,32 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.1", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "a30a8abed938137c7183c173848e3c9b3517f5e038226849a4ecc9b21a4b4e2a" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -7613,7 +7686,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.10.0", + "indexmap 2.11.1", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -7632,7 +7705,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7771,9 +7844,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slotmap" @@ -7948,7 +8021,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "rustversion", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7991,9 +8064,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote 1.0.40", @@ -8044,7 +8117,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8078,7 +8151,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -8142,15 +8215,15 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand 2.3.0", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", - "windows-sys 0.59.0", + "rustix", + "windows-sys 0.61.0", ] [[package]] @@ -8184,11 +8257,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.16", ] [[package]] @@ -8199,18 +8272,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8224,23 +8297,25 @@ dependencies = [ [[package]] name = "tiff" -version = "0.9.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ + "fax", "flate2", - "jpeg-decoder", + "half", + "quick-error 2.0.1", "weezl", + "zune-jpeg", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -8252,15 +8327,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -8286,7 +8361,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", + "png 0.17.16", "tiny-skia-path", ] @@ -8313,9 +8388,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -8365,7 +8440,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8470,7 +8545,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.1", "toml_datetime", "winnow 0.5.40", ] @@ -8481,12 +8556,12 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.1", "serde", "serde_spanned", "toml_datetime", "toml_write", - "winnow 0.7.12", + "winnow 0.7.13", ] [[package]] @@ -8529,7 +8604,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes 1.10.1", "http 1.3.1", "http-body 1.0.1", @@ -8545,7 +8620,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes 1.10.1", "futures-util", "http 1.3.1", @@ -8589,7 +8664,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8625,14 +8700,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -8677,13 +8752,22 @@ dependencies = [ [[package]] name = "typed-builder" -version = "0.10.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" +checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 1.0.109", + "syn 2.0.106", ] [[package]] @@ -8794,9 +8878,9 @@ checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" @@ -8880,12 +8964,12 @@ dependencies = [ [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", - "idna 1.0.3", + "idna 1.1.0", "percent-encoding", "serde", ] @@ -8950,7 +9034,7 @@ version = "4.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" dependencies = [ - "indexmap 2.10.0", + "indexmap 2.11.1", "serde", "serde_json", "utoipa-gen", @@ -8966,7 +9050,7 @@ dependencies = [ "proc-macro2", "quote 1.0.40", "regex", - "syn 2.0.104", + "syn 2.0.106", "ulid 1.2.1", ] @@ -8984,9 +9068,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", @@ -9139,11 +9223,20 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.5+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.0+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" +dependencies = [ + "wit-bindgen", ] [[package]] @@ -9157,35 +9250,36 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" dependencies = [ "cfg-if", "js-sys", @@ -9196,9 +9290,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote 1.0.40", "wasm-bindgen-macro-support", @@ -9206,22 +9300,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] @@ -9250,9 +9344,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" dependencies = [ "js-sys", "wasm-bindgen", @@ -9270,20 +9364,14 @@ dependencies = [ [[package]] name = "webp" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f53152f51fb5af0c08484c33d16cca96175881d1f3dec068c23b31a158c2d99" +checksum = "c071456adef4aca59bf6a583c46b90ff5eb0b4f758fc347cea81290288f37ce1" dependencies = [ "image", "libwebp-sys", ] -[[package]] -name = "webpki-roots" -version = "0.25.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" - [[package]] name = "webpki-roots" version = "0.26.11" @@ -9308,18 +9396,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "widestring" version = "1.2.0" @@ -9344,11 +9420,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -9373,9 +9449,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -9385,7 +9461,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -9396,9 +9472,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -9407,8 +9496,8 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", "windows-threading", ] @@ -9420,7 +9509,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9431,7 +9520,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9440,14 +9529,20 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", - "windows-link", + "windows-core 0.61.2", + "windows-link 0.1.3", ] [[package]] @@ -9456,9 +9551,9 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -9467,7 +9562,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -9476,7 +9580,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -9508,11 +9621,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" dependencies = [ - "windows-targets 0.53.3", + "windows-link 0.2.0", ] [[package]] @@ -9552,7 +9665,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -9569,7 +9682,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -9721,9 +9834,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -9739,13 +9852,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" [[package]] name = "writeable" @@ -9812,7 +9922,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", "synstructure 0.13.2", ] @@ -9829,7 +9939,7 @@ dependencies = [ "http 0.2.12", "hyper 0.14.32", "hyper-rustls 0.24.2", - "itertools", + "itertools 0.12.1", "log", "percent-encoding", "rustls 0.22.4", @@ -9845,22 +9955,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9880,7 +9990,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", "synstructure 0.13.2", ] @@ -9920,7 +10030,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote 1.0.40", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9940,9 +10050,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] diff --git a/README.md b/README.md index 3dd25cf5..1f1ccbb0 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,11 @@ The services and libraries that power the Revolt service.
| `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/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) | | `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/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) | | `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) | @@ -66,6 +68,7 @@ As a heads-up, the development environment uses the following ports: | `crates/bonfire` | 14703 | | `crates/services/autumn` | 14704 | | `crates/services/january` | 14705 | +| `crates/services/gifbox` | 14706 | Now you can clone and build the project: @@ -141,6 +144,8 @@ cargo run --bin revolt-bonfire cargo run --bin revolt-autumn # run the proxy server cargo run --bin revolt-january +# run the tenor proxy +cargo run --bin revolt-gifbox # run the push daemon (not usually needed in regular development) cargo run --bin revolt-pushd diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml new file mode 100644 index 00000000..4485d65d --- /dev/null +++ b/crates/core/coalesced/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "revolt-coalesced" +version = "0.8.8" +edition = "2021" +license = "MIT" +authors = ["Paul Makles ", "Zomatree "] +description = "Revolt Backend: Coalescion service" + +[features] +tokio = ["dep:tokio"] +queue = ["dep:indexmap"] +cache = ["dep:lru"] + +default = ["tokio", "queue", "cache"] + +[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"] } diff --git a/crates/core/coalesced/LICENSE b/crates/core/coalesced/LICENSE new file mode 100644 index 00000000..7c2815b9 --- /dev/null +++ b/crates/core/coalesced/LICENSE @@ -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. diff --git a/crates/core/coalesced/src/config.rs b/crates/core/coalesced/src/config.rs new file mode 100644 index 00000000..00ef7990 --- /dev/null +++ b/crates/core/coalesced/src/config.rs @@ -0,0 +1,23 @@ +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct CoalescionServiceConfig { + /// How many tasks are running at once + pub max_concurrent: Option, + /// 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, +} + +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) + } + } +} diff --git a/crates/core/coalesced/src/error.rs b/crates/core/coalesced/src/error.rs new file mode 100644 index 00000000..a5395597 --- /dev/null +++ b/crates/core/coalesced/src/error.rs @@ -0,0 +1,20 @@ +use std::fmt::Display; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Error { + RecvError, + MaxConcurrent, + MaxQueue, +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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"), + } + } +} + +impl std::error::Error for Error {} diff --git a/crates/core/coalesced/src/lib.rs b/crates/core/coalesced/src/lib.rs new file mode 100644 index 00000000..6fd18267 --- /dev/null +++ b/crates/core/coalesced/src/lib.rs @@ -0,0 +1,7 @@ +mod config; +mod error; +mod service; + +pub use config::CoalescionServiceConfig; +pub use error::Error; +pub use service::CoalescionService; \ No newline at end of file diff --git a/crates/core/coalesced/src/service.rs b/crates/core/coalesced/src/service.rs new file mode 100644 index 00000000..3d3b1c9b --- /dev/null +++ b/crates/core/coalesced/src/service.rs @@ -0,0 +1,168 @@ +use std::{collections::{HashMap}, fmt::Debug, hash::Hash, sync::Arc, future::Future}; + +use tokio::{sync::{watch::{channel as watch_channel, Receiver}, RwLock, Mutex}}; + +#[cfg(feature = "cache")] +use lru::LruCache; + +#[cfg(feature = "queue")] +use indexmap::IndexMap; + +use crate::{CoalescionServiceConfig, Error}; + +#[derive(Clone, Debug)] +#[allow(clippy::type_complexity)] +pub struct CoalescionService { + config: Arc, + watchers: Arc, Error>>>>>>, + #[cfg(feature = "queue")] + queue: Arc, Error>>>>>>, + #[cfg(feature = "cache")] + cache: Option>>>>, +} + +impl CoalescionService { + pub fn new() -> Self { + Self::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>) -> Self { + Self { + cache: Some(Arc::new(Mutex::new(cache))), + ..Self::from_config(config) + } + } + + async fn wait_for(&self, mut receiver: Receiver, Error>>>) -> Result, Error> { + receiver + .wait_for(|v| v.is_some()) + .await + .map_err(|_| Error::RecvError) + .and_then(|r| r.clone().unwrap()) + } + + async fn insert_and_execute Fut, Fut: Future>(&self, id: Id, func: F) -> Result, 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()); + }); + + #[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 + } + + pub async fn execute Fut, Fut: Future>(&self, id: Id, func: F) -> Result, Error> { + #[cfg(feature = "cache")] + if let Some(cache) = self.cache.as_ref() { + if let Some(value) = cache.lock().await.get(&id) { + return Ok(value.clone()) + } + }; + + 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()); + }); + + return response + } + } + } + } + } else { + Err(Error::MaxConcurrent) + } + + #[cfg(not(feature = "queue"))] + Err(Error::MaxConcurrent) + } + _ => { + self.insert_and_execute(id, func).await + } + } + } + } + + pub async fn current_task_count(&self) -> usize { + self.watchers.read().await.len() + } + + pub async fn current_queue_len(&self) -> usize { + self.queue.read().await.len() + } +} + +impl Default for CoalescionService { + fn default() -> Self { + Self::from_config(CoalescionServiceConfig::default()) + } +} diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index e3a04f2a..0f264457 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -56,6 +56,8 @@ voso_legacy_token = "" trust_cloudflare = false # easypwned endpoint easypwned = "" +# Tenor API Key +tenor_key = "" [api.security.captcha] # hCaptcha configuration @@ -277,3 +279,4 @@ files = "" proxy = "" pushd = "" crond = "" +gifbox = "" \ No newline at end of file diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index 6c4d58de..5863cbc4 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -190,6 +190,7 @@ pub struct ApiSecurity { pub captcha: ApiSecurityCaptcha, pub trust_cloudflare: bool, pub easypwned: String, + pub tenor_key: String, } #[derive(Deserialize, Debug, Clone)] @@ -365,6 +366,7 @@ pub struct Sentry { pub proxy: String, pub pushd: String, pub crond: String, + pub gifbox: String, } #[derive(Deserialize, Debug, Clone)] diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml new file mode 100644 index 00000000..d2e65145 --- /dev/null +++ b/crates/services/gifbox/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "revolt-gifbox" +version = "0.8.8" +edition = "2021" +license = "AGPL-3.0-or-later" + +[dependencies] +# Serialisation +serde = { version = "1.0", features = ["derive", "rc"] } +serde_json = "1.0.68" + +# Async runtime +tokio = { version = "1.0", features = ["full"] } + +# Web requests +reqwest = { version = "0.12", features = ["json"] } + +# Core crates +revolt-config = { version = "0.8.8", path = "../../core/config" } +revolt-models = { version = "0.8.8", path = "../../core/models" } +revolt-result = { version = "0.8.8", path = "../../core/result", features = [ + "utoipa", + "axum", +] } +revolt-coalesced = { version = "0.8.8", path = "../../core/coalesced" } + +# Axum / web server +axum = { version = "0.7.5" } +axum-extra = { version = "0.9", features = ["typed-header"] } + +# OpenAPI & documentation generation +utoipa-scalar = { version = "0.1.0", features = ["axum"] } +utoipa = { version = "4.2.3", features = ["axum_extras", "ulid"] } + +# Logging +tracing = "0.1" + +# Utils +lru_time_cache = "0.11.11" +urlencoding = "2.1.3" \ No newline at end of file diff --git a/crates/services/gifbox/Dockerfile b/crates/services/gifbox/Dockerfile new file mode 100644 index 00000000..5668448f --- /dev/null +++ b/crates/services/gifbox/Dockerfile @@ -0,0 +1,12 @@ +# Build Stage +FROM ghcr.io/revoltchat/base:latest AS builder + +# Bundle Stage +FROM gcr.io/distroless/cc-debian12:nonroot +COPY --from=builder /home/rust/src/target/release/revolt-gifbox ./ +COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/local/bin/ +COPY --from=mwader/static-ffmpeg:7.0.2 /ffprobe /usr/local/bin/ + +EXPOSE 14706 +USER nonroot +CMD ["./revolt-gifbox"] diff --git a/crates/services/gifbox/src/api.rs b/crates/services/gifbox/src/api.rs new file mode 100644 index 00000000..c137e7a3 --- /dev/null +++ b/crates/services/gifbox/src/api.rs @@ -0,0 +1,64 @@ +use std::sync::Arc; + +use axum::{extract::{Query, State}, routing::get, Json, Router}; +use revolt_result::{create_error, Result}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::tenor::{Tenor, types}; + +pub async fn router() -> Router { + Router::new() + .route("/", get(root)) + .route("/search", get(search)) +} + +/// Successful root response +#[derive(Serialize, Debug, ToSchema)] +pub struct RootResponse { + message: &'static str, + version: &'static str, +} + +/// Capture crate version from Cargo +static CRATE_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Root response from service +#[utoipa::path( + get, + path = "/", + responses( + (status = 200, description = "Echo response", body = RootResponse) + ) +)] +async fn root() -> Json { + Json(RootResponse { + message: "Gifbox lives on!", + version: CRATE_VERSION, + }) +} + +#[derive(Deserialize)] +struct SearchQueryParams { + pub query: String, + pub locale: String, + pub position: Option +} + +#[utoipa::path( + get, + path = "/search", + responses( + (status = 200, description = "Search results", body = SearchResponse) + ) +)] +async fn search( + Query(params): Query, + State(tenor): State, +) -> Result>> { + // Todo + tenor.search(¶ms.query, ¶ms.locale, params.position.as_deref()) + .await + .map_err(|_| create_error!(InternalError)) + .map(Json) +} \ No newline at end of file diff --git a/crates/services/gifbox/src/main.rs b/crates/services/gifbox/src/main.rs new file mode 100644 index 00000000..32d1ea58 --- /dev/null +++ b/crates/services/gifbox/src/main.rs @@ -0,0 +1,65 @@ +use std::net::{Ipv4Addr, SocketAddr}; + +use axum::Router; + +use revolt_config::config; +use tokio::net::TcpListener; +use utoipa::{ + openapi::security::{Http, HttpAuthScheme, SecurityScheme}, + Modify, OpenApi, +}; +use utoipa_scalar::{Scalar, Servable as ScalarServable}; + +mod api; +mod tenor; + +#[tokio::main] +async fn main() -> Result<(), std::io::Error> { + // Configure logging and environment + revolt_config::configure!(gifbox); + + // Configure API schema + #[derive(OpenApi)] + #[openapi( + modifiers(&SecurityAddon), + paths( + api::root, + ), + components( + schemas( + revolt_result::Error, + revolt_result::ErrorType, + ) + ) + )] + struct ApiDoc; + + struct SecurityAddon; + + impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + components.add_security_scheme( + "api_key", + SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), + ) + } + } + } + + let config = config().await; + let tenor = tenor::Tenor::new(&config.api.security.tenor_key); + + // Configure Axum and router + let app = Router::new() + .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) + .nest("/", api::router().await) + .with_state(tenor); + + // Configure TCP listener and bind + tracing::info!("Listening on 0.0.0.0:14706"); + tracing::info!("Play around with the API: http://localhost:14706/scalar"); + let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 14706)); + let listener = TcpListener::bind(&address).await?; + axum::serve(listener, app.into_make_service()).await +} diff --git a/crates/services/gifbox/src/tenor/mod.rs b/crates/services/gifbox/src/tenor/mod.rs new file mode 100644 index 00000000..56db6269 --- /dev/null +++ b/crates/services/gifbox/src/tenor/mod.rs @@ -0,0 +1,129 @@ +use std::{sync::Arc, time::Duration}; + +use reqwest::Client; +use tokio::sync::RwLock; +use lru_time_cache::LruCache; +use urlencoding::encode as url_encode; +use revolt_coalesced::{CoalescionService, CoalescionServiceConfig}; + +pub mod types; + +const TENOR_API_BASE_URL: &str = "https://tenor.googleapis.com/v2"; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TenorError { + HttpError, +} + +#[derive(Clone)] +pub struct Tenor { + pub key: Arc, + pub client: Client, + pub coalescion: CoalescionService, TenorError>>, + pub cache: Arc>>> +} + +impl Tenor { + pub fn new(key: &str) -> Self { + // 1 hour, 1k queries + let cache = LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60*60), 1000); + + Self { + key: Arc::from(key), + client: Client::new(), + coalescion: CoalescionService::from_config(CoalescionServiceConfig { + max_concurrent: Some(100), + queue_requests: true, + max_queue: None + }), + cache: Arc::new(RwLock::new(cache)), + } + } + + pub async fn search(&self, query: &str, locale: &str, position: Option<&str>) -> Result, TenorError> { + let unique_key = format!("{query}:{locale}:{position:?}"); + + if self.cache.read().await.contains_key(&unique_key) { + if let Some(response) = self.cache.write().await.get(&unique_key) { + return Ok(response.clone()) + } + } + + let res = self.coalescion.execute(unique_key.clone(), || { + let client = self.client.clone(); + + async move { + let mut url = format!( + "{TENOR_API_BASE_URL}/search?key={}&q={}&client_key=Gifbox&locale={}&contentfilter=medium&limit=1", + &self.key, + url_encode(query), + url_encode(locale), + ); + + if let Some(position) = position { + url.push_str("&pos="); + url.push_str(position); + }; + + let response = client.get(url) + .send() + .await + .inspect_err(|e| { revolt_config::capture_error(e); }) + .map_err(|_| TenorError::HttpError)?; + + let text = response.text().await.map_err(|e| { println!("{e:?}"); TenorError::HttpError })?; + + Ok(Arc::new(serde_json::from_str(&text).unwrap())) + } + }) + .await + .unwrap(); + + if let Ok(resp) = &*res { + self.cache.write().await.insert(unique_key, resp.clone()); + } + + (*res).clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use revolt_config::config; + + #[tokio::test(flavor = "current_thread")] + async fn test() { + let config = config().await; + + let tenor = Tenor::new(&config.api.security.tenor_key); + + let results =tenor.search("amog", "en_US", None).await.unwrap(); + + let result = &results.results[0]; + + println!("{:?}", result.media_formats.iter().next().unwrap()); + } + + #[tokio::test(flavor = "current_thread")] + async fn test_2() { + let config = config().await; + let tenor = Tenor::new(&config.api.security.tenor_key); + + let mut tasks = Vec::new(); + + for i in 1..1001 { + let tenor = tenor.clone(); + println!("creating search task {i}"); + + tasks.push((i, tokio::spawn(async move { + tenor.search(&format!("amog-{i}"), "en_US", None).await + }))); + }; + + for (i, task) in tasks { + task.await.unwrap().unwrap(); + println!("Got result for {i}"); + }; + } +} \ No newline at end of file diff --git a/crates/services/gifbox/src/tenor/types.rs b/crates/services/gifbox/src/tenor/types.rs new file mode 100644 index 00000000..a712fcb7 --- /dev/null +++ b/crates/services/gifbox/src/tenor/types.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use serde::{Serialize, Deserialize}; + + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct SearchResponse { + pub results: Vec, + pub next: String +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct MediaObject { + pub url: String, + pub dims: Vec, + pub duration: f64, + pub size: f64 +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct TenorResult { + pub created: f64, + #[serde(default)] + pub hasaudio: bool, + pub id: String, + pub media_formats: HashMap, + pub tags: Vec, + pub title: String, + pub content_description: String, + pub itemurl: String, + #[serde(default)] + pub hascaption: bool, + pub flags: Vec, + pub bg_color: Option, + pub url: String +} \ No newline at end of file diff --git a/scripts/build-image-layer.sh b/scripts/build-image-layer.sh index 0a0fa32b..064cee89 100644 --- a/scripts/build-image-layer.sh +++ b/scripts/build-image-layer.sh @@ -32,8 +32,10 @@ deps() { crates/core/permissions/src \ crates/core/presence/src \ crates/core/result/src \ + crates/core/coalesced/src \ crates/services/autumn/src \ crates/services/january/src \ + crates/services/gifbox/src \ crates/daemons/crond/src \ crates/daemons/pushd/src echo 'fn main() { panic!("stub"); }' | @@ -41,6 +43,7 @@ deps() { tee crates/delta/src/main.rs | tee crates/services/autumn/src/main.rs | tee crates/services/january/src/main.rs | + tee crates/services/gifbox/src/main.rs | tee crates/daemons/crond/src/main.rs | tee crates/daemons/pushd/src/main.rs echo '' | @@ -51,7 +54,8 @@ deps() { tee crates/core/parser/src/lib.rs | tee crates/core/permissions/src/lib.rs | tee crates/core/presence/src/lib.rs | - tee crates/core/result/src/lib.rs + tee crates/core/result/src/lib.rs | + tee crates/core/coalesced/src/lib.rs if [ -z "$TARGETARCH" ]; then cargo build -j 10 --locked --release @@ -72,7 +76,8 @@ apps() { crates/core/parser/src/lib.rs \ crates/core/permissions/src/lib.rs \ crates/core/presence/src/lib.rs \ - crates/core/result/src/lib.rs + crates/core/result/src/lib.rs \ + crates/core/coalesced/src/lib .rs if [ -z "$TARGETARCH" ]; then cargo build -j 10 --locked --release diff --git a/scripts/publish-debug-image.sh b/scripts/publish-debug-image.sh index 6686344c..cc67d9fd 100755 --- a/scripts/publish-debug-image.sh +++ b/scripts/publish-debug-image.sh @@ -25,6 +25,7 @@ docker build -t ghcr.io/revoltchat/server:$TAG - < crates/delta/Dockerfile docker build -t ghcr.io/revoltchat/bonfire:$TAG - < crates/bonfire/Dockerfile docker build -t ghcr.io/revoltchat/autumn:$TAG - < crates/services/autumn/Dockerfile docker build -t ghcr.io/revoltchat/january:$TAG - < crates/services/january/Dockerfile +docker build -t ghcr.io/revoltchat/gifbox:$TAG - < crates/services/gifbox/Dockerfile docker build -t ghcr.io/revoltchat/crond:$TAG - < crates/daemons/crond/Dockerfile docker build -t ghcr.io/revoltchat/pushd:$TAG - < crates/daemons/pushd/Dockerfile @@ -36,5 +37,6 @@ docker push ghcr.io/revoltchat/server:$TAG docker push ghcr.io/revoltchat/bonfire:$TAG docker push ghcr.io/revoltchat/autumn:$TAG docker push ghcr.io/revoltchat/january:$TAG +docker push ghcr.io/revoltchat/gifbox:$TAG docker push ghcr.io/revoltchat/crond:$TAG docker push ghcr.io/revoltchat/pushd:$TAG diff --git a/scripts/start.sh b/scripts/start.sh index 466d5607..ef485d22 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -4,10 +4,12 @@ cargo build \ --bin revolt-delta \ --bin revolt-bonfire \ --bin revolt-autumn \ - --bin revolt-january + --bin revolt-january \ + --bin revolt-gifbox trap 'pkill -f revolt-' SIGINT cargo run --bin revolt-delta & cargo run --bin revolt-bonfire & cargo run --bin revolt-autumn & -cargo run --bin revolt-january +cargo run --bin revolt-january & +cargo run --bin revolt-gifbox From b5cd5e30ef7d5e56e8964fb7c543965fa6bf5a4a Mon Sep 17 00:00:00 2001 From: Zomatree Date: Fri, 8 Aug 2025 00:44:01 +0100 Subject: [PATCH 09/14] feat: require auth for search --- Cargo.lock | 1 + crates/core/database/src/models/users/axum.rs | 9 +++--- crates/services/gifbox/Cargo.toml | 2 +- crates/services/gifbox/src/api.rs | 6 ++-- crates/services/gifbox/src/main.rs | 30 +++++++++++++++++-- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efcf2cb1..1274b75a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6517,6 +6517,7 @@ dependencies = [ "reqwest 0.12.23", "revolt-coalesced", "revolt-config", + "revolt-database", "revolt-models", "revolt-result", "serde", diff --git a/crates/core/database/src/models/users/axum.rs b/crates/core/database/src/models/users/axum.rs index a6d8b5a3..6b1f2f32 100644 --- a/crates/core/database/src/models/users/axum.rs +++ b/crates/core/database/src/models/users/axum.rs @@ -1,21 +1,20 @@ -use axum::{ - extract::{FromRef, FromRequestParts}, - http::request::Parts, -}; +use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts}; use revolt_result::{create_error, Error, Result}; use crate::{Database, User}; #[async_trait::async_trait] -impl FromRequestParts for User +impl FromRequestParts for User where Database: FromRef, + S: Send + Sync { type Rejection = Error; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { let db = Database::from_ref(state); + 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?; db.fetch_user(&bot.id).await diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index d2e65145..94cd0732 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -23,7 +23,7 @@ revolt-result = { version = "0.8.8", path = "../../core/result", features = [ "axum", ] } revolt-coalesced = { version = "0.8.8", path = "../../core/coalesced" } - +revolt-database = { version = "0.8.8", path = "../../core/database", features = ["axum-impl"] } # Axum / web server axum = { version = "0.7.5" } axum-extra = { version = "0.9", features = ["typed-header"] } diff --git a/crates/services/gifbox/src/api.rs b/crates/services/gifbox/src/api.rs index c137e7a3..07883652 100644 --- a/crates/services/gifbox/src/api.rs +++ b/crates/services/gifbox/src/api.rs @@ -4,10 +4,11 @@ use axum::{extract::{Query, State}, routing::get, Json, Router}; use revolt_result::{create_error, Result}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use revolt_database::User; -use crate::tenor::{Tenor, types}; +use crate::{AppState, tenor::{Tenor, types}}; -pub async fn router() -> Router { +pub async fn router() -> Router { Router::new() .route("/", get(root)) .route("/search", get(search)) @@ -53,6 +54,7 @@ struct SearchQueryParams { ) )] async fn search( + _user: User, Query(params): Query, State(tenor): State, ) -> Result>> { diff --git a/crates/services/gifbox/src/main.rs b/crates/services/gifbox/src/main.rs index 32d1ea58..3cfb2e95 100644 --- a/crates/services/gifbox/src/main.rs +++ b/crates/services/gifbox/src/main.rs @@ -1,8 +1,9 @@ use std::net::{Ipv4Addr, SocketAddr}; -use axum::Router; +use axum::{extract::FromRef, Router}; use revolt_config::config; +use revolt_database::{Database, DatabaseInfo}; use tokio::net::TcpListener; use utoipa::{ openapi::security::{Http, HttpAuthScheme, SecurityScheme}, @@ -10,9 +11,29 @@ use utoipa::{ }; use utoipa_scalar::{Scalar, Servable as ScalarServable}; +use crate::tenor::Tenor; + mod api; mod tenor; +#[derive(Clone)] +struct AppState { + pub database: Database, + pub tenor: Tenor +} + +impl FromRef for Database { + fn from_ref(state: &AppState) -> Self { + state.database.clone() + } +} + +impl FromRef for Tenor { + fn from_ref(state: &AppState) -> Self { + state.tenor.clone() + } +} + #[tokio::main] async fn main() -> Result<(), std::io::Error> { // Configure logging and environment @@ -48,13 +69,16 @@ async fn main() -> Result<(), std::io::Error> { } let config = config().await; - let tenor = tenor::Tenor::new(&config.api.security.tenor_key); + let state = AppState { + database: DatabaseInfo::Auto.connect().await.expect("Unable to connect to database"), + tenor: tenor::Tenor::new(&config.api.security.tenor_key) + }; // Configure Axum and router let app = Router::new() .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) .nest("/", api::router().await) - .with_state(tenor); + .with_state(state); // Configure TCP listener and bind tracing::info!("Listening on 0.0.0.0:14706"); From a92152d86da136997817e797c7af8e38731cdde8 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Fri, 8 Aug 2025 03:10:30 +0100 Subject: [PATCH 10/14] fix: use our own result types instead of tenors types add user auth --- crates/services/gifbox/src/api.rs | 28 +++++---- crates/services/gifbox/src/main.rs | 14 ++++- crates/services/gifbox/src/tenor/mod.rs | 64 +++++---------------- crates/services/gifbox/src/tenor/types.rs | 15 +++-- crates/services/gifbox/src/types.rs | 69 +++++++++++++++++++++++ 5 files changed, 119 insertions(+), 71 deletions(-) create mode 100644 crates/services/gifbox/src/types.rs diff --git a/crates/services/gifbox/src/api.rs b/crates/services/gifbox/src/api.rs index 07883652..36a29757 100644 --- a/crates/services/gifbox/src/api.rs +++ b/crates/services/gifbox/src/api.rs @@ -1,12 +1,18 @@ -use std::sync::Arc; - -use axum::{extract::{Query, State}, routing::get, Json, Router}; +use axum::{ + extract::{Query, State}, + routing::get, + Json, Router, +}; +use revolt_database::User; use revolt_result::{create_error, Result}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use revolt_database::User; -use crate::{AppState, tenor::{Tenor, types}}; +use crate::{ + tenor, + types, + AppState, +}; pub async fn router() -> Router { Router::new() @@ -43,7 +49,7 @@ async fn root() -> Json { struct SearchQueryParams { pub query: String, pub locale: String, - pub position: Option + pub position: Option, } #[utoipa::path( @@ -56,11 +62,13 @@ struct SearchQueryParams { async fn search( _user: User, Query(params): Query, - State(tenor): State, -) -> Result>> { + State(tenor): State, +) -> Result> { // Todo - tenor.search(¶ms.query, ¶ms.locale, params.position.as_deref()) + tenor + .search(¶ms.query, ¶ms.locale, params.position.as_deref()) .await .map_err(|_| create_error!(InternalError)) + .map(|results| results.as_ref().clone().into()) .map(Json) -} \ No newline at end of file +} diff --git a/crates/services/gifbox/src/main.rs b/crates/services/gifbox/src/main.rs index 3cfb2e95..a3339078 100644 --- a/crates/services/gifbox/src/main.rs +++ b/crates/services/gifbox/src/main.rs @@ -15,11 +15,12 @@ use crate::tenor::Tenor; mod api; mod tenor; +mod types; #[derive(Clone)] struct AppState { pub database: Database, - pub tenor: Tenor + pub tenor: Tenor, } impl FromRef for Database { @@ -45,11 +46,15 @@ async fn main() -> Result<(), std::io::Error> { modifiers(&SecurityAddon), paths( api::root, + api::search, ), components( schemas( revolt_result::Error, revolt_result::ErrorType, + types::SearchResponse, + types::MediaObject, + types::MediaResult, ) ) )] @@ -70,8 +75,11 @@ async fn main() -> Result<(), std::io::Error> { let config = config().await; let state = AppState { - database: DatabaseInfo::Auto.connect().await.expect("Unable to connect to database"), - tenor: tenor::Tenor::new(&config.api.security.tenor_key) + database: DatabaseInfo::Auto + .connect() + .await + .expect("Unable to connect to database"), + tenor: tenor::Tenor::new(&config.api.security.tenor_key), }; // Configure Axum and router diff --git a/crates/services/gifbox/src/tenor/mod.rs b/crates/services/gifbox/src/tenor/mod.rs index 56db6269..2ff3785c 100644 --- a/crates/services/gifbox/src/tenor/mod.rs +++ b/crates/services/gifbox/src/tenor/mod.rs @@ -1,10 +1,10 @@ use std::{sync::Arc, time::Duration}; -use reqwest::Client; -use tokio::sync::RwLock; use lru_time_cache::LruCache; -use urlencoding::encode as url_encode; +use reqwest::Client; use revolt_coalesced::{CoalescionService, CoalescionServiceConfig}; +use tokio::sync::RwLock; +use urlencoding::encode as url_encode; pub mod types; @@ -20,13 +20,13 @@ pub struct Tenor { pub key: Arc, pub client: Client, pub coalescion: CoalescionService, TenorError>>, - pub cache: Arc>>> + pub cache: Arc>>>, } impl Tenor { pub fn new(key: &str) -> Self { // 1 hour, 1k queries - let cache = LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60*60), 1000); + let cache = LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60 * 60), 1000); Self { key: Arc::from(key), @@ -34,18 +34,23 @@ impl Tenor { coalescion: CoalescionService::from_config(CoalescionServiceConfig { max_concurrent: Some(100), queue_requests: true, - max_queue: None + max_queue: None, }), cache: Arc::new(RwLock::new(cache)), } } - pub async fn search(&self, query: &str, locale: &str, position: Option<&str>) -> Result, TenorError> { + pub async fn search( + &self, + query: &str, + locale: &str, + position: Option<&str>, + ) -> Result, TenorError> { let unique_key = format!("{query}:{locale}:{position:?}"); if self.cache.read().await.contains_key(&unique_key) { if let Some(response) = self.cache.write().await.get(&unique_key) { - return Ok(response.clone()) + return Ok(response.clone()); } } @@ -54,7 +59,7 @@ impl Tenor { async move { let mut url = format!( - "{TENOR_API_BASE_URL}/search?key={}&q={}&client_key=Gifbox&locale={}&contentfilter=medium&limit=1", + "{TENOR_API_BASE_URL}/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=medium&limit=1", &self.key, url_encode(query), url_encode(locale), @@ -86,44 +91,3 @@ impl Tenor { (*res).clone() } } - -#[cfg(test)] -mod tests { - use super::*; - use revolt_config::config; - - #[tokio::test(flavor = "current_thread")] - async fn test() { - let config = config().await; - - let tenor = Tenor::new(&config.api.security.tenor_key); - - let results =tenor.search("amog", "en_US", None).await.unwrap(); - - let result = &results.results[0]; - - println!("{:?}", result.media_formats.iter().next().unwrap()); - } - - #[tokio::test(flavor = "current_thread")] - async fn test_2() { - let config = config().await; - let tenor = Tenor::new(&config.api.security.tenor_key); - - let mut tasks = Vec::new(); - - for i in 1..1001 { - let tenor = tenor.clone(); - println!("creating search task {i}"); - - tasks.push((i, tokio::spawn(async move { - tenor.search(&format!("amog-{i}"), "en_US", None).await - }))); - }; - - for (i, task) in tasks { - task.await.unwrap().unwrap(); - println!("Got result for {i}"); - }; - } -} \ No newline at end of file diff --git a/crates/services/gifbox/src/tenor/types.rs b/crates/services/gifbox/src/tenor/types.rs index a712fcb7..b4ad4a08 100644 --- a/crates/services/gifbox/src/tenor/types.rs +++ b/crates/services/gifbox/src/tenor/types.rs @@ -1,12 +1,11 @@ use std::collections::HashMap; -use serde::{Serialize, Deserialize}; - +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct SearchResponse { - pub results: Vec, - pub next: String + pub results: Vec, + pub next: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] @@ -14,11 +13,11 @@ pub struct MediaObject { pub url: String, pub dims: Vec, pub duration: f64, - pub size: f64 + pub size: f64, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct TenorResult { +pub struct MediaResult { pub created: f64, #[serde(default)] pub hasaudio: bool, @@ -32,5 +31,5 @@ pub struct TenorResult { pub hascaption: bool, pub flags: Vec, pub bg_color: Option, - pub url: String -} \ No newline at end of file + pub url: String, +} diff --git a/crates/services/gifbox/src/types.rs b/crates/services/gifbox/src/types.rs new file mode 100644 index 00000000..d4267602 --- /dev/null +++ b/crates/services/gifbox/src/types.rs @@ -0,0 +1,69 @@ +use std::collections::HashMap; + +use utoipa::ToSchema; +use serde::{Serialize, Deserialize}; + +use crate::tenor::types; + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +/// Response containing the current results and the id of the next result for pagination. +pub struct SearchResponse { + /// Current gif results. + pub results: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + /// Id of the next result. + pub next: Option, +} + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +/// Indivual gif result. +pub struct MediaResult { + /// Unique Tenor id. + pub id: String, + /// Mapping of each file format and url of the file. + pub media_formats: HashMap, + /// Public Tenor web url for the gif. + pub url: String +} + +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +/// Represents the gif in a certain file format. +pub struct MediaObject { + /// File url of the gif in a certain format. + pub url: String, + /// Width and height of the file in px. + pub dimensions: Vec, +} + + +impl From for SearchResponse { + fn from(value: types::SearchResponse) -> Self { + Self { + results: value.results.into_iter().map(|result| result.into()).collect(), + next: if value.next.is_empty() { + None + } else { + Some(value.next) + } + } + } +} + +impl From for MediaResult { + fn from(value: types::MediaResult) -> Self { + Self { + id: value.id, + media_formats: value.media_formats.into_iter().map(|(k, v)| (k, v.into())).collect(), + url: value.url + } + } +} + +impl From for MediaObject { + fn from(value: types::MediaObject) -> Self { + Self { + url: value.url, + dimensions: value.dims + } + } +} \ No newline at end of file From 5885e067a627b8fff1c8ce2bf9e852ff8cf3f07a Mon Sep 17 00:00:00 2001 From: Zomatree Date: Thu, 11 Sep 2025 14:18:28 +0100 Subject: [PATCH 11/14] feat: trending and categories routes --- crates/services/gifbox/src/api.rs | 74 ------- crates/services/gifbox/src/main.rs | 56 +++-- .../services/gifbox/src/routes/categories.rs | 47 +++++ crates/services/gifbox/src/routes/mod.rs | 15 ++ crates/services/gifbox/src/routes/root.rs | 22 ++ crates/services/gifbox/src/routes/search.rs | 51 +++++ crates/services/gifbox/src/routes/trending.rs | 45 +++++ crates/services/gifbox/src/tenor/mod.rs | 191 +++++++++++++++--- crates/services/gifbox/src/tenor/types.rs | 20 +- crates/services/gifbox/src/types.rs | 64 ++++-- 10 files changed, 437 insertions(+), 148 deletions(-) delete mode 100644 crates/services/gifbox/src/api.rs create mode 100644 crates/services/gifbox/src/routes/categories.rs create mode 100644 crates/services/gifbox/src/routes/mod.rs create mode 100644 crates/services/gifbox/src/routes/root.rs create mode 100644 crates/services/gifbox/src/routes/search.rs create mode 100644 crates/services/gifbox/src/routes/trending.rs diff --git a/crates/services/gifbox/src/api.rs b/crates/services/gifbox/src/api.rs deleted file mode 100644 index 36a29757..00000000 --- a/crates/services/gifbox/src/api.rs +++ /dev/null @@ -1,74 +0,0 @@ -use axum::{ - extract::{Query, State}, - routing::get, - Json, Router, -}; -use revolt_database::User; -use revolt_result::{create_error, Result}; -use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; - -use crate::{ - tenor, - types, - AppState, -}; - -pub async fn router() -> Router { - Router::new() - .route("/", get(root)) - .route("/search", get(search)) -} - -/// Successful root response -#[derive(Serialize, Debug, ToSchema)] -pub struct RootResponse { - message: &'static str, - version: &'static str, -} - -/// Capture crate version from Cargo -static CRATE_VERSION: &str = env!("CARGO_PKG_VERSION"); - -/// Root response from service -#[utoipa::path( - get, - path = "/", - responses( - (status = 200, description = "Echo response", body = RootResponse) - ) -)] -async fn root() -> Json { - Json(RootResponse { - message: "Gifbox lives on!", - version: CRATE_VERSION, - }) -} - -#[derive(Deserialize)] -struct SearchQueryParams { - pub query: String, - pub locale: String, - pub position: Option, -} - -#[utoipa::path( - get, - path = "/search", - responses( - (status = 200, description = "Search results", body = SearchResponse) - ) -)] -async fn search( - _user: User, - Query(params): Query, - State(tenor): State, -) -> Result> { - // Todo - tenor - .search(¶ms.query, ¶ms.locale, params.position.as_deref()) - .await - .map_err(|_| create_error!(InternalError)) - .map(|results| results.as_ref().clone().into()) - .map(Json) -} diff --git a/crates/services/gifbox/src/main.rs b/crates/services/gifbox/src/main.rs index a3339078..86588518 100644 --- a/crates/services/gifbox/src/main.rs +++ b/crates/services/gifbox/src/main.rs @@ -6,14 +6,14 @@ use revolt_config::config; use revolt_database::{Database, DatabaseInfo}; use tokio::net::TcpListener; use utoipa::{ - openapi::security::{Http, HttpAuthScheme, SecurityScheme}, + openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, Modify, OpenApi, }; use utoipa_scalar::{Scalar, Servable as ScalarServable}; use crate::tenor::Tenor; -mod api; +mod routes; mod tenor; mod types; @@ -35,6 +35,26 @@ impl FromRef for Tenor { } } +struct TokenAddon; + +impl Modify for TokenAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + let components = openapi.components.get_or_insert_default(); + + components.add_security_scheme( + "User Token", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new( + "X-Session-Token".to_string(), + ))), + ); + + components.add_security_scheme( + "Bot Token", + SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Bot-Token".to_string()))), + ); + } +} + #[tokio::main] async fn main() -> Result<(), std::io::Error> { // Configure logging and environment @@ -43,36 +63,28 @@ async fn main() -> Result<(), std::io::Error> { // Configure API schema #[derive(OpenApi)] #[openapi( - modifiers(&SecurityAddon), + modifiers(&TokenAddon), paths( - api::root, - api::search, + routes::categories::categories, + routes::root::root, + routes::search::search, + routes::trending::trending, + ), + tags( + (name = "Misc", description = "Misc routes for microservice."), + (name = "GIFs", description = "All routes for requesting GIFs from tenor.") ), components( schemas( revolt_result::Error, revolt_result::ErrorType, - types::SearchResponse, - types::MediaObject, types::MediaResult, + types::MediaObject, ) - ) + ), )] struct ApiDoc; - struct SecurityAddon; - - impl Modify for SecurityAddon { - fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { - if let Some(components) = openapi.components.as_mut() { - components.add_security_scheme( - "api_key", - SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), - ) - } - } - } - let config = config().await; let state = AppState { database: DatabaseInfo::Auto @@ -85,7 +97,7 @@ async fn main() -> Result<(), std::io::Error> { // Configure Axum and router let app = Router::new() .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) - .nest("/", api::router().await) + .nest("/", routes::router()) .with_state(state); // Configure TCP listener and bind diff --git a/crates/services/gifbox/src/routes/categories.rs b/crates/services/gifbox/src/routes/categories.rs new file mode 100644 index 00000000..ae85126d --- /dev/null +++ b/crates/services/gifbox/src/routes/categories.rs @@ -0,0 +1,47 @@ +use axum::{ + extract::{Query, State}, + Json, +}; +use revolt_database::User; +use revolt_result::{create_error, Result}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::{tenor, types}; + +#[derive(Deserialize, IntoParams)] +pub struct CategoriesQueryParams { + #[param(example = "en_US")] + pub locale: String, +} + +/// Trending GIF categories +#[utoipa::path( + get, + path = "/categories", + tag = "GIFs", + security(("User Token" = []), ("Bot Token" = [])), + params(CategoriesQueryParams), + responses( + (status = 200, description = "Categories results", body = inline(Vec)) + ) +)] +pub async fn categories( + _user: User, + Query(params): Query, + State(tenor): State, +) -> Result>> { + tenor + .categories(¶ms.locale) + .await + .map_err(|_| create_error!(InternalError)) + .map(|results| { + (*results) + .clone() + .tags + .into_iter() + .map(|cat| cat.into()) + .collect() + }) + .map(Json) +} diff --git a/crates/services/gifbox/src/routes/mod.rs b/crates/services/gifbox/src/routes/mod.rs new file mode 100644 index 00000000..efee317f --- /dev/null +++ b/crates/services/gifbox/src/routes/mod.rs @@ -0,0 +1,15 @@ +use crate::AppState; +use axum::routing::{get, Router}; + +pub mod categories; +pub mod root; +pub mod search; +pub mod trending; + +pub fn router() -> Router { + Router::new() + .route("/", get(root::root)) + .route("/categories", get(categories::categories)) + .route("/search", get(search::search)) + .route("/trending", get(trending::trending)) +} diff --git a/crates/services/gifbox/src/routes/root.rs b/crates/services/gifbox/src/routes/root.rs new file mode 100644 index 00000000..36acb877 --- /dev/null +++ b/crates/services/gifbox/src/routes/root.rs @@ -0,0 +1,22 @@ +use axum::Json; + +use crate::types; + +/// Capture crate version from Cargo +static CRATE_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Root response from service +#[utoipa::path( + get, + path = "/", + tag = "Misc", + responses( + (status = 200, description = "Root response", body = inline(types::RootResponse)) + ) +)] +pub async fn root() -> Json> { + Json(types::RootResponse { + message: "Gifbox lives on!", + version: CRATE_VERSION, + }) +} \ No newline at end of file diff --git a/crates/services/gifbox/src/routes/search.rs b/crates/services/gifbox/src/routes/search.rs new file mode 100644 index 00000000..ec0e6c6e --- /dev/null +++ b/crates/services/gifbox/src/routes/search.rs @@ -0,0 +1,51 @@ +use axum::{ + extract::{Query, State}, + Json, +}; +use revolt_database::User; +use revolt_result::{create_error, Result}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::{tenor, types}; + +#[derive(Deserialize, IntoParams)] +pub struct SearchQueryParams { + #[param(example = "Wave")] + pub query: String, + #[param(example = "en_US")] + pub locale: String, + pub limit: Option, + pub is_category: Option, + pub position: Option, +} + +/// Searches for GIFs with a query +#[utoipa::path( + get, + path = "/search", + tag = "GIFs", + security(("User Token" = []), ("Bot Token" = [])), + params(SearchQueryParams), + responses( + (status = 200, description = "Search results", body = inline(types::PaginatedMediaResponse)) + ) +)] +pub async fn search( + _user: User, + Query(params): Query, + State(tenor): State, +) -> Result> { + tenor + .search( + ¶ms.query, + ¶ms.locale, + params.limit.unwrap_or(50), + params.is_category.unwrap_or_default(), + params.position.as_deref().unwrap_or_default(), + ) + .await + .map_err(|_| create_error!(InternalError)) + .map(|results| results.as_ref().clone().into()) + .map(Json) +} diff --git a/crates/services/gifbox/src/routes/trending.rs b/crates/services/gifbox/src/routes/trending.rs new file mode 100644 index 00000000..f08ed242 --- /dev/null +++ b/crates/services/gifbox/src/routes/trending.rs @@ -0,0 +1,45 @@ +use axum::{ + extract::{Query, State}, + Json, +}; +use revolt_database::User; +use revolt_result::{create_error, Result}; +use serde::{Deserialize}; +use utoipa::{IntoParams}; + +use crate::{ + tenor, + types, +}; + +#[derive(Deserialize, IntoParams)] +pub struct TrendingQueryParams { + #[param(example = "en_US")] + pub locale: String, + pub limit: Option, + pub position: Option +} + +/// Trending GIFs +#[utoipa::path( + get, + path = "/featured", + tag = "GIFs", + security(("User Token" = []), ("Bot Token" = [])), + params(TrendingQueryParams), + responses( + (status = 200, description = "Trending results", body = inline(types::PaginatedMediaResponse)) + ) +)] +pub async fn trending( + _user: User, + Query(params): Query, + State(tenor): State, +) -> Result> { + tenor + .featured(¶ms.locale, params.limit.unwrap_or(50), params.position.as_deref().unwrap_or_default()) + .await + .map_err(|_| create_error!(InternalError)) + .map(|results| results.as_ref().clone().into()) + .map(Json) +} diff --git a/crates/services/gifbox/src/tenor/mod.rs b/crates/services/gifbox/src/tenor/mod.rs index 2ff3785c..da9b97aa 100644 --- a/crates/services/gifbox/src/tenor/mod.rs +++ b/crates/services/gifbox/src/tenor/mod.rs @@ -3,6 +3,7 @@ use std::{sync::Arc, time::Duration}; use lru_time_cache::LruCache; use reqwest::Client; use revolt_coalesced::{CoalescionService, CoalescionServiceConfig}; +use serde::de::DeserializeOwned; use tokio::sync::RwLock; use urlencoding::encode as url_encode; @@ -19,34 +20,89 @@ pub enum TenorError { pub struct Tenor { pub key: Arc, pub client: Client, - pub coalescion: CoalescionService, TenorError>>, - pub cache: Arc>>>, + + pub cache: Arc>>>, + pub cache_coalescion: + CoalescionService, TenorError>>, + + pub categories: Arc>>>, + pub categories_coalescion: + CoalescionService, TenorError>>, + + pub featured: Arc>>>, + pub featured_coalescion: + CoalescionService, TenorError>>, } impl Tenor { pub fn new(key: &str) -> Self { - // 1 hour, 1k queries - let cache = LruCache::with_expiry_duration_and_capacity(Duration::from_secs(60 * 60), 1000); - Self { key: Arc::from(key), client: Client::new(), - coalescion: CoalescionService::from_config(CoalescionServiceConfig { + + // 1 hour, 1k requests + cache: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity( + Duration::from_secs(60 * 60), + 1000, + ))), + cache_coalescion: CoalescionService::from_config(CoalescionServiceConfig { + max_concurrent: Some(100), + queue_requests: true, + max_queue: None, + }), + + // 1 day, 1k requests + categories: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity( + Duration::from_secs(60 * 60 * 24), + 1000, + ))), + categories_coalescion: CoalescionService::from_config(CoalescionServiceConfig { + max_concurrent: Some(100), + queue_requests: true, + max_queue: None, + }), + + // 1 day, 1k requests + featured: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity( + Duration::from_secs(60 * 60 * 24), + 1000, + ))), + featured_coalescion: CoalescionService::from_config(CoalescionServiceConfig { max_concurrent: Some(100), queue_requests: true, max_queue: None, }), - cache: Arc::new(RwLock::new(cache)), } } + pub async fn request(&self, path: String) -> Result, TenorError> { + let response = self + .client + .get(format!("{TENOR_API_BASE_URL}{path}")) + .send() + .await + .inspect_err(|e| { + revolt_config::capture_error(e); + }) + .map_err(|_| TenorError::HttpError)?; + + let text = response.text().await.map_err(|e| { + println!("{e:?}"); + TenorError::HttpError + })?; + + Ok(Arc::new(serde_json::from_str(&text).unwrap())) + } + pub async fn search( &self, query: &str, locale: &str, - position: Option<&str>, - ) -> Result, TenorError> { - let unique_key = format!("{query}:{locale}:{position:?}"); + limit: u32, + is_category: bool, + position: &str, + ) -> Result, TenorError> { + let unique_key = format!("s:{query}:{locale}:{is_category}:{position}"); if self.cache.read().await.contains_key(&unique_key) { if let Some(response) = self.cache.write().await.get(&unique_key) { @@ -54,32 +110,24 @@ impl Tenor { } } - let res = self.coalescion.execute(unique_key.clone(), || { - let client = self.client.clone(); + let res = self.cache_coalescion.execute(unique_key.clone(), || { + let mut path = format!( + "/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}", + &self.key, + url_encode(query), + url_encode(locale), + ); - async move { - let mut url = format!( - "{TENOR_API_BASE_URL}/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=medium&limit=1", - &self.key, - url_encode(query), - url_encode(locale), - ); + if !position.is_empty() { + path.push_str("&pos="); + path.push_str(position); + }; - if let Some(position) = position { - url.push_str("&pos="); - url.push_str(position); - }; - - let response = client.get(url) - .send() - .await - .inspect_err(|e| { revolt_config::capture_error(e); }) - .map_err(|_| TenorError::HttpError)?; - - let text = response.text().await.map_err(|e| { println!("{e:?}"); TenorError::HttpError })?; - - Ok(Arc::new(serde_json::from_str(&text).unwrap())) + if is_category { + path.push_str("&component=categories"); } + + self.request(path) }) .await .unwrap(); @@ -90,4 +138,81 @@ impl Tenor { (*res).clone() } + + pub async fn categories( + &self, + locale: &str, + ) -> Result, TenorError> { + let unique_key = format!("c-{locale}"); + + if self.categories.read().await.contains_key(&unique_key) { + if let Some(response) = self.categories.write().await.get(&unique_key) { + return Ok(response.clone()); + } + } + + let res = self + .categories_coalescion + .execute(unique_key.clone(), || { + let path = format!( + "/categories?key={}&client_key=Gifbox&locale={}&contentfilter=high", + &self.key, + url_encode(locale), + ); + + self.request(path) + }) + .await + .unwrap(); + + if let Ok(resp) = &*res { + self.categories + .write() + .await + .insert(unique_key, resp.clone()); + } + + (*res).clone() + } + + pub async fn featured( + &self, + locale: &str, + limit: u32, + position: &str, + ) -> Result, TenorError> { + let unique_key = format!("f-{locale}-{limit}-{position}"); + + if self.categories.read().await.contains_key(&unique_key) { + if let Some(response) = self.featured.write().await.get(&unique_key) { + return Ok(response.clone()); + } + } + + let res = self.featured_coalescion.execute(unique_key.clone(), || { + let mut path = format!( + "/featured?key={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}", + &self.key, + url_encode(locale), + ); + + if !position.is_empty() { + path.push_str("&pos="); + path.push_str(position); + }; + + self.request(path) + }) + .await + .unwrap(); + + if let Ok(resp) = &*res { + self.featured + .write() + .await + .insert(unique_key, resp.clone()); + } + + (*res).clone() + } } diff --git a/crates/services/gifbox/src/tenor/types.rs b/crates/services/gifbox/src/tenor/types.rs index b4ad4a08..8165e4b4 100644 --- a/crates/services/gifbox/src/tenor/types.rs +++ b/crates/services/gifbox/src/tenor/types.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct SearchResponse { - pub results: Vec, +pub struct PaginatedMediaResponse { + pub results: Vec, pub next: String, } @@ -17,7 +17,7 @@ pub struct MediaObject { } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -pub struct MediaResult { +pub struct MediaResponse { pub created: f64, #[serde(default)] pub hasaudio: bool, @@ -33,3 +33,17 @@ pub struct MediaResult { pub bg_color: Option, pub url: String, } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct CategoriesResponse { + pub locale: String, + pub tags: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct CategoryResponse { + pub searchterm: String, + pub path: String, + pub image: String, + pub name: String, +} diff --git a/crates/services/gifbox/src/types.rs b/crates/services/gifbox/src/types.rs index d4267602..1ee07e8a 100644 --- a/crates/services/gifbox/src/types.rs +++ b/crates/services/gifbox/src/types.rs @@ -1,13 +1,20 @@ use std::collections::HashMap; +use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use serde::{Serialize, Deserialize}; use crate::tenor::types; -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +/// Successful root response +#[derive(Serialize, Debug, ToSchema)] +pub struct RootResponse<'a> { + pub message: &'a str, + pub version: &'a str, +} + /// Response containing the current results and the id of the next result for pagination. -pub struct SearchResponse { +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +pub struct PaginatedMediaResponse { /// Current gif results. pub results: Vec, #[serde(skip_serializing_if = "Option::is_none")] @@ -15,19 +22,19 @@ pub struct SearchResponse { pub next: Option, } -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] /// Indivual gif result. +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] pub struct MediaResult { /// Unique Tenor id. pub id: String, /// Mapping of each file format and url of the file. pub media_formats: HashMap, /// Public Tenor web url for the gif. - pub url: String + pub url: String, } -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] /// Represents the gif in a certain file format. +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] pub struct MediaObject { /// File url of the gif in a certain format. pub url: String, @@ -35,26 +42,42 @@ pub struct MediaObject { pub dimensions: Vec, } +/// Represents a GIF category +#[derive(Serialize, Deserialize, ToSchema, Clone, Debug, PartialEq)] +pub struct CategoryResponse { + /// Category title + pub title: String, + /// Category image + pub image: String, +} -impl From for SearchResponse { - fn from(value: types::SearchResponse) -> Self { +impl From for PaginatedMediaResponse { + fn from(value: types::PaginatedMediaResponse) -> Self { Self { - results: value.results.into_iter().map(|result| result.into()).collect(), + results: value + .results + .into_iter() + .map(|result| result.into()) + .collect(), next: if value.next.is_empty() { None } else { Some(value.next) - } + }, } } } -impl From for MediaResult { - fn from(value: types::MediaResult) -> Self { +impl From for MediaResult { + fn from(value: types::MediaResponse) -> Self { Self { id: value.id, - media_formats: value.media_formats.into_iter().map(|(k, v)| (k, v.into())).collect(), - url: value.url + media_formats: value + .media_formats + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + url: value.url, } } } @@ -63,7 +86,16 @@ impl From for MediaObject { fn from(value: types::MediaObject) -> Self { Self { url: value.url, - dimensions: value.dims + dimensions: value.dims, } } -} \ No newline at end of file +} + +impl From for CategoryResponse { + fn from(value: types::CategoryResponse) -> Self { + Self { + title: value.searchterm, + image: value.image, + } + } +} From db559985465fdd04dc7864a443b727f19d5711c5 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Mon, 15 Sep 2025 01:44:21 +0100 Subject: [PATCH 12/14] docs: document `revolt-coalesced` --- crates/core/coalesced/Cargo.toml | 2 +- crates/core/coalesced/src/config.rs | 1 + crates/core/coalesced/src/error.rs | 15 +++- crates/core/coalesced/src/lib.rs | 34 ++++++- crates/core/coalesced/src/service.rs | 88 ++++++++++++++----- crates/services/gifbox/Cargo.toml | 11 ++- .../services/gifbox/src/routes/categories.rs | 1 + crates/services/gifbox/src/routes/root.rs | 2 +- crates/services/gifbox/src/routes/search.rs | 5 ++ crates/services/gifbox/src/routes/trending.rs | 20 +++-- crates/services/gifbox/src/tenor/mod.rs | 48 ++++------ crates/services/gifbox/src/tenor/types.rs | 2 + 12 files changed, 154 insertions(+), 75 deletions(-) diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index 4485d65d..cb23a8ad 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -11,7 +11,7 @@ tokio = ["dep:tokio"] queue = ["dep:indexmap"] cache = ["dep:lru"] -default = ["tokio", "queue", "cache"] +default = ["tokio"] [dependencies] tokio = { version = "1.47.0", features = ["sync"], optional = true } diff --git a/crates/core/coalesced/src/config.rs b/crates/core/coalesced/src/config.rs index 00ef7990..23da1698 100644 --- a/crates/core/coalesced/src/config.rs +++ b/crates/core/coalesced/src/config.rs @@ -1,4 +1,5 @@ #[derive(Clone, PartialEq, Eq, Debug)] +/// Config values for [`CoalescionService`]. pub struct CoalescionServiceConfig { /// How many tasks are running at once pub max_concurrent: Option, diff --git a/crates/core/coalesced/src/error.rs b/crates/core/coalesced/src/error.rs index a5395597..6be89785 100644 --- a/crates/core/coalesced/src/error.rs +++ b/crates/core/coalesced/src/error.rs @@ -1,18 +1,25 @@ -use std::fmt::Display; +use std::fmt; -#[derive(Clone, PartialEq, Eq, Debug)] +#[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 Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +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") } } } diff --git a/crates/core/coalesced/src/lib.rs b/crates/core/coalesced/src/lib.rs index 6fd18267..89839e23 100644 --- a/crates/core/coalesced/src/lib.rs +++ b/crates/core/coalesced/src/lib.rs @@ -1,7 +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; \ No newline at end of file +pub use service::CoalescionService; diff --git a/crates/core/coalesced/src/service.rs b/crates/core/coalesced/src/service.rs index 3d3b1c9b..aadadc8a 100644 --- a/crates/core/coalesced/src/service.rs +++ b/crates/core/coalesced/src/service.rs @@ -1,6 +1,9 @@ -use std::{collections::{HashMap}, fmt::Debug, hash::Hash, sync::Arc, future::Future}; +use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc}; -use tokio::{sync::{watch::{channel as watch_channel, Receiver}, RwLock, Mutex}}; +use tokio::sync::{ + watch::{channel as watch_channel, Receiver}, + RwLock, +}; #[cfg(feature = "cache")] use lru::LruCache; @@ -10,20 +13,23 @@ use indexmap::IndexMap; use crate::{CoalescionServiceConfig, Error}; -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] #[allow(clippy::type_complexity)] -pub struct CoalescionService { +/// # Coalescion service +/// +/// See module description for example usage. +pub struct CoalescionService { config: Arc, - watchers: Arc, Error>>>>>>, + watchers: Arc, Error>>>>>>, #[cfg(feature = "queue")] - queue: Arc, Error>>>>>>, + queue: Arc, Error>>>>>>, #[cfg(feature = "cache")] - cache: Option>>>>, + cache: Option>>>>, } -impl CoalescionService { +impl CoalescionService { pub fn new() -> Self { - Self::default() + Default::default() } pub fn from_config(config: CoalescionServiceConfig) -> Self { @@ -38,22 +44,37 @@ impl CoalescionService>) -> Self { + pub fn from_cache( + config: CoalescionServiceConfig, + cache: LruCache>, + ) -> Self { Self { cache: Some(Arc::new(Mutex::new(cache))), ..Self::from_config(config) } } - async fn wait_for(&self, mut receiver: Receiver, Error>>>) -> Result, Error> { + async fn wait_for( + &self, + mut receiver: Receiver, Error>>>, + ) -> Result, 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 Fut, Fut: Future>(&self, id: Id, func: F) -> Result, Error> { + async fn insert_and_execute< + Value: Send + Sync + 'static, + F: FnOnce() -> Fut, + Fut: Future, + >( + &self, + id: Id, + func: F, + ) -> Result, Error> { let (send, recv) = watch_channel(None); self.watchers.write().await.insert(id.clone(), recv); @@ -61,7 +82,7 @@ impl CoalescionService)); }); #[cfg(feature = "cache")] @@ -76,11 +97,21 @@ impl CoalescionService Fut, Fut: Future>(&self, id: Id, func: F) -> Result, Error> { + /// 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, + >( + &self, + id: Id, + func: F, + ) -> Result, Error> { #[cfg(feature = "cache")] if let Some(cache) = self.cache.as_ref() { if let Some(value) = cache.lock().await.get(&id) { - return Ok(value.clone()) + return Arc::downcast::(value.clone()).map_err(|_| Error::DowncastError); } }; @@ -105,10 +136,14 @@ impl CoalescionService= length) { - return Err(Error::MaxQueue) + if self + .config + .max_queue + .is_some_and(|max_queue| max_queue >= length) + { + return Err(Error::MaxQueue); }; let (send, recv) = watch_channel(None); @@ -130,10 +165,14 @@ impl CoalescionService), + ); }); - return response + return response; } } } @@ -145,23 +184,24 @@ impl CoalescionService { - self.insert_and_execute(id, func).await - } + _ => 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 Default for CoalescionService { +impl Default for CoalescionService { fn default() -> Self { Self::from_config(CoalescionServiceConfig::default()) } diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 94cd0732..f67a04e8 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -22,8 +22,13 @@ revolt-result = { version = "0.8.8", path = "../../core/result", features = [ "utoipa", "axum", ] } -revolt-coalesced = { version = "0.8.8", path = "../../core/coalesced" } -revolt-database = { version = "0.8.8", path = "../../core/database", features = ["axum-impl"] } +revolt-coalesced = { version = "0.8.8", path = "../../core/coalesced", features = [ + "queue", +] } +revolt-database = { version = "0.8.8", path = "../../core/database", features = [ + "axum-impl", +] } + # Axum / web server axum = { version = "0.7.5" } axum-extra = { version = "0.9", features = ["typed-header"] } @@ -37,4 +42,4 @@ tracing = "0.1" # Utils lru_time_cache = "0.11.11" -urlencoding = "2.1.3" \ No newline at end of file +urlencoding = "2.1.3" diff --git a/crates/services/gifbox/src/routes/categories.rs b/crates/services/gifbox/src/routes/categories.rs index ae85126d..a79f03c0 100644 --- a/crates/services/gifbox/src/routes/categories.rs +++ b/crates/services/gifbox/src/routes/categories.rs @@ -11,6 +11,7 @@ use crate::{tenor, types}; #[derive(Deserialize, IntoParams)] pub struct CategoriesQueryParams { + /// Users locale #[param(example = "en_US")] pub locale: String, } diff --git a/crates/services/gifbox/src/routes/root.rs b/crates/services/gifbox/src/routes/root.rs index 36acb877..4bf3aed5 100644 --- a/crates/services/gifbox/src/routes/root.rs +++ b/crates/services/gifbox/src/routes/root.rs @@ -19,4 +19,4 @@ pub async fn root() -> Json> { message: "Gifbox lives on!", version: CRATE_VERSION, }) -} \ No newline at end of file +} diff --git a/crates/services/gifbox/src/routes/search.rs b/crates/services/gifbox/src/routes/search.rs index ec0e6c6e..ac541dc2 100644 --- a/crates/services/gifbox/src/routes/search.rs +++ b/crates/services/gifbox/src/routes/search.rs @@ -11,12 +11,17 @@ use crate::{tenor, types}; #[derive(Deserialize, IntoParams)] pub struct SearchQueryParams { + /// Search query #[param(example = "Wave")] pub query: String, + /// Users locale #[param(example = "en_US")] pub locale: String, + /// Amount of results to respond with pub limit: Option, + /// Flag for if searching in a gif category pub is_category: Option, + /// Value of `next` for getting the next page of results with the current search query pub position: Option, } diff --git a/crates/services/gifbox/src/routes/trending.rs b/crates/services/gifbox/src/routes/trending.rs index f08ed242..b8105524 100644 --- a/crates/services/gifbox/src/routes/trending.rs +++ b/crates/services/gifbox/src/routes/trending.rs @@ -4,20 +4,20 @@ use axum::{ }; use revolt_database::User; use revolt_result::{create_error, Result}; -use serde::{Deserialize}; -use utoipa::{IntoParams}; +use serde::Deserialize; +use utoipa::IntoParams; -use crate::{ - tenor, - types, -}; +use crate::{tenor, types}; #[derive(Deserialize, IntoParams)] pub struct TrendingQueryParams { #[param(example = "en_US")] + /// Users locale pub locale: String, + /// Amount of results to respond with pub limit: Option, - pub position: Option + /// Value of `next` for getting the next page of results of featured gifs + pub position: Option, } /// Trending GIFs @@ -37,7 +37,11 @@ pub async fn trending( State(tenor): State, ) -> Result> { tenor - .featured(¶ms.locale, params.limit.unwrap_or(50), params.position.as_deref().unwrap_or_default()) + .featured( + ¶ms.locale, + params.limit.unwrap_or(50), + params.position.as_deref().unwrap_or_default(), + ) .await .map_err(|_| create_error!(InternalError)) .map(|results| results.as_ref().clone().into()) diff --git a/crates/services/gifbox/src/tenor/mod.rs b/crates/services/gifbox/src/tenor/mod.rs index da9b97aa..c90b74ce 100644 --- a/crates/services/gifbox/src/tenor/mod.rs +++ b/crates/services/gifbox/src/tenor/mod.rs @@ -1,3 +1,5 @@ +//! Interal Tenor API wrapper + use std::{sync::Arc, time::Duration}; use lru_time_cache::LruCache; @@ -20,18 +22,11 @@ pub enum TenorError { pub struct Tenor { pub key: Arc, pub client: Client, - + pub coalescion: CoalescionService, pub cache: Arc>>>, - pub cache_coalescion: - CoalescionService, TenorError>>, pub categories: Arc>>>, - pub categories_coalescion: - CoalescionService, TenorError>>, - pub featured: Arc>>>, - pub featured_coalescion: - CoalescionService, TenorError>>, } impl Tenor { @@ -39,39 +34,29 @@ impl Tenor { Self { key: Arc::from(key), client: Client::new(), + coalescion: CoalescionService::from_config(CoalescionServiceConfig { + max_concurrent: Some(100), + queue_requests: true, + max_queue: None, + }), // 1 hour, 1k requests cache: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity( Duration::from_secs(60 * 60), 1000, ))), - cache_coalescion: CoalescionService::from_config(CoalescionServiceConfig { - max_concurrent: Some(100), - queue_requests: true, - max_queue: None, - }), // 1 day, 1k requests categories: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity( Duration::from_secs(60 * 60 * 24), 1000, ))), - categories_coalescion: CoalescionService::from_config(CoalescionServiceConfig { - max_concurrent: Some(100), - queue_requests: true, - max_queue: None, - }), // 1 day, 1k requests featured: Arc::new(RwLock::new(LruCache::with_expiry_duration_and_capacity( Duration::from_secs(60 * 60 * 24), 1000, ))), - featured_coalescion: CoalescionService::from_config(CoalescionServiceConfig { - max_concurrent: Some(100), - queue_requests: true, - max_queue: None, - }), } } @@ -110,7 +95,7 @@ impl Tenor { } } - let res = self.cache_coalescion.execute(unique_key.clone(), || { + let res = self.coalescion.execute(unique_key.clone(), || { let mut path = format!( "/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}", &self.key, @@ -127,7 +112,7 @@ impl Tenor { path.push_str("&component=categories"); } - self.request(path) + self.request::(path) }) .await .unwrap(); @@ -152,7 +137,7 @@ impl Tenor { } let res = self - .categories_coalescion + .coalescion .execute(unique_key.clone(), || { let path = format!( "/categories?key={}&client_key=Gifbox&locale={}&contentfilter=high", @@ -160,7 +145,7 @@ impl Tenor { url_encode(locale), ); - self.request(path) + self.request::(path) }) .await .unwrap(); @@ -189,7 +174,7 @@ impl Tenor { } } - let res = self.featured_coalescion.execute(unique_key.clone(), || { + let res = self.coalescion.execute(unique_key.clone(), || { let mut path = format!( "/featured?key={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}", &self.key, @@ -201,16 +186,13 @@ impl Tenor { path.push_str(position); }; - self.request(path) + self.request::(path) }) .await .unwrap(); if let Ok(resp) = &*res { - self.featured - .write() - .await - .insert(unique_key, resp.clone()); + self.featured.write().await.insert(unique_key, resp.clone()); } (*res).clone() diff --git a/crates/services/gifbox/src/tenor/types.rs b/crates/services/gifbox/src/tenor/types.rs index 8165e4b4..15658252 100644 --- a/crates/services/gifbox/src/tenor/types.rs +++ b/crates/services/gifbox/src/tenor/types.rs @@ -1,3 +1,5 @@ +//! Tenor API models + use std::collections::HashMap; use serde::{Deserialize, Serialize}; From 154204742d21cbeff6e2577b00f50b495ea44631 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Thu, 18 Sep 2025 21:46:08 +0100 Subject: [PATCH 13/14] feat: add ratelimits to gifbox --- Cargo.lock | 1 + crates/services/gifbox/Cargo.toml | 3 ++ crates/services/gifbox/src/main.rs | 47 +++++++++++++----------- crates/services/gifbox/src/ratelimits.rs | 32 ++++++++++++++++ 4 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 crates/services/gifbox/src/ratelimits.rs diff --git a/Cargo.lock b/Cargo.lock index 1274b75a..3270fbf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6519,6 +6519,7 @@ dependencies = [ "revolt-config", "revolt-database", "revolt-models", + "revolt-ratelimits", "revolt-result", "serde", "serde_json", diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index f67a04e8..b640766e 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -28,6 +28,9 @@ revolt-coalesced = { version = "0.8.8", path = "../../core/coalesced", features revolt-database = { version = "0.8.8", path = "../../core/database", features = [ "axum-impl", ] } +revolt-ratelimits = { version = "0.8.8", path = "../../core/ratelimits", features = [ + "axum", +] } # Axum / web server axum = { version = "0.7.5" } diff --git a/crates/services/gifbox/src/main.rs b/crates/services/gifbox/src/main.rs index 86588518..059cb22f 100644 --- a/crates/services/gifbox/src/main.rs +++ b/crates/services/gifbox/src/main.rs @@ -1,9 +1,10 @@ use std::net::{Ipv4Addr, SocketAddr}; -use axum::{extract::FromRef, Router}; +use axum::{extract::FromRef, middleware::from_fn_with_state, Router}; use revolt_config::config; use revolt_database::{Database, DatabaseInfo}; +use revolt_ratelimits::axum as ratelimiter; use tokio::net::TcpListener; use utoipa::{ openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, @@ -13,31 +14,21 @@ use utoipa_scalar::{Scalar, Servable as ScalarServable}; use crate::tenor::Tenor; +mod ratelimits; mod routes; mod tenor; mod types; -#[derive(Clone)] +#[derive(Clone, FromRef)] struct AppState { pub database: Database, pub tenor: Tenor, + pub ratelimit_storage: ratelimiter::RatelimitStorage, } -impl FromRef for Database { - fn from_ref(state: &AppState) -> Self { - state.database.clone() - } -} +struct SecurityAddon; -impl FromRef for Tenor { - fn from_ref(state: &AppState) -> Self { - state.tenor.clone() - } -} - -struct TokenAddon; - -impl Modify for TokenAddon { +impl Modify for SecurityAddon { fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { let components = openapi.components.get_or_insert_default(); @@ -63,7 +54,7 @@ async fn main() -> Result<(), std::io::Error> { // Configure API schema #[derive(OpenApi)] #[openapi( - modifiers(&TokenAddon), + modifiers(&SecurityAddon), paths( routes::categories::categories, routes::root::root, @@ -86,18 +77,30 @@ async fn main() -> Result<(), std::io::Error> { struct ApiDoc; let config = config().await; + + let database = DatabaseInfo::Auto + .connect() + .await + .expect("Unable to connect to database"); + + let tenor = tenor::Tenor::new(&config.api.security.tenor_key); + + let ratelimit_storage = ratelimiter::RatelimitStorage::new(ratelimits::GifboxRatelimits); + let state = AppState { - database: DatabaseInfo::Auto - .connect() - .await - .expect("Unable to connect to database"), - tenor: tenor::Tenor::new(&config.api.security.tenor_key), + database, + tenor, + ratelimit_storage, }; // Configure Axum and router let app = Router::new() .merge(Scalar::with_url("/scalar", ApiDoc::openapi())) .nest("/", routes::router()) + .layer(from_fn_with_state( + state.clone(), + ratelimiter::ratelimit_middleware, + )) .with_state(state); // Configure TCP listener and bind diff --git a/crates/services/gifbox/src/ratelimits.rs b/crates/services/gifbox/src/ratelimits.rs new file mode 100644 index 00000000..10c904d4 --- /dev/null +++ b/crates/services/gifbox/src/ratelimits.rs @@ -0,0 +1,32 @@ +use axum::http::request::Parts; +use revolt_ratelimits::ratelimiter::RatelimitResolver; + +pub struct GifboxRatelimits; + +impl RatelimitResolver for GifboxRatelimits { + fn resolve_bucket<'a>(&self, parts: &'a Parts) -> (&'a str, Option<&'a str>) { + let path = parts + .uri + .path() + .trim_matches('/') + .split_terminator("/") + .next(); + + match path { + Some("categories") => ("categories", None), + Some("trending") => ("trending", None), + Some("search") => ("search", None), + _ => ("any", None), + } + } + + fn resolve_bucket_limit(&self, bucket: &str) -> u32 { + match bucket { + "categories" => 2, + "trending" => 5, + "search" => 10, + "any" => u32::MAX, + _ => unreachable!("Bucket defined but no limit set"), + } + } +} From 38dd4d10797b3e6e397fc219e818f379bdff19f2 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Fri, 19 Sep 2025 01:48:23 +0100 Subject: [PATCH 14/14] fix: swap to using reqwest for query building --- Cargo.lock | 1 - crates/services/gifbox/Cargo.toml | 1 - crates/services/gifbox/Dockerfile | 2 - crates/services/gifbox/src/tenor/mod.rs | 85 ++++++++++++------------- 4 files changed, 42 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3270fbf3..01223df4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6525,7 +6525,6 @@ dependencies = [ "serde_json", "tokio 1.47.1", "tracing", - "urlencoding", "utoipa", "utoipa-scalar", ] diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index b640766e..3e2b4173 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -45,4 +45,3 @@ tracing = "0.1" # Utils lru_time_cache = "0.11.11" -urlencoding = "2.1.3" diff --git a/crates/services/gifbox/Dockerfile b/crates/services/gifbox/Dockerfile index 5668448f..196409b7 100644 --- a/crates/services/gifbox/Dockerfile +++ b/crates/services/gifbox/Dockerfile @@ -4,8 +4,6 @@ FROM ghcr.io/revoltchat/base:latest AS builder # Bundle Stage FROM gcr.io/distroless/cc-debian12:nonroot COPY --from=builder /home/rust/src/target/release/revolt-gifbox ./ -COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/local/bin/ -COPY --from=mwader/static-ffmpeg:7.0.2 /ffprobe /usr/local/bin/ EXPOSE 14706 USER nonroot diff --git a/crates/services/gifbox/src/tenor/mod.rs b/crates/services/gifbox/src/tenor/mod.rs index c90b74ce..a9971b17 100644 --- a/crates/services/gifbox/src/tenor/mod.rs +++ b/crates/services/gifbox/src/tenor/mod.rs @@ -1,4 +1,4 @@ -//! Interal Tenor API wrapper +//! Internal Tenor API wrapper use std::{sync::Arc, time::Duration}; @@ -7,7 +7,6 @@ use reqwest::Client; use revolt_coalesced::{CoalescionService, CoalescionServiceConfig}; use serde::de::DeserializeOwned; use tokio::sync::RwLock; -use urlencoding::encode as url_encode; pub mod types; @@ -60,10 +59,11 @@ impl Tenor { } } - pub async fn request(&self, path: String) -> Result, TenorError> { + pub async fn request(&self, path: &str, query: &[Option<(&str, &str)>]) -> Result, TenorError> { let response = self .client .get(format!("{TENOR_API_BASE_URL}{path}")) + .query(query) .send() .await .inspect_err(|e| { @@ -72,7 +72,7 @@ impl Tenor { .map_err(|_| TenorError::HttpError)?; let text = response.text().await.map_err(|e| { - println!("{e:?}"); + revolt_config::capture_error(&e); TenorError::HttpError })?; @@ -95,24 +95,21 @@ impl Tenor { } } - let res = self.coalescion.execute(unique_key.clone(), || { - let mut path = format!( - "/search?key={}&q={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}", - &self.key, - url_encode(query), - url_encode(locale), - ); - - if !position.is_empty() { - path.push_str("&pos="); - path.push_str(position); - }; - - if is_category { - path.push_str("&component=categories"); - } - - self.request::(path) + let res = self.coalescion.execute(unique_key.clone(), || async move { + self.request::( + "/search", + &[ + Some(("key", &self.key)), + Some(("q", query)), + Some(("client_key", "Gifbox")), + Some(("media_filter", "webm,tinywebm")), + Some(("locale", locale)), + Some(("contentfilter", "high")), + Some(("limit", &limit.to_string())), + position.is_empty().then_some(("pos", position)), + is_category.then_some(("component", "categories")) + ] + ).await }) .await .unwrap(); @@ -138,14 +135,16 @@ impl Tenor { let res = self .coalescion - .execute(unique_key.clone(), || { - let path = format!( - "/categories?key={}&client_key=Gifbox&locale={}&contentfilter=high", - &self.key, - url_encode(locale), - ); - - self.request::(path) + .execute(unique_key.clone(), || async move { + self.request::( + "/categories", + &[ + Some(("key", &self.key)), + Some(("client_key", "Gifbox")), + Some(("locale", locale)), + Some(("contentfilter", "high")), + ] + ).await }) .await .unwrap(); @@ -174,19 +173,19 @@ impl Tenor { } } - let res = self.coalescion.execute(unique_key.clone(), || { - let mut path = format!( - "/featured?key={}&client_key=Gifbox&media_filter=webm,tinywebm&locale={}&contentfilter=high&limit={limit}", - &self.key, - url_encode(locale), - ); - - if !position.is_empty() { - path.push_str("&pos="); - path.push_str(position); - }; - - self.request::(path) + let res = self.coalescion.execute(unique_key.clone(), || async move { + self.request::( + "/featured", + &[ + Some(("key", &self.key)), + Some(("client_key", "Gifbox")), + Some(("media_filter", "webm,tinywebm")), + Some(("locale", locale)), + Some(("contentfilter", "high")), + Some(("limit", &limit.to_string())), + position.is_empty().then_some(("pos", position)), + ] + ).await }) .await .unwrap();