From 841985d3b994df1c6eefab2fc7ecbd77ab22c493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Sun, 3 May 2026 03:04:28 +0300 Subject: [PATCH 01/39] feat: add role icon support (#724) Signed-off-by: ispik --- .../core/database/src/models/files/model.rs | 20 +++++++++++++++++++ .../core/database/src/models/servers/model.rs | 7 +++++++ .../src/models/servers/ops/mongodb.rs | 1 + crates/core/database/src/util/bridge/v0.rs | 16 ++++++++++----- crates/core/models/src/v0/servers.rs | 9 +++++++++ crates/delta/src/routes/servers/roles_edit.rs | 20 ++++++++++++++++--- 6 files changed, 65 insertions(+), 8 deletions(-) diff --git a/crates/core/database/src/models/files/model.rs b/crates/core/database/src/models/files/model.rs index 5ea78f9b..1aaf8b82 100644 --- a/crates/core/database/src/models/files/model.rs +++ b/crates/core/database/src/models/files/model.rs @@ -70,6 +70,7 @@ auto_derived!( LegacyGroupIcon, ChannelIcon, ServerIcon, + RoleIcon, } /// Information about what the file was used for @@ -239,4 +240,23 @@ impl File { ) .await } + + /// Use a file for a role icon + pub async fn use_role_icon( + db: &Database, + id: &str, + parent: &str, + uploader_id: &str, + ) -> Result { + db.find_and_use_attachment( + id, + "icons", + FileUsedFor { + id: parent.to_owned(), + object_type: FileUsedForType::RoleIcon, + }, + uploader_id.to_owned(), + ) + .await + } } diff --git a/crates/core/database/src/models/servers/model.rs b/crates/core/database/src/models/servers/model.rs index 183dd3d2..50c45c59 100644 --- a/crates/core/database/src/models/servers/model.rs +++ b/crates/core/database/src/models/servers/model.rs @@ -86,6 +86,9 @@ auto_derived_partial!( /// Ranking of this role #[serde(default)] pub rank: i64, + /// Custom icon attachment + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, }, "PartialRole" ); @@ -129,6 +132,7 @@ auto_derived!( /// Optional fields on server object pub enum FieldsRole { Colour, + Icon, } ); @@ -305,6 +309,7 @@ impl Role { colour: self.colour, hoist: Some(self.hoist), rank: Some(self.rank), + icon: self.icon, } } @@ -318,6 +323,7 @@ impl Role { colour: None, hoist: false, permissions: Default::default(), + icon: None, }; db.insert_role(&server.id, &role).await?; @@ -367,6 +373,7 @@ impl Role { pub fn remove_field(&mut self, field: &FieldsRole) { match field { FieldsRole::Colour => self.colour = None, + FieldsRole::Icon => self.icon = None, } } diff --git a/crates/core/database/src/models/servers/ops/mongodb.rs b/crates/core/database/src/models/servers/ops/mongodb.rs index d8ef26f2..964de410 100644 --- a/crates/core/database/src/models/servers/ops/mongodb.rs +++ b/crates/core/database/src/models/servers/ops/mongodb.rs @@ -172,6 +172,7 @@ impl IntoDocumentPath for FieldsRole { fn as_path(&self) -> Option<&'static str> { Some(match self { FieldsRole::Colour => "colour", + FieldsRole::Icon => "icon", }) } } diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index 2e637795..45b80e55 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -190,7 +190,7 @@ impl From for Channel { role_permissions, nsfw, voice, - slowmode + slowmode, } => Channel::TextChannel { id, server, @@ -202,7 +202,7 @@ impl From for Channel { role_permissions, nsfw, voice: voice.map(|voice| voice.into()), - slowmode + slowmode, }, } } @@ -256,7 +256,7 @@ impl From for crate::Channel { role_permissions, nsfw, voice, - slowmode + slowmode, } => crate::Channel::TextChannel { id, server, @@ -268,7 +268,7 @@ impl From for crate::Channel { role_permissions, nsfw, voice: voice.map(|voice| voice.into()), - slowmode + slowmode, }, } } @@ -307,7 +307,7 @@ impl From for crate::PartialChannel { default_permissions: value.default_permissions, last_message_id: value.last_message_id, voice: value.voice.map(|voice| voice.into()), - slowmode: value.slowmode + slowmode: value.slowmode, } } } @@ -926,6 +926,7 @@ impl From for Role { colour: value.colour, hoist: value.hoist, rank: value.rank, + icon: value.icon.map(|f| f.into()), } } } @@ -939,6 +940,7 @@ impl From for crate::Role { colour: value.colour, hoist: value.hoist, rank: value.rank, + icon: value.icon.map(|f| f.into()), } } } @@ -952,6 +954,7 @@ impl From for PartialRole { colour: value.colour, hoist: value.hoist, rank: value.rank, + icon: value.icon.map(|f| f.into()), } } } @@ -965,6 +968,7 @@ impl From for crate::PartialRole { colour: value.colour, hoist: value.hoist, rank: value.rank, + icon: value.icon.map(|f| f.into()), } } } @@ -973,6 +977,7 @@ impl From for FieldsRole { fn from(value: crate::FieldsRole) -> Self { match value { crate::FieldsRole::Colour => FieldsRole::Colour, + crate::FieldsRole::Icon => FieldsRole::Icon, } } } @@ -981,6 +986,7 @@ impl From for crate::FieldsRole { fn from(value: FieldsRole) -> Self { match value { FieldsRole::Colour => crate::FieldsRole::Colour, + FieldsRole::Icon => crate::FieldsRole::Icon, } } } diff --git a/crates/core/models/src/v0/servers.rs b/crates/core/models/src/v0/servers.rs index d9835443..2aab765e 100644 --- a/crates/core/models/src/v0/servers.rs +++ b/crates/core/models/src/v0/servers.rs @@ -106,6 +106,9 @@ auto_derived_partial!( /// Ranking of this role #[cfg_attr(feature = "serde", serde(default))] pub rank: i64, + /// Role icon + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub icon: Option, }, "PartialRole" ); @@ -123,6 +126,7 @@ auto_derived!( /// Optional fields on server object pub enum FieldsRole { Colour, + Icon, } /// Channel category @@ -278,6 +282,11 @@ auto_derived!( /// /// **Removed** - no effect, use the edit server role positions route pub rank: Option, + /// Role icon + /// + /// Provide an Autumn attachment Id. + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))] + pub icon: Option, /// Fields to remove from role object #[cfg_attr(feature = "serde", serde(default))] pub remove: Vec, diff --git a/crates/delta/src/routes/servers/roles_edit.rs b/crates/delta/src/routes/servers/roles_edit.rs index 0a122c1f..e46932ef 100644 --- a/crates/delta/src/routes/servers/roles_edit.rs +++ b/crates/delta/src/routes/servers/roles_edit.rs @@ -1,7 +1,7 @@ use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, - Database, PartialRole, User + Database, File, PartialRole, User, }; use revolt_models::v0; use revolt_permissions::{calculate_server_permissions, ChannelPermission}; @@ -47,14 +47,27 @@ pub async fn edit( name, colour, hoist, + icon, remove, .. } = data; + if remove.contains(&v0::FieldsRole::Icon) { + if let Some(existing_icon) = &role.icon { + db.mark_attachment_as_deleted(&existing_icon.id).await?; + } + } + + let mut final_icon = None; + if let Some(icon_id) = icon { + final_icon = Some(File::use_role_icon(db, &icon_id, &role_id, &user.id).await?); + } + let partial = PartialRole { name, colour, hoist, + icon: final_icon, ..Default::default() }; @@ -69,8 +82,9 @@ pub async fn edit( for channel_id in &server.channels { let channel = Reference::from_unchecked(channel_id).as_channel(db).await?; - sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?; - }; + sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)) + .await?; + } Ok(Json(role.into())) } else { From 5378cd22b4c7d85f44c31a6af0dda00941b80d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Tue, 5 May 2026 21:57:42 +0300 Subject: [PATCH 02/39] fix: use correct response for NoEffect errors (#732) Signed-off-by: ispik --- crates/core/result/src/axum.rs | 6 ++---- crates/core/result/src/rocket.rs | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/core/result/src/axum.rs b/crates/core/result/src/axum.rs index 23b09f7b..f465854b 100644 --- a/crates/core/result/src/axum.rs +++ b/crates/core/result/src/axum.rs @@ -36,9 +36,7 @@ impl IntoResponse for Error { ErrorType::NotInGroup => StatusCode::NOT_FOUND, ErrorType::AlreadyPinned => StatusCode::BAD_REQUEST, ErrorType::NotPinned => StatusCode::BAD_REQUEST, - ErrorType::InSlowmode { - retry_after: _, - } => StatusCode::TOO_MANY_REQUESTS, + ErrorType::InSlowmode { retry_after: _ } => StatusCode::TOO_MANY_REQUESTS, ErrorType::CantCreateServers => StatusCode::FORBIDDEN, ErrorType::UnknownServer => StatusCode::NOT_FOUND, @@ -78,7 +76,7 @@ impl IntoResponse for Error { ErrorType::DuplicateNonce => StatusCode::CONFLICT, ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST, ErrorType::NotFound => StatusCode::NOT_FOUND, - ErrorType::NoEffect => StatusCode::OK, + ErrorType::NoEffect => StatusCode::BAD_REQUEST, ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST, ErrorType::LiveKitUnavailable => StatusCode::BAD_REQUEST, ErrorType::NotConnected => StatusCode::BAD_REQUEST, diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index 5d2904d0..649375e2 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -42,9 +42,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::NotInGroup => Status::NotFound, ErrorType::AlreadyPinned => Status::BadRequest, ErrorType::NotPinned => Status::BadRequest, - ErrorType::InSlowmode { - retry_after: _, - } => Status::TooManyRequests, + ErrorType::InSlowmode { retry_after: _ } => Status::TooManyRequests, ErrorType::InvalidFlagValue => Status::BadRequest, ErrorType::CantCreateServers => Status::Forbidden, @@ -84,7 +82,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::NotAuthenticated => Status::Unauthorized, ErrorType::DuplicateNonce => Status::Conflict, ErrorType::NotFound => Status::NotFound, - ErrorType::NoEffect => Status::Ok, + ErrorType::NoEffect => Status::BadRequest, ErrorType::FailedValidation { .. } => Status::BadRequest, ErrorType::LiveKitUnavailable => Status::BadRequest, ErrorType::NotAVoiceChannel => Status::BadRequest, From 6b41db984bb491b2e58324309cc70d8c14e0b814 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 6 May 2026 16:25:31 -0700 Subject: [PATCH 03/39] feat: blacklist private ip ranges and add january domain blocklist (#731) Signed-off-by: IAmTomahawkx --- Cargo.lock | 221 ++++++++++++++++++- crates/core/config/Revolt.toml | 2 + crates/core/config/src/lib.rs | 6 + crates/services/january/Cargo.toml | 2 + crates/services/january/src/requests.rs | 84 ++++++- crates/services/january/src/website_embed.rs | 3 +- 6 files changed, 299 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce648f79..3f452c4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,7 +288,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -328,7 +328,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -354,7 +354,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -3450,6 +3450,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "hostname" version = "0.3.1" @@ -4576,6 +4585,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -5466,6 +5481,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -5682,6 +5703,106 @@ dependencies = [ "digest", ] +[[package]] +name = "pdk-classy" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3e632c61a7f8ad1a77c4f52d9a85a89c577881f44e89b33e28163af38e515" +dependencies = [ + "bincode", + "futures", + "getrandom 0.2.17", + "http 0.2.12", + "log", + "pdk-proxy-wasm-stub", + "protobuf", + "proxy-wasm", + "serde", + "serde_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "pdk-core" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f2b1d1c8876b54d03d35a18fba1e5172ed8d7f1c379001c11b62c909fdf654" +dependencies = [ + "anyhow", + "log", + "pdk-classy", + "pdk-macros", + "pdk-script", + "protobuf", + "protobuf-codegen", + "rmp-serde", + "serde", + "serde_derive", + "serde_json", + "sha2", + "url", +] + +[[package]] +name = "pdk-ip-filter-lib" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab00d1dfe7b232fcb5424c3af23721993b439a51960f1f3152760228f492ec" +dependencies = [ + "anyhow", + "ipnet", + "pdk-core", + "thiserror 1.0.69", +] + +[[package]] +name = "pdk-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da346bb3e02aad6b6bf31e6e38798c0f3cdf8bd0319fe97d44addc5a0ea5de92" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 1.0.109", +] + +[[package]] +name = "pdk-pel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c60996708c43b91581ea9e6fa324af7eae5a9b8146dd96cacf2868e9260b654" +dependencies = [ + "base64 0.22.1", + "getrandom 0.2.17", + "serde_json", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "pdk-proxy-wasm-stub" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469571b12631b71dce33890917bec59f58c53f9d3b05b748d5aa873c950e1702" + +[[package]] +name = "pdk-script" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8842ea2908eb9b94ed7daa522cc75fcd5fe3738a20d7ecde67cb7829dbf129e" +dependencies = [ + "log", + "num-traits", + "oorandom", + "pdk-classy", + "pdk-pel", + "roxmltree", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", +] + [[package]] name = "pear" version = "0.2.9" @@ -6033,7 +6154,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.5.2", "pin-project-lite 0.2.17", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -6278,6 +6399,67 @@ dependencies = [ "prost", ] +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap 2.13.1", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "proxy-wasm" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d35d9e2bc5104e2e954b149aa1d5f9fa3bb27f73b45b2706020fed101db685" +dependencies = [ + "hashbrown 0.16.1", + "log", +] + [[package]] name = "pxfm" version = "0.1.28" @@ -7055,6 +7237,7 @@ dependencies = [ "lazy_static", "mime", "moka", + "pdk-ip-filter-lib", "regex", "reqwest 0.13.2", "revolt-config", @@ -7068,6 +7251,7 @@ dependencies = [ "tokio 1.51.0", "tracing", "tracing-subscriber", + "url", "utoipa", "utoipa-scalar", ] @@ -7608,6 +7792,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -7617,7 +7814,7 @@ dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -8781,7 +8978,7 @@ dependencies = [ "fastrand 2.4.0", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -9979,6 +10176,18 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[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.1" diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index 30a4622e..aee3a841 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -132,6 +132,8 @@ pkcs8 = "" key_id = "" team_id = "" +[january] +blocked_domains = [] [files] # Encryption key for stored files diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index a49b1cdf..b0cfa455 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -302,6 +302,11 @@ impl Pushd { } } +#[derive(Deserialize, Debug, Clone)] +pub struct January { + pub blocked_domains: Vec, +} + #[derive(Deserialize, Debug, Clone)] pub struct FilesLimit { pub min_file_size: usize, @@ -421,6 +426,7 @@ pub struct Settings { pub hosts: Hosts, pub api: Api, pub pushd: Pushd, + pub january: January, pub files: Files, pub features: Features, pub sentry: Sentry, diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index 78ec07ad..7059cca0 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -27,6 +27,8 @@ tokio = { workspace = true, features = [] } # Web requests reqwest = { workspace = true, features = ["json"] } +pdk-ip-filter-lib = "1.8.0" +url = { workspace = true } # Logging tracing = { workspace = true } diff --git a/crates/services/january/src/requests.rs b/crates/services/january/src/requests.rs index cf681171..b2b75430 100644 --- a/crates/services/january/src/requests.rs +++ b/crates/services/january/src/requests.rs @@ -1,12 +1,13 @@ use encoding_rs::{Encoding, UTF_8_INIT}; use lazy_static::lazy_static; use mime::Mime; +use pdk_ip_filter_lib::IpFilter; use regex::Regex; use reqwest::{ header::{self, CONTENT_TYPE}, redirect, Client, Response, }; -use revolt_config::report_internal_error; +use revolt_config::{config, report_internal_error}; use revolt_files::{create_thumbnail, decode_image, image_size_vec, is_valid_image, video_size}; use revolt_models::v0::{Embed, Image, ImageSize, Video}; use revolt_result::{create_error, Error, Result}; @@ -14,6 +15,7 @@ use std::{ io::{Cursor, Write}, time::Duration, }; +use url::{Host, Url}; lazy_static! { /// Request client @@ -23,7 +25,7 @@ lazy_static! { .redirect(redirect::Policy::custom(|attempt| { if attempt.previous().len() > 5 { // TODO config attempt.error("too many redirects") - } else if attempt.url().host_str() == Some("jan.revolt.chat") { // TODO config + } else if attempt.url().host_str() == Some("proxy.stoatusercontent.com") { // TODO config attempt.stop() } else { attempt.follow() @@ -33,10 +35,10 @@ lazy_static! { .expect("reqwest Client"); /// Spoof User Agent as Discord - static ref RE_USER_AGENT_SPOOFING_AS_DISCORD: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:vx|fx)?twitter|(?:fixv|fixup)?x|(?:old\\.|new\\.|www\\.)reddit).com").expect("valid regex"); + static ref RE_USER_AGENT_SPOOFING_AS_DISCORD: Regex = Regex::new("^(?:(?:vx|fx)?twitter|(?:fixv|fixup)?x|(?:old\\.|new\\.|www\\.)reddit).com").expect("valid regex"); /// Regex for matching new Reddit URLs - static ref RE_URL_NEW_REDDIT: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:new\\.|www\\.)?reddit).com").expect("valid regex"); + static ref RE_URL_NEW_REDDIT: Regex = Regex::new("^(?:(?:new\\.|www\\.)?reddit).com").expect("valid regex"); /// Cache for proxy results static ref PROXY_CACHE: moka::future::Cache)>> = moka::future::Cache::builder() @@ -59,6 +61,28 @@ lazy_static! { .max_capacity(10_000) // Cache up to 10k embeds .time_to_live(Duration::from_secs(60)) // For up to 1 minute .build(); + + static ref IP_BLOCKLIST: IpFilter = IpFilter::block(&[ + "10.0.0.0/8", // something something modern problem require modern solutions + "192.168.0.0/16", + "172.16.0.0/16", + "172.17.0.0/16", + "172.18.0.0/16", + "172.19.0.0/16", + "172.20.0.0/16", + "172.21.0.0/16", + "172.22.0.0/16", + "172.23.0.0/16", + "172.24.0.0/16", + "172.25.0.0/16", + "172.26.0.0/16", + "172.27.0.0/16", + "172.28.0.0/16", + "172.29.0.0/16", + "172.30.0.0/16", + "172.31.0.0/16", + "172.32.0.0/16"] + ).unwrap(); } /// Information about a successful request @@ -73,7 +97,7 @@ impl Request { if let Some(hit) = PROXY_CACHE.get(url).await { hit } else { - let Request { response, mime } = Request::new(url).await?; + let Request { response, mime } = Request::new_from_str(url).await?; if matches!(mime.type_(), mime::IMAGE | mime::VIDEO) { let bytes = report_internal_error!(response.bytes().await); @@ -135,7 +159,7 @@ impl Request { let request = if let Some(request) = request { request } else { - let request = Request::new(url).await?; + let request = Request::new_from_str(url).await?; if matches!(request.mime.type_(), mime::IMAGE) { request } else { @@ -173,7 +197,7 @@ impl Request { let response = if let Some(Request { response, .. }) = request { response } else { - let Request { response, mime } = Request::new(url).await?; + let Request { response, mime } = Request::new_from_str(url).await?; if matches!(mime.type_(), mime::VIDEO) { response } else { @@ -212,7 +236,7 @@ impl Request { if let Some(hit) = EMBED_CACHE.get(&url).await { Ok(hit) } else { - let request = Request::new(&url).await?; + let request = Request::new_from_str(&url).await?; let embed = match (request.mime.type_(), request.mime.subtype()) { (_, mime::HTML) => { let content_type = request @@ -255,15 +279,19 @@ impl Request { } /// Send a new request to a service - pub async fn new(url: &str) -> Result { + pub async fn new(url: Url) -> Result { + let url_host_str = url.host_str().ok_or(create_error!(ProxyError))?.to_string(); + + Request::url_is_blacklisted(&url).await?; + let response = CLIENT .get(url) .header( "User-Agent", - if RE_USER_AGENT_SPOOFING_AS_DISCORD.is_match(url) { + if RE_USER_AGENT_SPOOFING_AS_DISCORD.is_match(&url_host_str) { "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)" } else { - "Mozilla/5.0 (compatible; January/2.0; +https://github.com/revoltchat/backend)" + "Mozilla/5.0 (compatible; January/2.0; +https://github.com/stoatchat/stoatchat)" }, ) .header("Accept-Language", "en-US,en;q=0.5") @@ -290,12 +318,44 @@ impl Request { Ok(Request { response, mime }) } + pub async fn new_from_str(url: &str) -> Result { + let proper_url = Url::parse(url).map_err(|_| create_error!(ProxyError))?; + Request::new(proper_url).await + } + /// Check if something exists - pub async fn exists(url: &str) -> bool { + pub async fn exists(url: Url) -> bool { if let Ok(response) = CLIENT.head(url).send().await { response.status().is_success() } else { false } } + + pub async fn exists_from_str(url: &str) -> Result { + let proper_url = Url::parse(url).map_err(|_| create_error!(ProxyError))?; + Ok(Request::exists(proper_url).await) + } + + pub async fn url_is_blacklisted(url: &Url) -> Result<()> { + if let Some(host) = url.host() { + match host { + Host::Ipv4(ipv4) => { + let url_str = ipv4.to_string(); + if !IP_BLOCKLIST.is_allowed(&url_str) { + return Err(create_error!(InvalidOperation)); + } + } + Host::Domain(domain) => { + let config = config().await; + if config.january.blocked_domains.iter().any(|x| x == domain) { + return Err(create_error!(InvalidOperation)); + } + } + _ => (), + } + }; + + Ok(()) + } } diff --git a/crates/services/january/src/website_embed.rs b/crates/services/january/src/website_embed.rs index 25de1537..6ab1af58 100644 --- a/crates/services/january/src/website_embed.rs +++ b/crates/services/january/src/website_embed.rs @@ -236,11 +236,12 @@ pub async fn populate_special(original_url: String, metadata: &mut WebsiteMetada metadata.site_name.take(); // Verify the video exists - if !crate::requests::Request::exists(&format!( + if !crate::requests::Request::exists_from_str(&format!( "http://img.youtube.com/vi/{}/sddefault.jpg", id )) .await + .unwrap_or(false) { return; } From 21d82018cf84ab0fdd10613d254b9562aea8eea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Thu, 7 May 2026 02:35:20 +0300 Subject: [PATCH 04/39] feat: add legal links to root payload (#733) Signed-off-by: ispik --- crates/core/config/Revolt.toml | 6 ++++++ crates/core/config/src/lib.rs | 11 +++++++++++ crates/delta/src/routes/root.rs | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index aee3a841..2440bb31 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -317,6 +317,12 @@ emojis = 500_000 # default: 5 process_message_delay_limit = 5 +[features.legal_links] +# URLs for legal documents +terms_of_service = "" +privacy_policy = "" +guidelines = "" + [sentry] # Configuration for Sentry error reporting api = "" diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index b0cfa455..3dc2771a 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -382,6 +382,16 @@ pub struct FeaturesLimitsCollection { pub roles: HashMap, } +#[derive(Deserialize, Debug, Clone)] +pub struct LegalLinks { + /// Terms of Service URL + pub terms_of_service: String, + /// Privacy Policy URL + pub privacy_policy: String, + /// Guidelines URL + pub guidelines: String, +} + #[derive(Deserialize, Debug, Clone)] pub struct FeaturesAdvanced { #[serde(default)] @@ -399,6 +409,7 @@ impl Default for FeaturesAdvanced { #[derive(Deserialize, Debug, Clone)] pub struct Features { pub limits: FeaturesLimitsCollection, + pub legal_links: LegalLinks, pub webhooks_enabled: bool, pub mass_mentions_send_notifications: bool, pub mass_mentions_enabled: bool, diff --git a/crates/delta/src/routes/root.rs b/crates/delta/src/routes/root.rs index 14ead75c..c0fb4fae 100644 --- a/crates/delta/src/routes/root.rs +++ b/crates/delta/src/routes/root.rs @@ -57,6 +57,8 @@ pub struct RevoltFeatures { pub livekit: VoiceFeature, /// Limits pub limits: LimitsConfig, + /// Legal links + pub legal_links: LegalLinks, } /// # Limits For Users @@ -70,6 +72,17 @@ pub struct LimitsConfig { pub default: UserLimits, } +/// # Legal links +#[derive(Serialize, JsonSchema, Debug)] +pub struct LegalLinks { + /// Terms of Service URL + pub terms_of_service: String, + /// Privacy Policy URL + pub privacy_policy: String, + /// Guidelines URL + pub guidelines: String, +} + /// # Global limits #[derive(Serialize, JsonSchema, Debug)] pub struct GlobalLimits { @@ -238,6 +251,11 @@ pub async fn root() -> Result> { new_user: UserLimits::from_feature_limits(config.features.limits.new_user), default: UserLimits::from_feature_limits(config.features.limits.default), }, + legal_links: LegalLinks { + terms_of_service: config.features.legal_links.terms_of_service, + privacy_policy: config.features.legal_links.privacy_policy, + guidelines: config.features.legal_links.guidelines, + }, }, ws: config.hosts.events, app: config.hosts.app, From 356491e934b274f9e895df883dd63ef0b3123510 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 6 May 2026 21:33:52 -0700 Subject: [PATCH 05/39] fix: january ip redirects & domain resolver (#738) * fix: properly block private ip ranges I'm a dunce and forgot that domains do in fact resolve to ips. * fix: reimplement max redirects * fix: remove my debug error * fix: actually check redirect urls kind of the whole point of this thing. --- crates/services/january/src/requests.rs | 123 +++++++++++++++--------- 1 file changed, 75 insertions(+), 48 deletions(-) diff --git a/crates/services/january/src/requests.rs b/crates/services/january/src/requests.rs index b2b75430..bec5bc1d 100644 --- a/crates/services/january/src/requests.rs +++ b/crates/services/january/src/requests.rs @@ -10,9 +10,10 @@ use reqwest::{ use revolt_config::{config, report_internal_error}; use revolt_files::{create_thumbnail, decode_image, image_size_vec, is_valid_image, video_size}; use revolt_models::v0::{Embed, Image, ImageSize, Video}; -use revolt_result::{create_error, Error, Result}; +use revolt_result::{create_error, Error, Result, ToRevoltError}; use std::{ io::{Cursor, Write}, + str::FromStr, time::Duration, }; use url::{Host, Url}; @@ -22,15 +23,7 @@ lazy_static! { static ref CLIENT: Client = reqwest::Client::builder() .timeout(Duration::from_secs(10)) // TODO config .connect_timeout(Duration::from_secs(5)) // TODO config - .redirect(redirect::Policy::custom(|attempt| { - if attempt.previous().len() > 5 { // TODO config - attempt.error("too many redirects") - } else if attempt.url().host_str() == Some("proxy.stoatusercontent.com") { // TODO config - attempt.stop() - } else { - attempt.follow() - } - })) + .redirect(redirect::Policy::none()) .build() .expect("reqwest Client"); @@ -63,25 +56,14 @@ lazy_static! { .build(); static ref IP_BLOCKLIST: IpFilter = IpFilter::block(&[ - "10.0.0.0/8", // something something modern problem require modern solutions + "10.0.0.0/8", "192.168.0.0/16", - "172.16.0.0/16", - "172.17.0.0/16", - "172.18.0.0/16", - "172.19.0.0/16", - "172.20.0.0/16", - "172.21.0.0/16", - "172.22.0.0/16", - "172.23.0.0/16", - "172.24.0.0/16", - "172.25.0.0/16", - "172.26.0.0/16", - "172.27.0.0/16", - "172.28.0.0/16", - "172.29.0.0/16", - "172.30.0.0/16", - "172.31.0.0/16", - "172.32.0.0/16"] + "127.0.0.0/8", + "172.16.0.0/12", + "169.254.0.0/16", + "::1", + "fc00::/7" + ] ).unwrap(); } @@ -280,11 +262,14 @@ impl Request { /// Send a new request to a service pub async fn new(url: Url) -> Result { + let mut url = url; let url_host_str = url.host_str().ok_or(create_error!(ProxyError))?.to_string(); Request::url_is_blacklisted(&url).await?; + let mut redirect_count = 0; - let response = CLIENT + loop { + let response = CLIENT .get(url) .header( "User-Agent", @@ -299,23 +284,42 @@ impl Request { .await .map_err(|_| create_error!(ProxyError))?; - if !response.status().is_success() { - tracing::error!("{:?}", response); - return Err(create_error!(ProxyError)); + if response.status().is_redirection() { + redirect_count += 1; + + if redirect_count > 5 { + return Err(create_error!(ProxyError)); + } + if let Some(location) = response.headers().get("location") { + let location = location.to_str().map_err(|_| create_error!(ProxyError))?; + url = Url::from_str(location).to_internal_error()?; + + if !Request::url_is_blacklisted(&url).await? { + continue; + } + } else { + return Err(create_error!(ProxyError)); + } + } + + if !response.status().is_success() { + tracing::error!("{:?}", response); + return Err(create_error!(ProxyError)); + } + + let content_type = response + .headers() + .get(CONTENT_TYPE) + .ok_or(create_error!(ProxyError))? + .to_str() + .map_err(|_| create_error!(ProxyError))?; + + let mime: mime::Mime = content_type + .parse() + .map_err(|_| create_error!(ProxyError))?; + + return Ok(Request { response, mime }); } - - let content_type = response - .headers() - .get(CONTENT_TYPE) - .ok_or(create_error!(ProxyError))? - .to_str() - .map_err(|_| create_error!(ProxyError))?; - - let mime: mime::Mime = content_type - .parse() - .map_err(|_| create_error!(ProxyError))?; - - Ok(Request { response, mime }) } pub async fn new_from_str(url: &str) -> Result { @@ -337,7 +341,7 @@ impl Request { Ok(Request::exists(proper_url).await) } - pub async fn url_is_blacklisted(url: &Url) -> Result<()> { + pub async fn url_is_blacklisted(url: &Url) -> Result { if let Some(host) = url.host() { match host { Host::Ipv4(ipv4) => { @@ -347,15 +351,38 @@ impl Request { } } Host::Domain(domain) => { + let mut domain = domain.to_string(); + let config = config().await; - if config.january.blocked_domains.iter().any(|x| x == domain) { + + // First step: TLDs and blocked domains + if !domain.contains(".") // lazily block TLDs + || config.january.blocked_domains.iter().any(|x| x == &domain) + { return Err(create_error!(InvalidOperation)); } + + if !domain.contains(":") { + domain += ":80"; + } + + // Second step: resolve the IP and check the blocklist + if let Ok(mut resolved_ip) = tokio::net::lookup_host(domain.clone()).await { + if let Some(resolved_ip) = resolved_ip.next() { + if !IP_BLOCKLIST.is_allowed(&resolved_ip.ip().to_string()) { + return Err(create_error!(InvalidOperation)); + } + } else { + return Err(create_error!(InvalidOperation)); + } + } else { + return Err(create_error!(ProxyError)); + } } _ => (), } }; - Ok(()) + Ok(false) } } From df276ac40b60cf94bc58468690ffeb31be674312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Sat, 9 May 2026 00:37:35 +0300 Subject: [PATCH 06/39] chore: update emoji list (#740) Signed-off-by: ispik --- .../src/models/emojis/unicode_emoji.txt | 5180 ++++++++++++----- 1 file changed, 3637 insertions(+), 1543 deletions(-) diff --git a/crates/core/database/src/models/emojis/unicode_emoji.txt b/crates/core/database/src/models/emojis/unicode_emoji.txt index bc8036e3..06e8bf74 100644 --- a/crates/core/database/src/models/emojis/unicode_emoji.txt +++ b/crates/core/database/src/models/emojis/unicode_emoji.txt @@ -1,1421 +1,84 @@ -💯 -🔢 -😀 -😃 -😄 -😁 -😆 -😆 -😅 -🀣 -😂 -🙂 -🙃 -😉 -😊 -😇 -🥰 -😍 -🀩 -😘 -😗 -☺ -😚 -😙 -🥲 -😋 -😛 -😜 -🀪 -😝 -🀑 -🀗 -🀭 -🀫 -🀔 -🀐 -🀚 -😐 -😑 -😶 -😏 -😒 -🙄 -😬 -🀥 -😌 -😔 -😪 -🀀 -😎 -😷 -🀒 -🀕 -🀢 -🀮 -🀧 -🥵 -🥶 -🥎 -😵 -🀯 -🀠 -🥳 -🥞 -😎 -🀓 -🧐 -😕 -😟 -🙁 -☹ -😮 -😯 -😲 -😳 -🥺 -😊 -😧 -😚 -😰 -😥 -😢 -😭 -😱 -😖 -😣 -😞 -😓 -😩 -😫 -🥱 -😀 -😡 -😡 -😠 -🀬 -😈 -👿 -💀 -☠ -💩 -💩 -💩 -🀡 -👹 -👺 -👻 -👜 -👟 -🀖 -😺 -😞 -😹 -😻 -😌 -😜 -🙀 -😿 -😟 -🙈 -🙉 -🙊 -💋 -💌 -💘 -💝 -💖 -💗 -💓 -💞 -💕 -💟 -❣ -💔 -❀ -🧡 -💛 -💚 -💙 -💜 -🀎 -🖀 -🀍 -💢 -💥 -💥 -💫 -💊 -💚 -🕳 -💣 -💬 -👁‍🗚 -🗚 -🗯 -💭 -💀 -👋 -🀚 -🖐 -✋ -✋ -🖖 -👌 -🀌 -🀏 -✌ -🀞 -🀟 -🀘 -🀙 -👈 -👉 -👆 -🖕 -🖕 -👇 -☝ -👍 -👍 -👎 -👎 -✊ -✊ -👊 -👊 -👊 -🀛 -🀜 -👏 -🙌 -👐 -🀲 -🀝 -🙏 -✍ -💅 -🀳 -💪 -🊟 -🊿 -🊵 -🊶 -👂 -🊻 -👃 -🧠 -🫀 -🫁 -🊷 -🊎 -👀 -👁 -👅 -👄 -👶 -🧒 -👊 -👧 -🧑 -👱 -👚 -🧔 -👚‍🊰 -👚‍🊱 -👚‍🊳 -👚‍🊲 -👩 -👩‍🊰 -🧑‍🊰 -👩‍🊱 -🧑‍🊱 -👩‍🊳 -🧑‍🊳 -👩‍🊲 -🧑‍🊲 -👱‍♀ -👱‍♀ -👱‍♂ -🧓 -👎 -👵 -🙍 -🙍‍♂ -🙍‍♀ -🙎 -🙎‍♂ -🙎‍♀ -🙅 -🙅‍♂ -🙅‍♂ -🙅‍♀ -🙅‍♀ -🙆 -🙆‍♂ -🙆‍♀ -💁 -💁 -💁‍♂ -💁‍♂ -💁‍♀ -💁‍♀ -🙋 -🙋‍♂ -🙋‍♀ -🧏 -🧏‍♂ -🧏‍♀ -🙇 -🙇‍♂ -🙇‍♀ -🀊 -🀊‍♂ -🀊‍♀ -🀷 -🀷‍♂ -🀷‍♀ -🧑‍⚕ -👚‍⚕ -👩‍⚕ -🧑‍🎓 -👚‍🎓 -👩‍🎓 -🧑‍🏫 -👚‍🏫 -👩‍🏫 -🧑‍⚖ -👚‍⚖ -👩‍⚖ -🧑‍🌟 -👚‍🌟 -👩‍🌟 -🧑‍🍳 -👚‍🍳 -👩‍🍳 -🧑‍🔧 -👚‍🔧 -👩‍🔧 -🧑‍🏭 -👚‍🏭 -👩‍🏭 -🧑‍💌 -👚‍💌 -👩‍💌 -🧑‍🔬 -👚‍🔬 -👩‍🔬 -🧑‍💻 -👚‍💻 -👩‍💻 -🧑‍🎀 -👚‍🎀 -👩‍🎀 -🧑‍🎚 -👚‍🎚 -👩‍🎚 -🧑‍✈ -👚‍✈ -👩‍✈ -🧑‍🚀 -👚‍🚀 -👩‍🚀 -🧑‍🚒 -👚‍🚒 -👩‍🚒 -👮 -👮 -👮‍♂ -👮‍♀ -🕵 -🕵‍♂ -🕵‍♀ -💂 -💂‍♂ -💂‍♀ -🥷 -👷 -👷‍♂ -👷‍♀ -🀎 -👞 -👳 -👳‍♂ -👳‍♀ -👲 -🧕 -🀵 -🀵‍♂ -🀵‍♀ -👰 -👰‍♂ -👰‍♀ -👰‍♀ -🀰 -🀱 -👩‍🍌 -👚‍🍌 -🧑‍🍌 -👌 -🎅 -🀶 -🧑‍🎄 -🊞 -🊞‍♂ -🊞‍♀ -🊹 -🊹‍♂ -🊹‍♀ -🧙 -🧙‍♂ -🧙‍♀ -🧚 -🧚‍♂ -🧚‍♀ -🧛 -🧛‍♂ -🧛‍♀ -🧜 -🧜‍♂ -🧜‍♀ -🧝 -🧝‍♂ -🧝‍♀ -🧞 -🧞‍♂ -🧞‍♀ -🧟 -🧟‍♂ -🧟‍♀ -💆 -💆‍♂ -💆‍♀ -💇 -💇‍♂ -💇‍♀ -🚶 -🚶‍♂ -🚶‍♀ -🧍 -🧍‍♂ -🧍‍♀ -🧎 -🧎‍♂ -🧎‍♀ -🧑‍🊯 -👚‍🊯 -👩‍🊯 -🧑‍🊌 -👚‍🊌 -👩‍🊌 -🧑‍🊜 -👚‍🊜 -👩‍🊜 -🏃 -🏃 -🏃‍♂ -🏃‍♀ -💃 -💃 -🕺 -🕎 -👯 -👯‍♂ -👯‍♀ -🧖 -🧖‍♂ -🧖‍♀ -🧗 -🧗‍♂ -🧗‍♀ -🀺 -🏇 -⛷ -🏂 -🏌 -🏌‍♂ -🏌‍♀ -🏄 -🏄‍♂ -🏄‍♀ -🚣 -🚣‍♂ -🚣‍♀ -🏊 -🏊‍♂ -🏊‍♀ -⛹ -⛹‍♂ -⛹‍♂ -⛹‍♀ -⛹‍♀ -🏋 -🏋‍♂ -🏋‍♀ -🚎 -🚎‍♂ -🚎‍♀ -🚵 -🚵‍♂ -🚵‍♀ -🀞 -🀞‍♂ -🀞‍♀ -🀌 -🀌‍♂ -🀌‍♀ -🀜 -🀜‍♂ -🀜‍♀ -🀟 -🀟‍♂ -🀟‍♀ -🀹 -🀹‍♂ -🀹‍♀ -🧘 -🧘‍♂ -🧘‍♀ -🛀 -🛌 -🧑‍🀝‍🧑 -👭 -👫 -👬 -💏 -👩‍❀‍💋‍👚 -👚‍❀‍💋‍👚 -👩‍❀‍💋‍👩 -💑 -👩‍❀‍👚 -👚‍❀‍👚 -👩‍❀‍👩 -👪 -👚‍👩‍👊 -👚‍👩‍👧 -👚‍👩‍👧‍👊 -👚‍👩‍👊‍👊 -👚‍👩‍👧‍👧 -👚‍👚‍👊 -👚‍👚‍👧 -👚‍👚‍👧‍👊 -👚‍👚‍👊‍👊 -👚‍👚‍👧‍👧 -👩‍👩‍👊 -👩‍👩‍👧 -👩‍👩‍👧‍👊 -👩‍👩‍👊‍👊 -👩‍👩‍👧‍👧 -👚‍👊 -👚‍👊‍👊 -👚‍👧 -👚‍👧‍👊 -👚‍👧‍👧 -👩‍👊 -👩‍👊‍👊 -👩‍👧 -👩‍👧‍👊 -👩‍👧‍👧 -🗣 -👀 -👥 -🫂 -👣 -🐵 -🐒 -🊍 -🊧 -🐶 -🐕 -🊮 -🐕‍🊺 -🐩 -🐺 -🊊 -🊝 -🐱 -🐈 -🐈‍⬛ -🊁 -🐯 -🐅 -🐆 -🐎 -🐎 -🊄 -🊓 -🊌 -🊬 -🐮 -🐂 -🐃 -🐄 -🐷 -🐖 -🐗 -🐜 -🐏 -🐑 -🐐 -🐪 -🐫 -🊙 -🊒 -🐘 -🊣 -🊏 -🊛 -🐭 -🐁 -🐀 -🐹 -🐰 -🐇 -🐿 -🊫 -🊔 -🊇 -🐻 -🐻‍❄ -🐚 -🐌 -🊥 -🊊 -🊚 -🊘 -🊡 -🐟 -🐟 -🊃 -🐔 -🐓 -🐣 -🐀 -🐥 -🐊 -🐧 -🕊 -🊅 -🊆 -🊢 -🊉 -🊀 -🪶 -🊩 -🊚 -🊜 -🐞 -🐊 -🐢 -🊎 -🐍 -🐲 -🐉 -🊕 -🊖 -🐳 -🐋 -🐬 -🐬 -🊭 -🐟 -🐠 -🐡 -🊈 -🐙 -🐚 -🐌 -🊋 -🐛 -🐜 -🐝 -🐝 -🪲 -🐞 -🊗 -🪳 -🕷 -🕞 -🊂 -🊟 -🪰 -🪱 -🊠 -💐 -🌞 -💮 -🏵 -🌹 -🥀 -🌺 -🌻 -🌌 -🌷 -🌱 -🪎 -🌲 -🌳 -🌎 -🌵 -🌟 -🌿 -☘ -🍀 -🍁 -🍂 -🍃 -🍇 -🍈 -🍉 -🍊 -🍊 -🍊 -🍋 -🍌 -🍌 -🍍 -🥭 -🍎 -🍏 -🍐 -🍑 -🍒 -🍓 -🫐 -🥝 -🍅 -🫒 -🥥 -🥑 -🍆 -🥔 -🥕 -🌜 -🌶 -🫑 -🥒 -🥬 -🥊 -🧄 -🧅 -🍄 -🥜 -🌰 -🍞 -🥐 -🥖 -🫓 -🥚 -🥯 -🥞 -🧇 -🧀 -🍖 -🍗 -🥩 -🥓 -🍔 -🍟 -🍕 -🌭 -🥪 -🌮 -🌯 -🫔 -🥙 -🧆 -🥚 -🍳 -🥘 -🍲 -🫕 -🥣 -🥗 -🍿 -🧈 -🧂 -🥫 -🍱 -🍘 -🍙 -🍚 -🍛 -🍜 -🍝 -🍠 -🍢 -🍣 -🍀 -🍥 -🥮 -🍡 -🥟 -🥠 -🥡 -🊀 -🊞 -🊐 -🊑 -🊪 -🍊 -🍧 -🍚 -🍩 -🍪 -🎂 -🍰 -🧁 -🥧 -🍫 -🍬 -🍭 -🍮 -🍯 -🍌 -🥛 -☕ -🫖 -🍵 -🍶 -🍟 -🍷 -🍞 -🍹 -🍺 -🍻 -🥂 -🥃 -🥀 -🧋 -🧃 -🧉 -🧊 -🥢 -🍜 -🍎 -🥄 -🔪 -🔪 -🏺 -🌍 -🌎 -🌏 -🌐 -🗺 -🗟 -🧭 -🏔 -⛰ -🌋 -🗻 -🏕 -🏖 -🏜 -🏝 -🏞 -🏟 -🏛 -🏗 -🧱 -🪚 -🪵 -🛖 -🏘 -🏚 -🏠 -🏡 -🏢 -🏣 -🏀 -🏥 -🏊 -🏚 -🏩 -🏪 -🏫 -🏬 -🏭 -🏯 -🏰 -💒 -🗌 -🗜 -⛪ -🕌 -🛕 -🕍 -⛩ -🕋 -⛲ -⛺ -🌁 -🌃 -🏙 -🌄 -🌅 -🌆 -🌇 -🌉 -♚ -🎠 -🎡 -🎢 -💈 -🎪 -🚂 -🚃 -🚄 -🚅 -🚆 -🚇 -🚈 -🚉 -🚊 -🚝 -🚞 -🚋 -🚌 -🚍 -🚎 -🚎 -🚐 -🚑 -🚒 -🚓 -🚔 -🚕 -🚖 -🚗 -🚗 -🚘 -🚙 -🛻 -🚚 -🚛 -🚜 -🏎 -🏍 -🛵 -🊜 -🊌 -🛺 -🚲 -🛎 -🛹 -🛌 -🚏 -🛣 -🛀 -🛢 -⛜ -🚚 -🚥 -🚊 -🛑 -🚧 -⚓ -⛵ -⛵ -🛶 -🚀 -🛳 -⛎ -🛥 -🚢 -✈ -🛩 -🛫 -🛬 -🪂 -💺 -🚁 -🚟 -🚠 -🚡 -🛰 -🚀 -🛞 -🛎 -🧳 -⌛ -⏳ + +*⃣ +0⃣ +1⃣ +2⃣ +3⃣ +4⃣ +5⃣ +6⃣ +7⃣ +8⃣ +9⃣ +© +® +‌ +⁉ +™ +ℹ +↔ +↕ +↖ +↗ +↘ +↙ +↩ +↪ ⌚ +⌛ +⌚ +⏏ +⏩ +⏪ +⏫ +⏬ +⏭ +⏮ +⏯ ⏰ ⏱ ⏲ -🕰 -🕛 -🕧 -🕐 -🕜 -🕑 -🕝 -🕒 -🕞 -🕓 -🕟 -🕔 -🕠 -🕕 -🕡 -🕖 -🕢 -🕗 -🕣 -🕘 -🕀 -🕙 -🕥 -🕚 -🕊 -🌑 -🌒 -🌓 -🌔 -🌔 -🌕 -🌖 -🌗 -🌘 -🌙 -🌚 -🌛 -🌜 -🌡 +⏳ +⏞ +⏹ +⏺ +Ⓜ +▪ +▫ +▶ +◀ +◻ +◌ +â—œ +â—Ÿ ☀ -🌝 -🌞 -🪐 -⭐ -🌟 -🌠 -🌌 ☁ -⛅ -⛈ -🌀 -🌥 -🌊 -🌧 -🌚 -🌩 -🌪 -🌫 -🌬 -🌀 -🌈 -🌂 ☂ -☔ -⛱ -⚡ -❄ ☃ -⛄ ☄ -🔥 -💧 -🌊 -🎃 -🎄 -🎆 -🎇 -🧚 -✹ -🎈 -🎉 -🎊 -🎋 -🎍 -🎎 -🎏 -🎐 -🎑 -🧧 -🎀 -🎁 -🎗 -🎟 -🎫 -🎖 -🏆 -🏅 -🥇 -🥈 -🥉 -âšœ -⚟ -🥎 -🏀 -🏐 -🏈 -🏉 -🎟 -🥏 -🎳 -🏏 -🏑 -🏒 -🥍 -🏓 -🏞 -🥊 -🥋 -🥅 -⛳ -⛞ -🎣 -🀿 -🎜 -🎿 -🛷 -🥌 -🎯 -🪀 -🪁 -🎱 -🔮 -🪄 -🧿 -🎮 -🕹 -🎰 -🎲 -🧩 -🧞 -🪅 -🪆 -♠ -♥ -♊ -♣ -♟ -🃏 -🀄 -🎎 -🎭 -🖌 -🎚 -🧵 -🪡 -🧶 -🪢 -👓 -🕶 -🥜 -🥌 -🊺 -👔 -👕 -👕 -👖 -🧣 -🧀 -🧥 -🧊 -👗 -👘 -🥻 -🩱 -🩲 -🩳 -👙 -👚 -👛 -👜 -👝 -🛍 -🎒 -🩎 -👞 -👞 -👟 -🥟 -🥿 -👠 -👡 -🩰 -👢 -👑 -👒 -🎩 -🎓 -🧢 -🪖 -⛑ -📿 -💄 -💍 -💎 -🔇 -🔈 -🔉 -🔊 -📢 -📣 -📯 -🔔 -🔕 -🎌 -🎵 -🎶 -🎙 -🎚 -🎛 -🎀 -🎧 -📻 -🎷 -🪗 -🎞 -🎹 -🎺 -🎻 -🪕 -🥁 -🪘 -📱 -📲 ☎ -☎ -📞 -📟 -📠 -🔋 -🔌 -💻 -🖥 -🖚 -⌚ -🖱 -🖲 -💜 -💟 -💿 -📀 -🧮 -🎥 -🎞 -📜 -🎬 -📺 -📷 -📞 -📹 -📌 -🔍 -🔎 -🕯 -💡 -🔊 -🏮 -🏮 -🪔 -📔 -📕 -📖 -📖 -📗 -📘 -📙 -📚 -📓 -📒 -📃 -📜 -📄 -📰 -🗞 -📑 -🔖 -🏷 -💰 -🪙 -💎 -💵 -💶 -💷 -💞 -💳 -🧟 -💹 -✉ -📧 -📧 -📚 -📩 -📀 -📥 -📊 -📫 -📪 -📬 -📭 -📮 -🗳 -✏ -✒ -🖋 -🖊 -🖌 -🖍 -📝 -📝 -💌 -📁 -📂 -🗂 -📅 -📆 -🗒 -🗓 -📇 -📈 -📉 -📊 -📋 -📌 -📍 -📎 -🖇 -📏 -📐 -✂ -🗃 -🗄 -🗑 -🔒 -🔓 -🔏 -🔐 -🔑 -🗝 -🔚 -🪓 -⛏ -⚒ -🛠 -🗡 -⚔ -🔫 -🪃 -🏹 -🛡 -🪚 -🔧 -🪛 -🔩 -⚙ -🗜 -⚖ -🊯 -🔗 -⛓ -🪝 -🧰 -🧲 -🪜 -⚗ -🧪 -🧫 -🧬 -🔬 -🔭 -📡 -💉 -🩞 -💊 -🩹 -🩺 -🚪 -🛗 -🪞 -🪟 -🛏 -🛋 -🪑 -🚜 -🪠 -🚿 -🛁 -🪀 -🪒 -🧎 -🧷 -🧹 -🧺 -🧻 -🪣 -🧌 -🪥 -🧜 -🧯 -🛒 -🚬 -⚰ -🪊 -⚱ -🗿 -🪧 -🏧 -🚮 -🚰 -♿ -🚹 -🚺 -🚻 -🚌 -🚟 -🛂 -🛃 -🛄 -🛅 -⚠ -🚞 -⛔ -🚫 -🚳 -🚭 -🚯 -🚱 -🚷 -📵 -🔞 +☑ +☔ +☕ +☘ +☝ +☝🏻 +☝🏌 +☝🏜 +☝🏟 +☝🏿 +☠ ☢ ☣ -⬆ -↗ -➡ -↘ -⬇ -↙ -⬅ -↖ -↕ -↔ -↩ -↪ -‎ -‵ -🔃 -🔄 -🔙 -🔚 -🔛 -🔜 -🔝 -🛐 -⚛ -🕉 -✡ -☞ -☯ -✝ ☊ ☪ ☮ -🕎 -🔯 +☯ +☞ +☹ +☺ +♀ +♂ ♈ ♉ ♊ @@ -1428,163 +91,157 @@ ♑ ♒ ♓ -⛎ -🔀 -🔁 -🔂 -▶ -⏩ -⏭ -⏯ -◀ -⏪ -⏮ -🔌 -⏫ -🔜 -⏬ -⏞ -⏹ -⏺ -⏏ -🎊 -🔅 -🔆 -📶 -📳 -📎 -♀ -♂ -⚧ -✖ -➕ -➖ -➗ +♟ +♠ +♣ +♥ +♊ +♚ +♻ ♟ -‌ -⁉ +♿ +⚒ +⚓ +⚔ +⚕ +⚖ +⚗ +⚙ +⚛ +⚜ +⚠ +⚡ +⚧ +⚪ +⚫ +⚰ +⚱ +âšœ +⚟ +⛄ +⛅ +⛈ +⛎ +⛏ +⛑ +⛓ +⛓‍💥 +⛔ +⛩ +⛪ +⛰ +⛱ +⛲ +⛳ +⛎ +⛵ +⛷ +⛞ +⛹ +⛹‍♀ +⛹‍♂ +⛹🏻 +⛹🏻‍♀ +⛹🏻‍♂ +⛹🏌 +⛹🏌‍♀ +⛹🏌‍♂ +⛹🏜 +⛹🏜‍♀ +⛹🏜‍♂ +⛹🏟 +⛹🏟‍♀ +⛹🏟‍♂ +⛹🏿 +⛹🏿‍♀ +⛹🏿‍♂ +⛺ +⛜ +✂ +✅ +✈ +✉ +✊ +✊🏻 +✊🏌 +✊🏜 +✊🏟 +✊🏿 +✋ +✋🏻 +✋🏌 +✋🏜 +✋🏟 +✋🏿 +✌ +✌🏻 +✌🏌 +✌🏜 +✌🏟 +✌🏿 +✍ +✍🏻 +✍🏌 +✍🏜 +✍🏟 +✍🏿 +✏ +✒ +✔ +✖ +✝ +✡ +✹ +✳ +✎ +❄ +❇ +❌ +❎ ❓ ❔ ❕ ❗ -❗ -〰 -💱 -💲 -⚕ -♻ -⚜ -🔱 -📛 -🔰 -⭕ -✅ -☑ -✔ -❌ -❎ +❣ +❀ +❀‍🔥 +❀‍🩹 +➕ +➖ +➗ +➡ ➰ ➿ +‎ +‵ +⬅ +⬆ +⬇ +⬛ +⬜ +⭐ +⭕ +〰 〜 -✳ -✎ -❇ -© -® -™ -#⃣ -*⃣ -0⃣ -1⃣ -2⃣ -3⃣ -4⃣ -5⃣ -6⃣ -7⃣ -8⃣ -9⃣ -🔟 -🔠 -🔡 -🔣 -🔀 +㊗ +㊙ +🀄 +🃏 🅰 -🆎 🅱 +🅟 +🅿 +🆎 🆑 🆒 🆓 -ℹ 🆔 -Ⓜ 🆕 🆖 -🅟 🆗 -🅿 🆘 🆙 🆚 -🈁 -🈂 -🈷 -🈶 -🈯 -🉐 -🈹 -🈚 -🈲 -🉑 -🈞 -🈎 -🈳 -㊗ -㊙ -🈺 -🈵 -🔎 -🟠 -🟡 -🟢 -🔵 -🟣 -🟀 -⚫ -⚪ -🟥 -🟧 -🟚 -🟩 -🟊 -🟪 -🟫 -⬛ -⬜ -◌ -◻ -â—Ÿ -â—œ -▪ -▫ -🔶 -🔷 -🔞 -🔹 -🔺 -🔻 -💠 -🔘 -🔳 -🔲 -🏁 -🚩 -🎌 -🏎 -🏳 -🏳‍🌈 -🏳‍⚧ -🏎‍☠ 🇊🇚 🇊🇩 🇊🇪 @@ -1636,6 +293,7 @@ 🇚🇳 🇚🇎 🇚🇵 +🇚🇶 🇚🇷 🇚🇺 🇚🇻 @@ -1659,7 +317,6 @@ 🇪🇞 🇪🇹 🇪🇺 -🇪🇺 🇫🇮 🇫🇯 🇫🇰 @@ -1668,7 +325,6 @@ 🇫🇷 🇬🇊 🇬🇧 -🇬🇧 🇬🇩 🇬🇪 🇬🇫 @@ -1845,6 +501,3444 @@ 🇿🇊 🇿🇲 🇿🇌 +🈁 +🈂 +🈚 +🈯 +🈲 +🈳 +🈎 +🈵 +🈶 +🈷 +🈞 +🈹 +🈺 +🉐 +🉑 +🌀 +🌁 +🌂 +🌃 +🌄 +🌅 +🌆 +🌇 +🌈 +🌉 +🌊 +🌋 +🌌 +🌍 +🌎 +🌏 +🌐 +🌑 +🌒 +🌓 +🌔 +🌕 +🌖 +🌗 +🌘 +🌙 +🌚 +🌛 +🌜 +🌝 +🌞 +🌟 +🌠 +🌡 +🌀 +🌥 +🌊 +🌧 +🌚 +🌩 +🌪 +🌫 +🌬 +🌭 +🌮 +🌯 +🌰 +🌱 +🌲 +🌳 +🌎 +🌵 +🌶 +🌷 +🌞 +🌹 +🌺 +🌻 +🌌 +🌜 +🌟 +🌿 +🍀 +🍁 +🍂 +🍃 +🍄 +🍄‍🟫 +🍅 +🍆 +🍇 +🍈 +🍉 +🍊 +🍋 +🍋‍🟩 +🍌 +🍍 +🍎 +🍏 +🍐 +🍑 +🍒 +🍓 +🍔 +🍕 +🍖 +🍗 +🍘 +🍙 +🍚 +🍛 +🍜 +🍝 +🍞 +🍟 +🍠 +🍡 +🍢 +🍣 +🍀 +🍥 +🍊 +🍧 +🍚 +🍩 +🍪 +🍫 +🍬 +🍭 +🍮 +🍯 +🍰 +🍱 +🍲 +🍳 +🍎 +🍵 +🍶 +🍷 +🍞 +🍹 +🍺 +🍻 +🍌 +🍜 +🍟 +🍿 +🎀 +🎁 +🎂 +🎃 +🎄 +🎅 +🎅🏻 +🎅🏌 +🎅🏜 +🎅🏟 +🎅🏿 +🎆 +🎇 +🎈 +🎉 +🎊 +🎋 +🎌 +🎍 +🎎 +🎏 +🎐 +🎑 +🎒 +🎓 +🎖 +🎗 +🎙 +🎚 +🎛 +🎞 +🎟 +🎠 +🎡 +🎢 +🎣 +🎀 +🎥 +🎊 +🎧 +🎚 +🎩 +🎪 +🎫 +🎬 +🎭 +🎮 +🎯 +🎰 +🎱 +🎲 +🎳 +🎎 +🎵 +🎶 +🎷 +🎞 +🎹 +🎺 +🎻 +🎌 +🎜 +🎟 +🎿 +🏀 +🏁 +🏂 +🏂🏻 +🏂🏌 +🏂🏜 +🏂🏟 +🏂🏿 +🏃 +🏃‍♀ +🏃‍♀‍➡ +🏃‍♂ +🏃‍♂‍➡ +🏃‍➡ +🏃🏻 +🏃🏻‍♀ +🏃🏻‍♀‍➡ +🏃🏻‍♂ +🏃🏻‍♂‍➡ +🏃🏻‍➡ +🏃🏌 +🏃🏌‍♀ +🏃🏌‍♀‍➡ +🏃🏌‍♂ +🏃🏌‍♂‍➡ +🏃🏌‍➡ +🏃🏜 +🏃🏜‍♀ +🏃🏜‍♀‍➡ +🏃🏜‍♂ +🏃🏜‍♂‍➡ +🏃🏜‍➡ +🏃🏟 +🏃🏟‍♀ +🏃🏟‍♀‍➡ +🏃🏟‍♂ +🏃🏟‍♂‍➡ +🏃🏟‍➡ +🏃🏿 +🏃🏿‍♀ +🏃🏿‍♀‍➡ +🏃🏿‍♂ +🏃🏿‍♂‍➡ +🏃🏿‍➡ +🏄 +🏄‍♀ +🏄‍♂ +🏄🏻 +🏄🏻‍♀ +🏄🏻‍♂ +🏄🏌 +🏄🏌‍♀ +🏄🏌‍♂ +🏄🏜 +🏄🏜‍♀ +🏄🏜‍♂ +🏄🏟 +🏄🏟‍♀ +🏄🏟‍♂ +🏄🏿 +🏄🏿‍♀ +🏄🏿‍♂ +🏅 +🏆 +🏇 +🏇🏻 +🏇🏌 +🏇🏜 +🏇🏟 +🏇🏿 +🏈 +🏉 +🏊 +🏊‍♀ +🏊‍♂ +🏊🏻 +🏊🏻‍♀ +🏊🏻‍♂ +🏊🏌 +🏊🏌‍♀ +🏊🏌‍♂ +🏊🏜 +🏊🏜‍♀ +🏊🏜‍♂ +🏊🏟 +🏊🏟‍♀ +🏊🏟‍♂ +🏊🏿 +🏊🏿‍♀ +🏊🏿‍♂ +🏋 +🏋‍♀ +🏋‍♂ +🏋🏻 +🏋🏻‍♀ +🏋🏻‍♂ +🏋🏌 +🏋🏌‍♀ +🏋🏌‍♂ +🏋🏜 +🏋🏜‍♀ +🏋🏜‍♂ +🏋🏟 +🏋🏟‍♀ +🏋🏟‍♂ +🏋🏿 +🏋🏿‍♀ +🏋🏿‍♂ +🏌 +🏌‍♀ +🏌‍♂ +🏌🏻 +🏌🏻‍♀ +🏌🏻‍♂ +🏌🏌 +🏌🏌‍♀ +🏌🏌‍♂ +🏌🏜 +🏌🏜‍♀ +🏌🏜‍♂ +🏌🏟 +🏌🏟‍♀ +🏌🏟‍♂ +🏌🏿 +🏌🏿‍♀ +🏌🏿‍♂ +🏍 +🏎 +🏏 +🏐 +🏑 +🏒 +🏓 +🏔 +🏕 +🏖 +🏗 +🏘 +🏙 +🏚 +🏛 +🏜 +🏝 +🏞 +🏟 +🏠 +🏡 +🏢 +🏣 +🏀 +🏥 +🏊 +🏧 +🏚 +🏩 +🏪 +🏫 +🏬 +🏭 +🏮 +🏯 +🏰 +🏳 +🏳‍⚧ +🏳‍🌈 +🏎 +🏎‍☠ 🏎󠁧󠁢󠁥󠁮󠁧󠁿 🏎󠁧󠁢󠁳󠁣󠁎󠁿 -🏎󠁧󠁢󠁷󠁬󠁳󠁿 \ No newline at end of file +🏎󠁧󠁢󠁷󠁬󠁳󠁿 +🏵 +🏷 +🏞 +🏹 +🏺 +🐀 +🐁 +🐂 +🐃 +🐄 +🐅 +🐆 +🐇 +🐈 +🐈‍⬛ +🐉 +🐊 +🐋 +🐌 +🐍 +🐎 +🐏 +🐐 +🐑 +🐒 +🐓 +🐔 +🐕 +🐕‍🊺 +🐖 +🐗 +🐘 +🐙 +🐚 +🐛 +🐜 +🐝 +🐞 +🐟 +🐠 +🐡 +🐢 +🐣 +🐀 +🐥 +🐊 +🐊‍⬛ +🐊‍🔥 +🐧 +🐚 +🐩 +🐪 +🐫 +🐬 +🐭 +🐮 +🐯 +🐰 +🐱 +🐲 +🐳 +🐎 +🐵 +🐶 +🐷 +🐞 +🐹 +🐺 +🐻 +🐻‍❄ +🐌 +🐜 +🐟 +🐿 +👀 +👁 +👁‍🗚 +👂 +👂🏻 +👂🏌 +👂🏜 +👂🏟 +👂🏿 +👃 +👃🏻 +👃🏌 +👃🏜 +👃🏟 +👃🏿 +👄 +👅 +👆 +👆🏻 +👆🏌 +👆🏜 +👆🏟 +👆🏿 +👇 +👇🏻 +👇🏌 +👇🏜 +👇🏟 +👇🏿 +👈 +👈🏻 +👈🏌 +👈🏜 +👈🏟 +👈🏿 +👉 +👉🏻 +👉🏌 +👉🏜 +👉🏟 +👉🏿 +👊 +👊🏻 +👊🏌 +👊🏜 +👊🏟 +👊🏿 +👋 +👋🏻 +👋🏌 +👋🏜 +👋🏟 +👋🏿 +👌 +👌🏻 +👌🏌 +👌🏜 +👌🏟 +👌🏿 +👍 +👍🏻 +👍🏌 +👍🏜 +👍🏟 +👍🏿 +👎 +👎🏻 +👎🏌 +👎🏜 +👎🏟 +👎🏿 +👏 +👏🏻 +👏🏌 +👏🏜 +👏🏟 +👏🏿 +👐 +👐🏻 +👐🏌 +👐🏜 +👐🏟 +👐🏿 +👑 +👒 +👓 +👔 +👕 +👖 +👗 +👘 +👙 +👚 +👛 +👜 +👝 +👞 +👟 +👠 +👡 +👢 +👣 +👀 +👥 +👊 +👊🏻 +👊🏌 +👊🏜 +👊🏟 +👊🏿 +👧 +👧🏻 +👧🏌 +👧🏜 +👧🏟 +👧🏿 +👚 +👚‍⚕ +👚‍⚖ +👚‍✈ +👚‍❀‍👚 +👚‍❀‍💋‍👚 +👚‍🌟 +👚‍🍳 +👚‍🍌 +👚‍🎓 +👚‍🎀 +👚‍🎚 +👚‍🏫 +👚‍🏭 +👚‍👊 +👚‍👊‍👊 +👚‍👧 +👚‍👧‍👊 +👚‍👧‍👧 +👚‍👚‍👊 +👚‍👚‍👊‍👊 +👚‍👚‍👧 +👚‍👚‍👧‍👊 +👚‍👚‍👧‍👧 +👚‍👩‍👊 +👚‍👩‍👊‍👊 +👚‍👩‍👧 +👚‍👩‍👧‍👊 +👚‍👩‍👧‍👧 +👚‍💻 +👚‍💌 +👚‍🔧 +👚‍🔬 +👚‍🚀 +👚‍🚒 +👚‍🊯 +👚‍🊯‍➡ +👚‍🊰 +👚‍🊱 +👚‍🊲 +👚‍🊳 +👚‍🊌 +👚‍🊌‍➡ +👚‍🊜 +👚‍🊜‍➡ +👚🏻 +👚🏻‍⚕ +👚🏻‍⚖ +👚🏻‍✈ +👚🏻‍❀‍👚🏻 +👚🏻‍❀‍👚🏌 +👚🏻‍❀‍👚🏜 +👚🏻‍❀‍👚🏟 +👚🏻‍❀‍👚🏿 +👚🏻‍❀‍💋‍👚🏻 +👚🏻‍❀‍💋‍👚🏌 +👚🏻‍❀‍💋‍👚🏜 +👚🏻‍❀‍💋‍👚🏟 +👚🏻‍❀‍💋‍👚🏿 +👚🏻‍🌟 +👚🏻‍🍳 +👚🏻‍🍌 +👚🏻‍🎓 +👚🏻‍🎀 +👚🏻‍🎚 +👚🏻‍🏫 +👚🏻‍🏭 +👚🏻‍🐰‍👚🏌 +👚🏻‍🐰‍👚🏜 +👚🏻‍🐰‍👚🏟 +👚🏻‍🐰‍👚🏿 +👚🏻‍💻 +👚🏻‍💌 +👚🏻‍🔧 +👚🏻‍🔬 +👚🏻‍🚀 +👚🏻‍🚒 +👚🏻‍🀝‍👚🏌 +👚🏻‍🀝‍👚🏜 +👚🏻‍🀝‍👚🏟 +👚🏻‍🀝‍👚🏿 +👚🏻‍🊯 +👚🏻‍🊯‍➡ +👚🏻‍🊰 +👚🏻‍🊱 +👚🏻‍🊲 +👚🏻‍🊳 +👚🏻‍🊌 +👚🏻‍🊌‍➡ +👚🏻‍🊜 +👚🏻‍🊜‍➡ +👚🏻‍🫯‍👚🏌 +👚🏻‍🫯‍👚🏜 +👚🏻‍🫯‍👚🏟 +👚🏻‍🫯‍👚🏿 +👚🏌 +👚🏌‍⚕ +👚🏌‍⚖ +👚🏌‍✈ +👚🏌‍❀‍👚🏻 +👚🏌‍❀‍👚🏌 +👚🏌‍❀‍👚🏜 +👚🏌‍❀‍👚🏟 +👚🏌‍❀‍👚🏿 +👚🏌‍❀‍💋‍👚🏻 +👚🏌‍❀‍💋‍👚🏌 +👚🏌‍❀‍💋‍👚🏜 +👚🏌‍❀‍💋‍👚🏟 +👚🏌‍❀‍💋‍👚🏿 +👚🏌‍🌟 +👚🏌‍🍳 +👚🏌‍🍌 +👚🏌‍🎓 +👚🏌‍🎀 +👚🏌‍🎚 +👚🏌‍🏫 +👚🏌‍🏭 +👚🏌‍🐰‍👚🏻 +👚🏌‍🐰‍👚🏜 +👚🏌‍🐰‍👚🏟 +👚🏌‍🐰‍👚🏿 +👚🏌‍💻 +👚🏌‍💌 +👚🏌‍🔧 +👚🏌‍🔬 +👚🏌‍🚀 +👚🏌‍🚒 +👚🏌‍🀝‍👚🏻 +👚🏌‍🀝‍👚🏜 +👚🏌‍🀝‍👚🏟 +👚🏌‍🀝‍👚🏿 +👚🏌‍🊯 +👚🏌‍🊯‍➡ +👚🏌‍🊰 +👚🏌‍🊱 +👚🏌‍🊲 +👚🏌‍🊳 +👚🏌‍🊌 +👚🏌‍🊌‍➡ +👚🏌‍🊜 +👚🏌‍🊜‍➡ +👚🏌‍🫯‍👚🏻 +👚🏌‍🫯‍👚🏜 +👚🏌‍🫯‍👚🏟 +👚🏌‍🫯‍👚🏿 +👚🏜 +👚🏜‍⚕ +👚🏜‍⚖ +👚🏜‍✈ +👚🏜‍❀‍👚🏻 +👚🏜‍❀‍👚🏌 +👚🏜‍❀‍👚🏜 +👚🏜‍❀‍👚🏟 +👚🏜‍❀‍👚🏿 +👚🏜‍❀‍💋‍👚🏻 +👚🏜‍❀‍💋‍👚🏌 +👚🏜‍❀‍💋‍👚🏜 +👚🏜‍❀‍💋‍👚🏟 +👚🏜‍❀‍💋‍👚🏿 +👚🏜‍🌟 +👚🏜‍🍳 +👚🏜‍🍌 +👚🏜‍🎓 +👚🏜‍🎀 +👚🏜‍🎚 +👚🏜‍🏫 +👚🏜‍🏭 +👚🏜‍🐰‍👚🏻 +👚🏜‍🐰‍👚🏌 +👚🏜‍🐰‍👚🏟 +👚🏜‍🐰‍👚🏿 +👚🏜‍💻 +👚🏜‍💌 +👚🏜‍🔧 +👚🏜‍🔬 +👚🏜‍🚀 +👚🏜‍🚒 +👚🏜‍🀝‍👚🏻 +👚🏜‍🀝‍👚🏌 +👚🏜‍🀝‍👚🏟 +👚🏜‍🀝‍👚🏿 +👚🏜‍🊯 +👚🏜‍🊯‍➡ +👚🏜‍🊰 +👚🏜‍🊱 +👚🏜‍🊲 +👚🏜‍🊳 +👚🏜‍🊌 +👚🏜‍🊌‍➡ +👚🏜‍🊜 +👚🏜‍🊜‍➡ +👚🏜‍🫯‍👚🏻 +👚🏜‍🫯‍👚🏌 +👚🏜‍🫯‍👚🏟 +👚🏜‍🫯‍👚🏿 +👚🏟 +👚🏟‍⚕ +👚🏟‍⚖ +👚🏟‍✈ +👚🏟‍❀‍👚🏻 +👚🏟‍❀‍👚🏌 +👚🏟‍❀‍👚🏜 +👚🏟‍❀‍👚🏟 +👚🏟‍❀‍👚🏿 +👚🏟‍❀‍💋‍👚🏻 +👚🏟‍❀‍💋‍👚🏌 +👚🏟‍❀‍💋‍👚🏜 +👚🏟‍❀‍💋‍👚🏟 +👚🏟‍❀‍💋‍👚🏿 +👚🏟‍🌟 +👚🏟‍🍳 +👚🏟‍🍌 +👚🏟‍🎓 +👚🏟‍🎀 +👚🏟‍🎚 +👚🏟‍🏫 +👚🏟‍🏭 +👚🏟‍🐰‍👚🏻 +👚🏟‍🐰‍👚🏌 +👚🏟‍🐰‍👚🏜 +👚🏟‍🐰‍👚🏿 +👚🏟‍💻 +👚🏟‍💌 +👚🏟‍🔧 +👚🏟‍🔬 +👚🏟‍🚀 +👚🏟‍🚒 +👚🏟‍🀝‍👚🏻 +👚🏟‍🀝‍👚🏌 +👚🏟‍🀝‍👚🏜 +👚🏟‍🀝‍👚🏿 +👚🏟‍🊯 +👚🏟‍🊯‍➡ +👚🏟‍🊰 +👚🏟‍🊱 +👚🏟‍🊲 +👚🏟‍🊳 +👚🏟‍🊌 +👚🏟‍🊌‍➡ +👚🏟‍🊜 +👚🏟‍🊜‍➡ +👚🏟‍🫯‍👚🏻 +👚🏟‍🫯‍👚🏌 +👚🏟‍🫯‍👚🏜 +👚🏟‍🫯‍👚🏿 +👚🏿 +👚🏿‍⚕ +👚🏿‍⚖ +👚🏿‍✈ +👚🏿‍❀‍👚🏻 +👚🏿‍❀‍👚🏌 +👚🏿‍❀‍👚🏜 +👚🏿‍❀‍👚🏟 +👚🏿‍❀‍👚🏿 +👚🏿‍❀‍💋‍👚🏻 +👚🏿‍❀‍💋‍👚🏌 +👚🏿‍❀‍💋‍👚🏜 +👚🏿‍❀‍💋‍👚🏟 +👚🏿‍❀‍💋‍👚🏿 +👚🏿‍🌟 +👚🏿‍🍳 +👚🏿‍🍌 +👚🏿‍🎓 +👚🏿‍🎀 +👚🏿‍🎚 +👚🏿‍🏫 +👚🏿‍🏭 +👚🏿‍🐰‍👚🏻 +👚🏿‍🐰‍👚🏌 +👚🏿‍🐰‍👚🏜 +👚🏿‍🐰‍👚🏟 +👚🏿‍💻 +👚🏿‍💌 +👚🏿‍🔧 +👚🏿‍🔬 +👚🏿‍🚀 +👚🏿‍🚒 +👚🏿‍🀝‍👚🏻 +👚🏿‍🀝‍👚🏌 +👚🏿‍🀝‍👚🏜 +👚🏿‍🀝‍👚🏟 +👚🏿‍🊯 +👚🏿‍🊯‍➡ +👚🏿‍🊰 +👚🏿‍🊱 +👚🏿‍🊲 +👚🏿‍🊳 +👚🏿‍🊌 +👚🏿‍🊌‍➡ +👚🏿‍🊜 +👚🏿‍🊜‍➡ +👚🏿‍🫯‍👚🏻 +👚🏿‍🫯‍👚🏌 +👚🏿‍🫯‍👚🏜 +👚🏿‍🫯‍👚🏟 +👩 +👩‍⚕ +👩‍⚖ +👩‍✈ +👩‍❀‍👚 +👩‍❀‍👩 +👩‍❀‍💋‍👚 +👩‍❀‍💋‍👩 +👩‍🌟 +👩‍🍳 +👩‍🍌 +👩‍🎓 +👩‍🎀 +👩‍🎚 +👩‍🏫 +👩‍🏭 +👩‍👊 +👩‍👊‍👊 +👩‍👧 +👩‍👧‍👊 +👩‍👧‍👧 +👩‍👩‍👊 +👩‍👩‍👊‍👊 +👩‍👩‍👧 +👩‍👩‍👧‍👊 +👩‍👩‍👧‍👧 +👩‍💻 +👩‍💌 +👩‍🔧 +👩‍🔬 +👩‍🚀 +👩‍🚒 +👩‍🊯 +👩‍🊯‍➡ +👩‍🊰 +👩‍🊱 +👩‍🊲 +👩‍🊳 +👩‍🊌 +👩‍🊌‍➡ +👩‍🊜 +👩‍🊜‍➡ +👩🏻 +👩🏻‍⚕ +👩🏻‍⚖ +👩🏻‍✈ +👩🏻‍❀‍👚🏻 +👩🏻‍❀‍👚🏌 +👩🏻‍❀‍👚🏜 +👩🏻‍❀‍👚🏟 +👩🏻‍❀‍👚🏿 +👩🏻‍❀‍👩🏻 +👩🏻‍❀‍👩🏌 +👩🏻‍❀‍👩🏜 +👩🏻‍❀‍👩🏟 +👩🏻‍❀‍👩🏿 +👩🏻‍❀‍💋‍👚🏻 +👩🏻‍❀‍💋‍👚🏌 +👩🏻‍❀‍💋‍👚🏜 +👩🏻‍❀‍💋‍👚🏟 +👩🏻‍❀‍💋‍👚🏿 +👩🏻‍❀‍💋‍👩🏻 +👩🏻‍❀‍💋‍👩🏌 +👩🏻‍❀‍💋‍👩🏜 +👩🏻‍❀‍💋‍👩🏟 +👩🏻‍❀‍💋‍👩🏿 +👩🏻‍🌟 +👩🏻‍🍳 +👩🏻‍🍌 +👩🏻‍🎓 +👩🏻‍🎀 +👩🏻‍🎚 +👩🏻‍🏫 +👩🏻‍🏭 +👩🏻‍🐰‍👩🏌 +👩🏻‍🐰‍👩🏜 +👩🏻‍🐰‍👩🏟 +👩🏻‍🐰‍👩🏿 +👩🏻‍💻 +👩🏻‍💌 +👩🏻‍🔧 +👩🏻‍🔬 +👩🏻‍🚀 +👩🏻‍🚒 +👩🏻‍🀝‍👚🏌 +👩🏻‍🀝‍👚🏜 +👩🏻‍🀝‍👚🏟 +👩🏻‍🀝‍👚🏿 +👩🏻‍🀝‍👩🏌 +👩🏻‍🀝‍👩🏜 +👩🏻‍🀝‍👩🏟 +👩🏻‍🀝‍👩🏿 +👩🏻‍🊯 +👩🏻‍🊯‍➡ +👩🏻‍🊰 +👩🏻‍🊱 +👩🏻‍🊲 +👩🏻‍🊳 +👩🏻‍🊌 +👩🏻‍🊌‍➡ +👩🏻‍🊜 +👩🏻‍🊜‍➡ +👩🏻‍🫯‍👩🏌 +👩🏻‍🫯‍👩🏜 +👩🏻‍🫯‍👩🏟 +👩🏻‍🫯‍👩🏿 +👩🏌 +👩🏌‍⚕ +👩🏌‍⚖ +👩🏌‍✈ +👩🏌‍❀‍👚🏻 +👩🏌‍❀‍👚🏌 +👩🏌‍❀‍👚🏜 +👩🏌‍❀‍👚🏟 +👩🏌‍❀‍👚🏿 +👩🏌‍❀‍👩🏻 +👩🏌‍❀‍👩🏌 +👩🏌‍❀‍👩🏜 +👩🏌‍❀‍👩🏟 +👩🏌‍❀‍👩🏿 +👩🏌‍❀‍💋‍👚🏻 +👩🏌‍❀‍💋‍👚🏌 +👩🏌‍❀‍💋‍👚🏜 +👩🏌‍❀‍💋‍👚🏟 +👩🏌‍❀‍💋‍👚🏿 +👩🏌‍❀‍💋‍👩🏻 +👩🏌‍❀‍💋‍👩🏌 +👩🏌‍❀‍💋‍👩🏜 +👩🏌‍❀‍💋‍👩🏟 +👩🏌‍❀‍💋‍👩🏿 +👩🏌‍🌟 +👩🏌‍🍳 +👩🏌‍🍌 +👩🏌‍🎓 +👩🏌‍🎀 +👩🏌‍🎚 +👩🏌‍🏫 +👩🏌‍🏭 +👩🏌‍🐰‍👩🏻 +👩🏌‍🐰‍👩🏜 +👩🏌‍🐰‍👩🏟 +👩🏌‍🐰‍👩🏿 +👩🏌‍💻 +👩🏌‍💌 +👩🏌‍🔧 +👩🏌‍🔬 +👩🏌‍🚀 +👩🏌‍🚒 +👩🏌‍🀝‍👚🏻 +👩🏌‍🀝‍👚🏜 +👩🏌‍🀝‍👚🏟 +👩🏌‍🀝‍👚🏿 +👩🏌‍🀝‍👩🏻 +👩🏌‍🀝‍👩🏜 +👩🏌‍🀝‍👩🏟 +👩🏌‍🀝‍👩🏿 +👩🏌‍🊯 +👩🏌‍🊯‍➡ +👩🏌‍🊰 +👩🏌‍🊱 +👩🏌‍🊲 +👩🏌‍🊳 +👩🏌‍🊌 +👩🏌‍🊌‍➡ +👩🏌‍🊜 +👩🏌‍🊜‍➡ +👩🏌‍🫯‍👩🏻 +👩🏌‍🫯‍👩🏜 +👩🏌‍🫯‍👩🏟 +👩🏌‍🫯‍👩🏿 +👩🏜 +👩🏜‍⚕ +👩🏜‍⚖ +👩🏜‍✈ +👩🏜‍❀‍👚🏻 +👩🏜‍❀‍👚🏌 +👩🏜‍❀‍👚🏜 +👩🏜‍❀‍👚🏟 +👩🏜‍❀‍👚🏿 +👩🏜‍❀‍👩🏻 +👩🏜‍❀‍👩🏌 +👩🏜‍❀‍👩🏜 +👩🏜‍❀‍👩🏟 +👩🏜‍❀‍👩🏿 +👩🏜‍❀‍💋‍👚🏻 +👩🏜‍❀‍💋‍👚🏌 +👩🏜‍❀‍💋‍👚🏜 +👩🏜‍❀‍💋‍👚🏟 +👩🏜‍❀‍💋‍👚🏿 +👩🏜‍❀‍💋‍👩🏻 +👩🏜‍❀‍💋‍👩🏌 +👩🏜‍❀‍💋‍👩🏜 +👩🏜‍❀‍💋‍👩🏟 +👩🏜‍❀‍💋‍👩🏿 +👩🏜‍🌟 +👩🏜‍🍳 +👩🏜‍🍌 +👩🏜‍🎓 +👩🏜‍🎀 +👩🏜‍🎚 +👩🏜‍🏫 +👩🏜‍🏭 +👩🏜‍🐰‍👩🏻 +👩🏜‍🐰‍👩🏌 +👩🏜‍🐰‍👩🏟 +👩🏜‍🐰‍👩🏿 +👩🏜‍💻 +👩🏜‍💌 +👩🏜‍🔧 +👩🏜‍🔬 +👩🏜‍🚀 +👩🏜‍🚒 +👩🏜‍🀝‍👚🏻 +👩🏜‍🀝‍👚🏌 +👩🏜‍🀝‍👚🏟 +👩🏜‍🀝‍👚🏿 +👩🏜‍🀝‍👩🏻 +👩🏜‍🀝‍👩🏌 +👩🏜‍🀝‍👩🏟 +👩🏜‍🀝‍👩🏿 +👩🏜‍🊯 +👩🏜‍🊯‍➡ +👩🏜‍🊰 +👩🏜‍🊱 +👩🏜‍🊲 +👩🏜‍🊳 +👩🏜‍🊌 +👩🏜‍🊌‍➡ +👩🏜‍🊜 +👩🏜‍🊜‍➡ +👩🏜‍🫯‍👩🏻 +👩🏜‍🫯‍👩🏌 +👩🏜‍🫯‍👩🏟 +👩🏜‍🫯‍👩🏿 +👩🏟 +👩🏟‍⚕ +👩🏟‍⚖ +👩🏟‍✈ +👩🏟‍❀‍👚🏻 +👩🏟‍❀‍👚🏌 +👩🏟‍❀‍👚🏜 +👩🏟‍❀‍👚🏟 +👩🏟‍❀‍👚🏿 +👩🏟‍❀‍👩🏻 +👩🏟‍❀‍👩🏌 +👩🏟‍❀‍👩🏜 +👩🏟‍❀‍👩🏟 +👩🏟‍❀‍👩🏿 +👩🏟‍❀‍💋‍👚🏻 +👩🏟‍❀‍💋‍👚🏌 +👩🏟‍❀‍💋‍👚🏜 +👩🏟‍❀‍💋‍👚🏟 +👩🏟‍❀‍💋‍👚🏿 +👩🏟‍❀‍💋‍👩🏻 +👩🏟‍❀‍💋‍👩🏌 +👩🏟‍❀‍💋‍👩🏜 +👩🏟‍❀‍💋‍👩🏟 +👩🏟‍❀‍💋‍👩🏿 +👩🏟‍🌟 +👩🏟‍🍳 +👩🏟‍🍌 +👩🏟‍🎓 +👩🏟‍🎀 +👩🏟‍🎚 +👩🏟‍🏫 +👩🏟‍🏭 +👩🏟‍🐰‍👩🏻 +👩🏟‍🐰‍👩🏌 +👩🏟‍🐰‍👩🏜 +👩🏟‍🐰‍👩🏿 +👩🏟‍💻 +👩🏟‍💌 +👩🏟‍🔧 +👩🏟‍🔬 +👩🏟‍🚀 +👩🏟‍🚒 +👩🏟‍🀝‍👚🏻 +👩🏟‍🀝‍👚🏌 +👩🏟‍🀝‍👚🏜 +👩🏟‍🀝‍👚🏿 +👩🏟‍🀝‍👩🏻 +👩🏟‍🀝‍👩🏌 +👩🏟‍🀝‍👩🏜 +👩🏟‍🀝‍👩🏿 +👩🏟‍🊯 +👩🏟‍🊯‍➡ +👩🏟‍🊰 +👩🏟‍🊱 +👩🏟‍🊲 +👩🏟‍🊳 +👩🏟‍🊌 +👩🏟‍🊌‍➡ +👩🏟‍🊜 +👩🏟‍🊜‍➡ +👩🏟‍🫯‍👩🏻 +👩🏟‍🫯‍👩🏌 +👩🏟‍🫯‍👩🏜 +👩🏟‍🫯‍👩🏿 +👩🏿 +👩🏿‍⚕ +👩🏿‍⚖ +👩🏿‍✈ +👩🏿‍❀‍👚🏻 +👩🏿‍❀‍👚🏌 +👩🏿‍❀‍👚🏜 +👩🏿‍❀‍👚🏟 +👩🏿‍❀‍👚🏿 +👩🏿‍❀‍👩🏻 +👩🏿‍❀‍👩🏌 +👩🏿‍❀‍👩🏜 +👩🏿‍❀‍👩🏟 +👩🏿‍❀‍👩🏿 +👩🏿‍❀‍💋‍👚🏻 +👩🏿‍❀‍💋‍👚🏌 +👩🏿‍❀‍💋‍👚🏜 +👩🏿‍❀‍💋‍👚🏟 +👩🏿‍❀‍💋‍👚🏿 +👩🏿‍❀‍💋‍👩🏻 +👩🏿‍❀‍💋‍👩🏌 +👩🏿‍❀‍💋‍👩🏜 +👩🏿‍❀‍💋‍👩🏟 +👩🏿‍❀‍💋‍👩🏿 +👩🏿‍🌟 +👩🏿‍🍳 +👩🏿‍🍌 +👩🏿‍🎓 +👩🏿‍🎀 +👩🏿‍🎚 +👩🏿‍🏫 +👩🏿‍🏭 +👩🏿‍🐰‍👩🏻 +👩🏿‍🐰‍👩🏌 +👩🏿‍🐰‍👩🏜 +👩🏿‍🐰‍👩🏟 +👩🏿‍💻 +👩🏿‍💌 +👩🏿‍🔧 +👩🏿‍🔬 +👩🏿‍🚀 +👩🏿‍🚒 +👩🏿‍🀝‍👚🏻 +👩🏿‍🀝‍👚🏌 +👩🏿‍🀝‍👚🏜 +👩🏿‍🀝‍👚🏟 +👩🏿‍🀝‍👩🏻 +👩🏿‍🀝‍👩🏌 +👩🏿‍🀝‍👩🏜 +👩🏿‍🀝‍👩🏟 +👩🏿‍🊯 +👩🏿‍🊯‍➡ +👩🏿‍🊰 +👩🏿‍🊱 +👩🏿‍🊲 +👩🏿‍🊳 +👩🏿‍🊌 +👩🏿‍🊌‍➡ +👩🏿‍🊜 +👩🏿‍🊜‍➡ +👩🏿‍🫯‍👩🏻 +👩🏿‍🫯‍👩🏌 +👩🏿‍🫯‍👩🏜 +👩🏿‍🫯‍👩🏟 +👪 +👫 +👫🏻 +👫🏌 +👫🏜 +👫🏟 +👫🏿 +👬 +👬🏻 +👬🏌 +👬🏜 +👬🏟 +👬🏿 +👭 +👭🏻 +👭🏌 +👭🏜 +👭🏟 +👭🏿 +👮 +👮‍♀ +👮‍♂ +👮🏻 +👮🏻‍♀ +👮🏻‍♂ +👮🏌 +👮🏌‍♀ +👮🏌‍♂ +👮🏜 +👮🏜‍♀ +👮🏜‍♂ +👮🏟 +👮🏟‍♀ +👮🏟‍♂ +👮🏿 +👮🏿‍♀ +👮🏿‍♂ +👯 +👯‍♀ +👯‍♂ +👯🏻 +👯🏻‍♀ +👯🏻‍♂ +👯🏌 +👯🏌‍♀ +👯🏌‍♂ +👯🏜 +👯🏜‍♀ +👯🏜‍♂ +👯🏟 +👯🏟‍♀ +👯🏟‍♂ +👯🏿 +👯🏿‍♀ +👯🏿‍♂ +👰 +👰‍♀ +👰‍♂ +👰🏻 +👰🏻‍♀ +👰🏻‍♂ +👰🏌 +👰🏌‍♀ +👰🏌‍♂ +👰🏜 +👰🏜‍♀ +👰🏜‍♂ +👰🏟 +👰🏟‍♀ +👰🏟‍♂ +👰🏿 +👰🏿‍♀ +👰🏿‍♂ +👱 +👱‍♀ +👱‍♂ +👱🏻 +👱🏻‍♀ +👱🏻‍♂ +👱🏌 +👱🏌‍♀ +👱🏌‍♂ +👱🏜 +👱🏜‍♀ +👱🏜‍♂ +👱🏟 +👱🏟‍♀ +👱🏟‍♂ +👱🏿 +👱🏿‍♀ +👱🏿‍♂ +👲 +👲🏻 +👲🏌 +👲🏜 +👲🏟 +👲🏿 +👳 +👳‍♀ +👳‍♂ +👳🏻 +👳🏻‍♀ +👳🏻‍♂ +👳🏌 +👳🏌‍♀ +👳🏌‍♂ +👳🏜 +👳🏜‍♀ +👳🏜‍♂ +👳🏟 +👳🏟‍♀ +👳🏟‍♂ +👳🏿 +👳🏿‍♀ +👳🏿‍♂ +👎 +👎🏻 +👎🏌 +👎🏜 +👎🏟 +👎🏿 +👵 +👵🏻 +👵🏌 +👵🏜 +👵🏟 +👵🏿 +👶 +👶🏻 +👶🏌 +👶🏜 +👶🏟 +👶🏿 +👷 +👷‍♀ +👷‍♂ +👷🏻 +👷🏻‍♀ +👷🏻‍♂ +👷🏌 +👷🏌‍♀ +👷🏌‍♂ +👷🏜 +👷🏜‍♀ +👷🏜‍♂ +👷🏟 +👷🏟‍♀ +👷🏟‍♂ +👷🏿 +👷🏿‍♀ +👷🏿‍♂ +👞 +👞🏻 +👞🏌 +👞🏜 +👞🏟 +👞🏿 +👹 +👺 +👻 +👌 +👌🏻 +👌🏌 +👌🏜 +👌🏟 +👌🏿 +👜 +👟 +👿 +💀 +💁 +💁‍♀ +💁‍♂ +💁🏻 +💁🏻‍♀ +💁🏻‍♂ +💁🏌 +💁🏌‍♀ +💁🏌‍♂ +💁🏜 +💁🏜‍♀ +💁🏜‍♂ +💁🏟 +💁🏟‍♀ +💁🏟‍♂ +💁🏿 +💁🏿‍♀ +💁🏿‍♂ +💂 +💂‍♀ +💂‍♂ +💂🏻 +💂🏻‍♀ +💂🏻‍♂ +💂🏌 +💂🏌‍♀ +💂🏌‍♂ +💂🏜 +💂🏜‍♀ +💂🏜‍♂ +💂🏟 +💂🏟‍♀ +💂🏟‍♂ +💂🏿 +💂🏿‍♀ +💂🏿‍♂ +💃 +💃🏻 +💃🏌 +💃🏜 +💃🏟 +💃🏿 +💄 +💅 +💅🏻 +💅🏌 +💅🏜 +💅🏟 +💅🏿 +💆 +💆‍♀ +💆‍♂ +💆🏻 +💆🏻‍♀ +💆🏻‍♂ +💆🏌 +💆🏌‍♀ +💆🏌‍♂ +💆🏜 +💆🏜‍♀ +💆🏜‍♂ +💆🏟 +💆🏟‍♀ +💆🏟‍♂ +💆🏿 +💆🏿‍♀ +💆🏿‍♂ +💇 +💇‍♀ +💇‍♂ +💇🏻 +💇🏻‍♀ +💇🏻‍♂ +💇🏌 +💇🏌‍♀ +💇🏌‍♂ +💇🏜 +💇🏜‍♀ +💇🏜‍♂ +💇🏟 +💇🏟‍♀ +💇🏟‍♂ +💇🏿 +💇🏿‍♀ +💇🏿‍♂ +💈 +💉 +💊 +💋 +💌 +💍 +💎 +💏 +💏🏻 +💏🏌 +💏🏜 +💏🏟 +💏🏿 +💐 +💑 +💑🏻 +💑🏌 +💑🏜 +💑🏟 +💑🏿 +💒 +💓 +💔 +💕 +💖 +💗 +💘 +💙 +💚 +💛 +💜 +💝 +💞 +💟 +💠 +💡 +💢 +💣 +💀 +💥 +💊 +💧 +💚 +💩 +💪 +💪🏻 +💪🏌 +💪🏜 +💪🏟 +💪🏿 +💫 +💬 +💭 +💮 +💯 +💰 +💱 +💲 +💳 +💎 +💵 +💶 +💷 +💞 +💹 +💺 +💻 +💌 +💜 +💟 +💿 +📀 +📁 +📂 +📃 +📄 +📅 +📆 +📇 +📈 +📉 +📊 +📋 +📌 +📍 +📎 +📏 +📐 +📑 +📒 +📓 +📔 +📕 +📖 +📗 +📘 +📙 +📚 +📛 +📜 +📝 +📞 +📟 +📠 +📡 +📢 +📣 +📀 +📥 +📊 +📧 +📚 +📩 +📪 +📫 +📬 +📭 +📮 +📯 +📰 +📱 +📲 +📳 +📎 +📵 +📶 +📷 +📞 +📹 +📺 +📻 +📌 +📜 +📿 +🔀 +🔁 +🔂 +🔃 +🔄 +🔅 +🔆 +🔇 +🔈 +🔉 +🔊 +🔋 +🔌 +🔍 +🔎 +🔏 +🔐 +🔑 +🔒 +🔓 +🔔 +🔕 +🔖 +🔗 +🔘 +🔙 +🔚 +🔛 +🔜 +🔝 +🔞 +🔟 +🔠 +🔡 +🔢 +🔣 +🔀 +🔥 +🔊 +🔧 +🔚 +🔩 +🔪 +🔫 +🔬 +🔭 +🔮 +🔯 +🔰 +🔱 +🔲 +🔳 +🔎 +🔵 +🔶 +🔷 +🔞 +🔹 +🔺 +🔻 +🔌 +🔜 +🕉 +🕊 +🕋 +🕌 +🕍 +🕎 +🕐 +🕑 +🕒 +🕓 +🕔 +🕕 +🕖 +🕗 +🕘 +🕙 +🕚 +🕛 +🕜 +🕝 +🕞 +🕟 +🕠 +🕡 +🕢 +🕣 +🕀 +🕥 +🕊 +🕧 +🕯 +🕰 +🕳 +🕎 +🕎🏻 +🕎🏌 +🕎🏜 +🕎🏟 +🕎🏿 +🕵 +🕵‍♀ +🕵‍♂ +🕵🏻 +🕵🏻‍♀ +🕵🏻‍♂ +🕵🏌 +🕵🏌‍♀ +🕵🏌‍♂ +🕵🏜 +🕵🏜‍♀ +🕵🏜‍♂ +🕵🏟 +🕵🏟‍♀ +🕵🏟‍♂ +🕵🏿 +🕵🏿‍♀ +🕵🏿‍♂ +🕶 +🕷 +🕞 +🕹 +🕺 +🕺🏻 +🕺🏌 +🕺🏜 +🕺🏟 +🕺🏿 +🖇 +🖊 +🖋 +🖌 +🖍 +🖐 +🖐🏻 +🖐🏌 +🖐🏜 +🖐🏟 +🖐🏿 +🖕 +🖕🏻 +🖕🏌 +🖕🏜 +🖕🏟 +🖕🏿 +🖖 +🖖🏻 +🖖🏌 +🖖🏜 +🖖🏟 +🖖🏿 +🖀 +🖥 +🖚 +🖱 +🖲 +🖌 +🗂 +🗃 +🗄 +🗑 +🗒 +🗓 +🗜 +🗝 +🗞 +🗡 +🗣 +🗚 +🗯 +🗳 +🗺 +🗻 +🗌 +🗜 +🗟 +🗿 +😀 +😁 +😂 +😃 +😄 +😅 +😆 +😇 +😈 +😉 +😊 +😋 +😌 +😍 +😎 +😏 +😐 +😑 +😒 +😓 +😔 +😕 +😖 +😗 +😘 +😙 +😚 +😛 +😜 +😝 +😞 +😟 +😠 +😡 +😢 +😣 +😀 +😥 +😊 +😧 +😚 +😩 +😪 +😫 +😬 +😭 +😮 +😮‍💚 +😯 +😰 +😱 +😲 +😳 +😎 +😵 +😵‍💫 +😶 +😶‍🌫 +😷 +😞 +😹 +😺 +😻 +😌 +😜 +😟 +😿 +🙀 +🙁 +🙂 +🙂‍↔ +🙂‍↕ +🙃 +🙄 +🙅 +🙅‍♀ +🙅‍♂ +🙅🏻 +🙅🏻‍♀ +🙅🏻‍♂ +🙅🏌 +🙅🏌‍♀ +🙅🏌‍♂ +🙅🏜 +🙅🏜‍♀ +🙅🏜‍♂ +🙅🏟 +🙅🏟‍♀ +🙅🏟‍♂ +🙅🏿 +🙅🏿‍♀ +🙅🏿‍♂ +🙆 +🙆‍♀ +🙆‍♂ +🙆🏻 +🙆🏻‍♀ +🙆🏻‍♂ +🙆🏌 +🙆🏌‍♀ +🙆🏌‍♂ +🙆🏜 +🙆🏜‍♀ +🙆🏜‍♂ +🙆🏟 +🙆🏟‍♀ +🙆🏟‍♂ +🙆🏿 +🙆🏿‍♀ +🙆🏿‍♂ +🙇 +🙇‍♀ +🙇‍♂ +🙇🏻 +🙇🏻‍♀ +🙇🏻‍♂ +🙇🏌 +🙇🏌‍♀ +🙇🏌‍♂ +🙇🏜 +🙇🏜‍♀ +🙇🏜‍♂ +🙇🏟 +🙇🏟‍♀ +🙇🏟‍♂ +🙇🏿 +🙇🏿‍♀ +🙇🏿‍♂ +🙈 +🙉 +🙊 +🙋 +🙋‍♀ +🙋‍♂ +🙋🏻 +🙋🏻‍♀ +🙋🏻‍♂ +🙋🏌 +🙋🏌‍♀ +🙋🏌‍♂ +🙋🏜 +🙋🏜‍♀ +🙋🏜‍♂ +🙋🏟 +🙋🏟‍♀ +🙋🏟‍♂ +🙋🏿 +🙋🏿‍♀ +🙋🏿‍♂ +🙌 +🙌🏻 +🙌🏌 +🙌🏜 +🙌🏟 +🙌🏿 +🙍 +🙍‍♀ +🙍‍♂ +🙍🏻 +🙍🏻‍♀ +🙍🏻‍♂ +🙍🏌 +🙍🏌‍♀ +🙍🏌‍♂ +🙍🏜 +🙍🏜‍♀ +🙍🏜‍♂ +🙍🏟 +🙍🏟‍♀ +🙍🏟‍♂ +🙍🏿 +🙍🏿‍♀ +🙍🏿‍♂ +🙎 +🙎‍♀ +🙎‍♂ +🙎🏻 +🙎🏻‍♀ +🙎🏻‍♂ +🙎🏌 +🙎🏌‍♀ +🙎🏌‍♂ +🙎🏜 +🙎🏜‍♀ +🙎🏜‍♂ +🙎🏟 +🙎🏟‍♀ +🙎🏟‍♂ +🙎🏿 +🙎🏿‍♀ +🙎🏿‍♂ +🙏 +🙏🏻 +🙏🏌 +🙏🏜 +🙏🏟 +🙏🏿 +🚀 +🚁 +🚂 +🚃 +🚄 +🚅 +🚆 +🚇 +🚈 +🚉 +🚊 +🚋 +🚌 +🚍 +🚎 +🚏 +🚐 +🚑 +🚒 +🚓 +🚔 +🚕 +🚖 +🚗 +🚘 +🚙 +🚚 +🚛 +🚜 +🚝 +🚞 +🚟 +🚠 +🚡 +🚢 +🚣 +🚣‍♀ +🚣‍♂ +🚣🏻 +🚣🏻‍♀ +🚣🏻‍♂ +🚣🏌 +🚣🏌‍♀ +🚣🏌‍♂ +🚣🏜 +🚣🏜‍♀ +🚣🏜‍♂ +🚣🏟 +🚣🏟‍♀ +🚣🏟‍♂ +🚣🏿 +🚣🏿‍♀ +🚣🏿‍♂ +🚀 +🚥 +🚊 +🚧 +🚚 +🚩 +🚪 +🚫 +🚬 +🚭 +🚮 +🚯 +🚰 +🚱 +🚲 +🚳 +🚎 +🚎‍♀ +🚎‍♂ +🚎🏻 +🚎🏻‍♀ +🚎🏻‍♂ +🚎🏌 +🚎🏌‍♀ +🚎🏌‍♂ +🚎🏜 +🚎🏜‍♀ +🚎🏜‍♂ +🚎🏟 +🚎🏟‍♀ +🚎🏟‍♂ +🚎🏿 +🚎🏿‍♀ +🚎🏿‍♂ +🚵 +🚵‍♀ +🚵‍♂ +🚵🏻 +🚵🏻‍♀ +🚵🏻‍♂ +🚵🏌 +🚵🏌‍♀ +🚵🏌‍♂ +🚵🏜 +🚵🏜‍♀ +🚵🏜‍♂ +🚵🏟 +🚵🏟‍♀ +🚵🏟‍♂ +🚵🏿 +🚵🏿‍♀ +🚵🏿‍♂ +🚶 +🚶‍♀ +🚶‍♀‍➡ +🚶‍♂ +🚶‍♂‍➡ +🚶‍➡ +🚶🏻 +🚶🏻‍♀ +🚶🏻‍♀‍➡ +🚶🏻‍♂ +🚶🏻‍♂‍➡ +🚶🏻‍➡ +🚶🏌 +🚶🏌‍♀ +🚶🏌‍♀‍➡ +🚶🏌‍♂ +🚶🏌‍♂‍➡ +🚶🏌‍➡ +🚶🏜 +🚶🏜‍♀ +🚶🏜‍♀‍➡ +🚶🏜‍♂ +🚶🏜‍♂‍➡ +🚶🏜‍➡ +🚶🏟 +🚶🏟‍♀ +🚶🏟‍♀‍➡ +🚶🏟‍♂ +🚶🏟‍♂‍➡ +🚶🏟‍➡ +🚶🏿 +🚶🏿‍♀ +🚶🏿‍♀‍➡ +🚶🏿‍♂ +🚶🏿‍♂‍➡ +🚶🏿‍➡ +🚷 +🚞 +🚹 +🚺 +🚻 +🚌 +🚜 +🚟 +🚿 +🛀 +🛀🏻 +🛀🏌 +🛀🏜 +🛀🏟 +🛀🏿 +🛁 +🛂 +🛃 +🛄 +🛅 +🛋 +🛌 +🛌🏻 +🛌🏌 +🛌🏜 +🛌🏟 +🛌🏿 +🛍 +🛎 +🛏 +🛐 +🛑 +🛒 +🛕 +🛖 +🛗 +🛘 +🛜 +🛝 +🛞 +🛟 +🛠 +🛡 +🛢 +🛣 +🛀 +🛥 +🛩 +🛫 +🛬 +🛰 +🛳 +🛎 +🛵 +🛶 +🛷 +🛞 +🛹 +🛺 +🛻 +🛌 +🟠 +🟡 +🟢 +🟣 +🟀 +🟥 +🟊 +🟧 +🟚 +🟩 +🟪 +🟫 +🟰 +🀌 +🀌🏻 +🀌🏌 +🀌🏜 +🀌🏟 +🀌🏿 +🀍 +🀎 +🀏 +🀏🏻 +🀏🏌 +🀏🏜 +🀏🏟 +🀏🏿 +🀐 +🀑 +🀒 +🀓 +🀔 +🀕 +🀖 +🀗 +🀘 +🀘🏻 +🀘🏌 +🀘🏜 +🀘🏟 +🀘🏿 +🀙 +🀙🏻 +🀙🏌 +🀙🏜 +🀙🏟 +🀙🏿 +🀚 +🀚🏻 +🀚🏌 +🀚🏜 +🀚🏟 +🀚🏿 +🀛 +🀛🏻 +🀛🏌 +🀛🏜 +🀛🏟 +🀛🏿 +🀜 +🀜🏻 +🀜🏌 +🀜🏜 +🀜🏟 +🀜🏿 +🀝 +🀝🏻 +🀝🏌 +🀝🏜 +🀝🏟 +🀝🏿 +🀞 +🀞🏻 +🀞🏌 +🀞🏜 +🀞🏟 +🀞🏿 +🀟 +🀟🏻 +🀟🏌 +🀟🏜 +🀟🏟 +🀟🏿 +🀠 +🀡 +🀢 +🀣 +🀀 +🀥 +🀊 +🀊‍♀ +🀊‍♂ +🀊🏻 +🀊🏻‍♀ +🀊🏻‍♂ +🀊🏌 +🀊🏌‍♀ +🀊🏌‍♂ +🀊🏜 +🀊🏜‍♀ +🀊🏜‍♂ +🀊🏟 +🀊🏟‍♀ +🀊🏟‍♂ +🀊🏿 +🀊🏿‍♀ +🀊🏿‍♂ +🀧 +🀚 +🀩 +🀪 +🀫 +🀬 +🀭 +🀮 +🀯 +🀰 +🀰🏻 +🀰🏌 +🀰🏜 +🀰🏟 +🀰🏿 +🀱 +🀱🏻 +🀱🏌 +🀱🏜 +🀱🏟 +🀱🏿 +🀲 +🀲🏻 +🀲🏌 +🀲🏜 +🀲🏟 +🀲🏿 +🀳 +🀳🏻 +🀳🏌 +🀳🏜 +🀳🏟 +🀳🏿 +🀎 +🀎🏻 +🀎🏌 +🀎🏜 +🀎🏟 +🀎🏿 +🀵 +🀵‍♀ +🀵‍♂ +🀵🏻 +🀵🏻‍♀ +🀵🏻‍♂ +🀵🏌 +🀵🏌‍♀ +🀵🏌‍♂ +🀵🏜 +🀵🏜‍♀ +🀵🏜‍♂ +🀵🏟 +🀵🏟‍♀ +🀵🏟‍♂ +🀵🏿 +🀵🏿‍♀ +🀵🏿‍♂ +🀶 +🀶🏻 +🀶🏌 +🀶🏜 +🀶🏟 +🀶🏿 +🀷 +🀷‍♀ +🀷‍♂ +🀷🏻 +🀷🏻‍♀ +🀷🏻‍♂ +🀷🏌 +🀷🏌‍♀ +🀷🏌‍♂ +🀷🏜 +🀷🏜‍♀ +🀷🏜‍♂ +🀷🏟 +🀷🏟‍♀ +🀷🏟‍♂ +🀷🏿 +🀷🏿‍♀ +🀷🏿‍♂ +🀞 +🀞‍♀ +🀞‍♂ +🀞🏻 +🀞🏻‍♀ +🀞🏻‍♂ +🀞🏌 +🀞🏌‍♀ +🀞🏌‍♂ +🀞🏜 +🀞🏜‍♀ +🀞🏜‍♂ +🀞🏟 +🀞🏟‍♀ +🀞🏟‍♂ +🀞🏿 +🀞🏿‍♀ +🀞🏿‍♂ +🀹 +🀹‍♀ +🀹‍♂ +🀹🏻 +🀹🏻‍♀ +🀹🏻‍♂ +🀹🏌 +🀹🏌‍♀ +🀹🏌‍♂ +🀹🏜 +🀹🏜‍♀ +🀹🏜‍♂ +🀹🏟 +🀹🏟‍♀ +🀹🏟‍♂ +🀹🏿 +🀹🏿‍♀ +🀹🏿‍♂ +🀺 +🀌 +🀌‍♀ +🀌‍♂ +🀌🏻 +🀌🏻‍♀ +🀌🏻‍♂ +🀌🏌 +🀌🏌‍♀ +🀌🏌‍♂ +🀌🏜 +🀌🏜‍♀ +🀌🏜‍♂ +🀌🏟 +🀌🏟‍♀ +🀌🏟‍♂ +🀌🏿 +🀌🏿‍♀ +🀌🏿‍♂ +🀜 +🀜‍♀ +🀜‍♂ +🀜🏻 +🀜🏻‍♀ +🀜🏻‍♂ +🀜🏌 +🀜🏌‍♀ +🀜🏌‍♂ +🀜🏜 +🀜🏜‍♀ +🀜🏜‍♂ +🀜🏟 +🀜🏟‍♀ +🀜🏟‍♂ +🀜🏿 +🀜🏿‍♀ +🀜🏿‍♂ +🀟 +🀟‍♀ +🀟‍♂ +🀟🏻 +🀟🏻‍♀ +🀟🏻‍♂ +🀟🏌 +🀟🏌‍♀ +🀟🏌‍♂ +🀟🏜 +🀟🏜‍♀ +🀟🏜‍♂ +🀟🏟 +🀟🏟‍♀ +🀟🏟‍♂ +🀟🏿 +🀟🏿‍♀ +🀟🏿‍♂ +🀿 +🥀 +🥁 +🥂 +🥃 +🥄 +🥅 +🥇 +🥈 +🥉 +🥊 +🥋 +🥌 +🥍 +🥎 +🥏 +🥐 +🥑 +🥒 +🥓 +🥔 +🥕 +🥖 +🥗 +🥘 +🥙 +🥚 +🥛 +🥜 +🥝 +🥞 +🥟 +🥠 +🥡 +🥢 +🥣 +🥀 +🥥 +🥊 +🥧 +🥚 +🥩 +🥪 +🥫 +🥬 +🥭 +🥮 +🥯 +🥰 +🥱 +🥲 +🥳 +🥎 +🥵 +🥶 +🥷 +🥷🏻 +🥷🏌 +🥷🏜 +🥷🏟 +🥷🏿 +🥞 +🥹 +🥺 +🥻 +🥌 +🥜 +🥟 +🥿 +🊀 +🊁 +🊂 +🊃 +🊄 +🊅 +🊆 +🊇 +🊈 +🊉 +🊊 +🊋 +🊌 +🊍 +🊎 +🊏 +🊐 +🊑 +🊒 +🊓 +🊔 +🊕 +🊖 +🊗 +🊘 +🊙 +🊚 +🊛 +🊜 +🊝 +🊞 +🊟 +🊠 +🊡 +🊢 +🊣 +🊀 +🊥 +🊊 +🊧 +🊚 +🊩 +🊪 +🊫 +🊬 +🊭 +🊮 +🊯 +🊎 +🊵 +🊵🏻 +🊵🏌 +🊵🏜 +🊵🏟 +🊵🏿 +🊶 +🊶🏻 +🊶🏌 +🊶🏜 +🊶🏟 +🊶🏿 +🊷 +🊞 +🊞‍♀ +🊞‍♂ +🊞🏻 +🊞🏻‍♀ +🊞🏻‍♂ +🊞🏌 +🊞🏌‍♀ +🊞🏌‍♂ +🊞🏜 +🊞🏜‍♀ +🊞🏜‍♂ +🊞🏟 +🊞🏟‍♀ +🊞🏟‍♂ +🊞🏿 +🊞🏿‍♀ +🊞🏿‍♂ +🊹 +🊹‍♀ +🊹‍♂ +🊹🏻 +🊹🏻‍♀ +🊹🏻‍♂ +🊹🏌 +🊹🏌‍♀ +🊹🏌‍♂ +🊹🏜 +🊹🏜‍♀ +🊹🏜‍♂ +🊹🏟 +🊹🏟‍♀ +🊹🏟‍♂ +🊹🏿 +🊹🏿‍♀ +🊹🏿‍♂ +🊺 +🊻 +🊻🏻 +🊻🏌 +🊻🏜 +🊻🏟 +🊻🏿 +🊌 +🊜 +🊟 +🊿 +🧀 +🧁 +🧂 +🧃 +🧄 +🧅 +🧆 +🧇 +🧈 +🧉 +🧊 +🧋 +🧌 +🧍 +🧍‍♀ +🧍‍♂ +🧍🏻 +🧍🏻‍♀ +🧍🏻‍♂ +🧍🏌 +🧍🏌‍♀ +🧍🏌‍♂ +🧍🏜 +🧍🏜‍♀ +🧍🏜‍♂ +🧍🏟 +🧍🏟‍♀ +🧍🏟‍♂ +🧍🏿 +🧍🏿‍♀ +🧍🏿‍♂ +🧎 +🧎‍♀ +🧎‍♀‍➡ +🧎‍♂ +🧎‍♂‍➡ +🧎‍➡ +🧎🏻 +🧎🏻‍♀ +🧎🏻‍♀‍➡ +🧎🏻‍♂ +🧎🏻‍♂‍➡ +🧎🏻‍➡ +🧎🏌 +🧎🏌‍♀ +🧎🏌‍♀‍➡ +🧎🏌‍♂ +🧎🏌‍♂‍➡ +🧎🏌‍➡ +🧎🏜 +🧎🏜‍♀ +🧎🏜‍♀‍➡ +🧎🏜‍♂ +🧎🏜‍♂‍➡ +🧎🏜‍➡ +🧎🏟 +🧎🏟‍♀ +🧎🏟‍♀‍➡ +🧎🏟‍♂ +🧎🏟‍♂‍➡ +🧎🏟‍➡ +🧎🏿 +🧎🏿‍♀ +🧎🏿‍♀‍➡ +🧎🏿‍♂ +🧎🏿‍♂‍➡ +🧎🏿‍➡ +🧏 +🧏‍♀ +🧏‍♂ +🧏🏻 +🧏🏻‍♀ +🧏🏻‍♂ +🧏🏌 +🧏🏌‍♀ +🧏🏌‍♂ +🧏🏜 +🧏🏜‍♀ +🧏🏜‍♂ +🧏🏟 +🧏🏟‍♀ +🧏🏟‍♂ +🧏🏿 +🧏🏿‍♀ +🧏🏿‍♂ +🧐 +🧑 +🧑‍⚕ +🧑‍⚖ +🧑‍✈ +🧑‍🌟 +🧑‍🍳 +🧑‍🍌 +🧑‍🎄 +🧑‍🎓 +🧑‍🎀 +🧑‍🎚 +🧑‍🏫 +🧑‍🏭 +🧑‍💻 +🧑‍💌 +🧑‍🔧 +🧑‍🔬 +🧑‍🚀 +🧑‍🚒 +🧑‍🀝‍🧑 +🧑‍🊯 +🧑‍🊯‍➡ +🧑‍🊰 +🧑‍🊱 +🧑‍🊲 +🧑‍🊳 +🧑‍🊌 +🧑‍🊌‍➡ +🧑‍🊜 +🧑‍🊜‍➡ +🧑‍🧑‍🧒 +🧑‍🧑‍🧒‍🧒 +🧑‍🧒 +🧑‍🧒‍🧒 +🧑‍🩰 +🧑🏻 +🧑🏻‍⚕ +🧑🏻‍⚖ +🧑🏻‍✈ +🧑🏻‍❀‍💋‍🧑🏌 +🧑🏻‍❀‍💋‍🧑🏜 +🧑🏻‍❀‍💋‍🧑🏟 +🧑🏻‍❀‍💋‍🧑🏿 +🧑🏻‍❀‍🧑🏌 +🧑🏻‍❀‍🧑🏜 +🧑🏻‍❀‍🧑🏟 +🧑🏻‍❀‍🧑🏿 +🧑🏻‍🌟 +🧑🏻‍🍳 +🧑🏻‍🍌 +🧑🏻‍🎄 +🧑🏻‍🎓 +🧑🏻‍🎀 +🧑🏻‍🎚 +🧑🏻‍🏫 +🧑🏻‍🏭 +🧑🏻‍🐰‍🧑🏌 +🧑🏻‍🐰‍🧑🏜 +🧑🏻‍🐰‍🧑🏟 +🧑🏻‍🐰‍🧑🏿 +🧑🏻‍💻 +🧑🏻‍💌 +🧑🏻‍🔧 +🧑🏻‍🔬 +🧑🏻‍🚀 +🧑🏻‍🚒 +🧑🏻‍🀝‍🧑🏻 +🧑🏻‍🀝‍🧑🏌 +🧑🏻‍🀝‍🧑🏜 +🧑🏻‍🀝‍🧑🏟 +🧑🏻‍🀝‍🧑🏿 +🧑🏻‍🊯 +🧑🏻‍🊯‍➡ +🧑🏻‍🊰 +🧑🏻‍🊱 +🧑🏻‍🊲 +🧑🏻‍🊳 +🧑🏻‍🊌 +🧑🏻‍🊌‍➡ +🧑🏻‍🊜 +🧑🏻‍🊜‍➡ +🧑🏻‍🩰 +🧑🏻‍🫯‍🧑🏌 +🧑🏻‍🫯‍🧑🏜 +🧑🏻‍🫯‍🧑🏟 +🧑🏻‍🫯‍🧑🏿 +🧑🏌 +🧑🏌‍⚕ +🧑🏌‍⚖ +🧑🏌‍✈ +🧑🏌‍❀‍💋‍🧑🏻 +🧑🏌‍❀‍💋‍🧑🏜 +🧑🏌‍❀‍💋‍🧑🏟 +🧑🏌‍❀‍💋‍🧑🏿 +🧑🏌‍❀‍🧑🏻 +🧑🏌‍❀‍🧑🏜 +🧑🏌‍❀‍🧑🏟 +🧑🏌‍❀‍🧑🏿 +🧑🏌‍🌟 +🧑🏌‍🍳 +🧑🏌‍🍌 +🧑🏌‍🎄 +🧑🏌‍🎓 +🧑🏌‍🎀 +🧑🏌‍🎚 +🧑🏌‍🏫 +🧑🏌‍🏭 +🧑🏌‍🐰‍🧑🏻 +🧑🏌‍🐰‍🧑🏜 +🧑🏌‍🐰‍🧑🏟 +🧑🏌‍🐰‍🧑🏿 +🧑🏌‍💻 +🧑🏌‍💌 +🧑🏌‍🔧 +🧑🏌‍🔬 +🧑🏌‍🚀 +🧑🏌‍🚒 +🧑🏌‍🀝‍🧑🏻 +🧑🏌‍🀝‍🧑🏌 +🧑🏌‍🀝‍🧑🏜 +🧑🏌‍🀝‍🧑🏟 +🧑🏌‍🀝‍🧑🏿 +🧑🏌‍🊯 +🧑🏌‍🊯‍➡ +🧑🏌‍🊰 +🧑🏌‍🊱 +🧑🏌‍🊲 +🧑🏌‍🊳 +🧑🏌‍🊌 +🧑🏌‍🊌‍➡ +🧑🏌‍🊜 +🧑🏌‍🊜‍➡ +🧑🏌‍🩰 +🧑🏌‍🫯‍🧑🏻 +🧑🏌‍🫯‍🧑🏜 +🧑🏌‍🫯‍🧑🏟 +🧑🏌‍🫯‍🧑🏿 +🧑🏜 +🧑🏜‍⚕ +🧑🏜‍⚖ +🧑🏜‍✈ +🧑🏜‍❀‍💋‍🧑🏻 +🧑🏜‍❀‍💋‍🧑🏌 +🧑🏜‍❀‍💋‍🧑🏟 +🧑🏜‍❀‍💋‍🧑🏿 +🧑🏜‍❀‍🧑🏻 +🧑🏜‍❀‍🧑🏌 +🧑🏜‍❀‍🧑🏟 +🧑🏜‍❀‍🧑🏿 +🧑🏜‍🌟 +🧑🏜‍🍳 +🧑🏜‍🍌 +🧑🏜‍🎄 +🧑🏜‍🎓 +🧑🏜‍🎀 +🧑🏜‍🎚 +🧑🏜‍🏫 +🧑🏜‍🏭 +🧑🏜‍🐰‍🧑🏻 +🧑🏜‍🐰‍🧑🏌 +🧑🏜‍🐰‍🧑🏟 +🧑🏜‍🐰‍🧑🏿 +🧑🏜‍💻 +🧑🏜‍💌 +🧑🏜‍🔧 +🧑🏜‍🔬 +🧑🏜‍🚀 +🧑🏜‍🚒 +🧑🏜‍🀝‍🧑🏻 +🧑🏜‍🀝‍🧑🏌 +🧑🏜‍🀝‍🧑🏜 +🧑🏜‍🀝‍🧑🏟 +🧑🏜‍🀝‍🧑🏿 +🧑🏜‍🊯 +🧑🏜‍🊯‍➡ +🧑🏜‍🊰 +🧑🏜‍🊱 +🧑🏜‍🊲 +🧑🏜‍🊳 +🧑🏜‍🊌 +🧑🏜‍🊌‍➡ +🧑🏜‍🊜 +🧑🏜‍🊜‍➡ +🧑🏜‍🩰 +🧑🏜‍🫯‍🧑🏻 +🧑🏜‍🫯‍🧑🏌 +🧑🏜‍🫯‍🧑🏟 +🧑🏜‍🫯‍🧑🏿 +🧑🏟 +🧑🏟‍⚕ +🧑🏟‍⚖ +🧑🏟‍✈ +🧑🏟‍❀‍💋‍🧑🏻 +🧑🏟‍❀‍💋‍🧑🏌 +🧑🏟‍❀‍💋‍🧑🏜 +🧑🏟‍❀‍💋‍🧑🏿 +🧑🏟‍❀‍🧑🏻 +🧑🏟‍❀‍🧑🏌 +🧑🏟‍❀‍🧑🏜 +🧑🏟‍❀‍🧑🏿 +🧑🏟‍🌟 +🧑🏟‍🍳 +🧑🏟‍🍌 +🧑🏟‍🎄 +🧑🏟‍🎓 +🧑🏟‍🎀 +🧑🏟‍🎚 +🧑🏟‍🏫 +🧑🏟‍🏭 +🧑🏟‍🐰‍🧑🏻 +🧑🏟‍🐰‍🧑🏌 +🧑🏟‍🐰‍🧑🏜 +🧑🏟‍🐰‍🧑🏿 +🧑🏟‍💻 +🧑🏟‍💌 +🧑🏟‍🔧 +🧑🏟‍🔬 +🧑🏟‍🚀 +🧑🏟‍🚒 +🧑🏟‍🀝‍🧑🏻 +🧑🏟‍🀝‍🧑🏌 +🧑🏟‍🀝‍🧑🏜 +🧑🏟‍🀝‍🧑🏟 +🧑🏟‍🀝‍🧑🏿 +🧑🏟‍🊯 +🧑🏟‍🊯‍➡ +🧑🏟‍🊰 +🧑🏟‍🊱 +🧑🏟‍🊲 +🧑🏟‍🊳 +🧑🏟‍🊌 +🧑🏟‍🊌‍➡ +🧑🏟‍🊜 +🧑🏟‍🊜‍➡ +🧑🏟‍🩰 +🧑🏟‍🫯‍🧑🏻 +🧑🏟‍🫯‍🧑🏌 +🧑🏟‍🫯‍🧑🏜 +🧑🏟‍🫯‍🧑🏿 +🧑🏿 +🧑🏿‍⚕ +🧑🏿‍⚖ +🧑🏿‍✈ +🧑🏿‍❀‍💋‍🧑🏻 +🧑🏿‍❀‍💋‍🧑🏌 +🧑🏿‍❀‍💋‍🧑🏜 +🧑🏿‍❀‍💋‍🧑🏟 +🧑🏿‍❀‍🧑🏻 +🧑🏿‍❀‍🧑🏌 +🧑🏿‍❀‍🧑🏜 +🧑🏿‍❀‍🧑🏟 +🧑🏿‍🌟 +🧑🏿‍🍳 +🧑🏿‍🍌 +🧑🏿‍🎄 +🧑🏿‍🎓 +🧑🏿‍🎀 +🧑🏿‍🎚 +🧑🏿‍🏫 +🧑🏿‍🏭 +🧑🏿‍🐰‍🧑🏻 +🧑🏿‍🐰‍🧑🏌 +🧑🏿‍🐰‍🧑🏜 +🧑🏿‍🐰‍🧑🏟 +🧑🏿‍💻 +🧑🏿‍💌 +🧑🏿‍🔧 +🧑🏿‍🔬 +🧑🏿‍🚀 +🧑🏿‍🚒 +🧑🏿‍🀝‍🧑🏻 +🧑🏿‍🀝‍🧑🏌 +🧑🏿‍🀝‍🧑🏜 +🧑🏿‍🀝‍🧑🏟 +🧑🏿‍🀝‍🧑🏿 +🧑🏿‍🊯 +🧑🏿‍🊯‍➡ +🧑🏿‍🊰 +🧑🏿‍🊱 +🧑🏿‍🊲 +🧑🏿‍🊳 +🧑🏿‍🊌 +🧑🏿‍🊌‍➡ +🧑🏿‍🊜 +🧑🏿‍🊜‍➡ +🧑🏿‍🩰 +🧑🏿‍🫯‍🧑🏻 +🧑🏿‍🫯‍🧑🏌 +🧑🏿‍🫯‍🧑🏜 +🧑🏿‍🫯‍🧑🏟 +🧒 +🧒🏻 +🧒🏌 +🧒🏜 +🧒🏟 +🧒🏿 +🧓 +🧓🏻 +🧓🏌 +🧓🏜 +🧓🏟 +🧓🏿 +🧔 +🧔‍♀ +🧔‍♂ +🧔🏻 +🧔🏻‍♀ +🧔🏻‍♂ +🧔🏌 +🧔🏌‍♀ +🧔🏌‍♂ +🧔🏜 +🧔🏜‍♀ +🧔🏜‍♂ +🧔🏟 +🧔🏟‍♀ +🧔🏟‍♂ +🧔🏿 +🧔🏿‍♀ +🧔🏿‍♂ +🧕 +🧕🏻 +🧕🏌 +🧕🏜 +🧕🏟 +🧕🏿 +🧖 +🧖‍♀ +🧖‍♂ +🧖🏻 +🧖🏻‍♀ +🧖🏻‍♂ +🧖🏌 +🧖🏌‍♀ +🧖🏌‍♂ +🧖🏜 +🧖🏜‍♀ +🧖🏜‍♂ +🧖🏟 +🧖🏟‍♀ +🧖🏟‍♂ +🧖🏿 +🧖🏿‍♀ +🧖🏿‍♂ +🧗 +🧗‍♀ +🧗‍♂ +🧗🏻 +🧗🏻‍♀ +🧗🏻‍♂ +🧗🏌 +🧗🏌‍♀ +🧗🏌‍♂ +🧗🏜 +🧗🏜‍♀ +🧗🏜‍♂ +🧗🏟 +🧗🏟‍♀ +🧗🏟‍♂ +🧗🏿 +🧗🏿‍♀ +🧗🏿‍♂ +🧘 +🧘‍♀ +🧘‍♂ +🧘🏻 +🧘🏻‍♀ +🧘🏻‍♂ +🧘🏌 +🧘🏌‍♀ +🧘🏌‍♂ +🧘🏜 +🧘🏜‍♀ +🧘🏜‍♂ +🧘🏟 +🧘🏟‍♀ +🧘🏟‍♂ +🧘🏿 +🧘🏿‍♀ +🧘🏿‍♂ +🧙 +🧙‍♀ +🧙‍♂ +🧙🏻 +🧙🏻‍♀ +🧙🏻‍♂ +🧙🏌 +🧙🏌‍♀ +🧙🏌‍♂ +🧙🏜 +🧙🏜‍♀ +🧙🏜‍♂ +🧙🏟 +🧙🏟‍♀ +🧙🏟‍♂ +🧙🏿 +🧙🏿‍♀ +🧙🏿‍♂ +🧚 +🧚‍♀ +🧚‍♂ +🧚🏻 +🧚🏻‍♀ +🧚🏻‍♂ +🧚🏌 +🧚🏌‍♀ +🧚🏌‍♂ +🧚🏜 +🧚🏜‍♀ +🧚🏜‍♂ +🧚🏟 +🧚🏟‍♀ +🧚🏟‍♂ +🧚🏿 +🧚🏿‍♀ +🧚🏿‍♂ +🧛 +🧛‍♀ +🧛‍♂ +🧛🏻 +🧛🏻‍♀ +🧛🏻‍♂ +🧛🏌 +🧛🏌‍♀ +🧛🏌‍♂ +🧛🏜 +🧛🏜‍♀ +🧛🏜‍♂ +🧛🏟 +🧛🏟‍♀ +🧛🏟‍♂ +🧛🏿 +🧛🏿‍♀ +🧛🏿‍♂ +🧜 +🧜‍♀ +🧜‍♂ +🧜🏻 +🧜🏻‍♀ +🧜🏻‍♂ +🧜🏌 +🧜🏌‍♀ +🧜🏌‍♂ +🧜🏜 +🧜🏜‍♀ +🧜🏜‍♂ +🧜🏟 +🧜🏟‍♀ +🧜🏟‍♂ +🧜🏿 +🧜🏿‍♀ +🧜🏿‍♂ +🧝 +🧝‍♀ +🧝‍♂ +🧝🏻 +🧝🏻‍♀ +🧝🏻‍♂ +🧝🏌 +🧝🏌‍♀ +🧝🏌‍♂ +🧝🏜 +🧝🏜‍♀ +🧝🏜‍♂ +🧝🏟 +🧝🏟‍♀ +🧝🏟‍♂ +🧝🏿 +🧝🏿‍♀ +🧝🏿‍♂ +🧞 +🧞‍♀ +🧞‍♂ +🧟 +🧟‍♀ +🧟‍♂ +🧠 +🧡 +🧢 +🧣 +🧀 +🧥 +🧊 +🧧 +🧚 +🧩 +🧪 +🧫 +🧬 +🧭 +🧮 +🧯 +🧰 +🧱 +🧲 +🧳 +🧎 +🧵 +🧶 +🧷 +🧞 +🧹 +🧺 +🧻 +🧌 +🧜 +🧟 +🧿 +🩰 +🩱 +🩲 +🩳 +🩎 +🩵 +🩶 +🩷 +🩞 +🩹 +🩺 +🩻 +🩌 +🪀 +🪁 +🪂 +🪃 +🪄 +🪅 +🪆 +🪇 +🪈 +🪉 +🪊 +🪎 +🪏 +🪐 +🪑 +🪒 +🪓 +🪔 +🪕 +🪖 +🪗 +🪘 +🪙 +🪚 +🪛 +🪜 +🪝 +🪞 +🪟 +🪠 +🪡 +🪢 +🪣 +🪀 +🪥 +🪊 +🪧 +🪚 +🪩 +🪪 +🪫 +🪬 +🪭 +🪮 +🪯 +🪰 +🪱 +🪲 +🪳 +🪎 +🪵 +🪶 +🪷 +🪞 +🪹 +🪺 +🪻 +🪌 +🪜 +🪟 +🪿 +🫀 +🫁 +🫂 +🫃 +🫃🏻 +🫃🏌 +🫃🏜 +🫃🏟 +🫃🏿 +🫄 +🫄🏻 +🫄🏌 +🫄🏜 +🫄🏟 +🫄🏿 +🫅 +🫅🏻 +🫅🏌 +🫅🏜 +🫅🏟 +🫅🏿 +🫆 +🫈 +🫍 +🫎 +🫏 +🫐 +🫑 +🫒 +🫓 +🫔 +🫕 +🫖 +🫗 +🫘 +🫙 +🫚 +🫛 +🫜 +🫟 +🫠 +🫡 +🫢 +🫣 +🫀 +🫥 +🫊 +🫧 +🫚 +🫩 +🫪 +🫯 +🫰 +🫰🏻 +🫰🏌 +🫰🏜 +🫰🏟 +🫰🏿 +🫱 +🫱🏻 +🫱🏻‍🫲🏌 +🫱🏻‍🫲🏜 +🫱🏻‍🫲🏟 +🫱🏻‍🫲🏿 +🫱🏌 +🫱🏌‍🫲🏻 +🫱🏌‍🫲🏜 +🫱🏌‍🫲🏟 +🫱🏌‍🫲🏿 +🫱🏜 +🫱🏜‍🫲🏻 +🫱🏜‍🫲🏌 +🫱🏜‍🫲🏟 +🫱🏜‍🫲🏿 +🫱🏟 +🫱🏟‍🫲🏻 +🫱🏟‍🫲🏌 +🫱🏟‍🫲🏜 +🫱🏟‍🫲🏿 +🫱🏿 +🫱🏿‍🫲🏻 +🫱🏿‍🫲🏌 +🫱🏿‍🫲🏜 +🫱🏿‍🫲🏟 +🫲 +🫲🏻 +🫲🏌 +🫲🏜 +🫲🏟 +🫲🏿 +🫳 +🫳🏻 +🫳🏌 +🫳🏜 +🫳🏟 +🫳🏿 +🫎 +🫎🏻 +🫎🏌 +🫎🏜 +🫎🏟 +🫎🏿 +🫵 +🫵🏻 +🫵🏌 +🫵🏜 +🫵🏟 +🫵🏿 +🫶 +🫶🏻 +🫶🏌 +🫶🏜 +🫶🏟 +🫶🏿 +🫷 +🫷🏻 +🫷🏌 +🫷🏜 +🫷🏟 +🫷🏿 +🫞 +🫞🏻 +🫞🏌 +🫞🏜 +🫞🏟 +🫞🏿 \ No newline at end of file From 9fd7128f800badbd184baf943d4f799e601201e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Sat, 9 May 2026 00:38:57 +0300 Subject: [PATCH 07/39] fix: encode filenames in redirects (#737) * fix: encode filenames in redirects Signed-off-by: ispik * refactor: don't add another dependency when the one needed exists Signed-off-by: ispik --------- Signed-off-by: ispik --- Cargo.lock | 1 + crates/services/autumn/Cargo.toml | 2 +- crates/services/autumn/src/api.rs | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f452c4d..03b3406f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7002,6 +7002,7 @@ dependencies = [ "tracing", "tracing-subscriber", "ulid 1.2.1", + "url-escape", "utoipa", "utoipa-scalar", "webp", diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index 762c4ee3..1636a8ec 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -31,7 +31,7 @@ imagesize = { workspace = true } # Utility lazy_static = { workspace = true } moka = { workspace = true, features = ["future"] } - +url-escape = { workspace = true } # Serialisation strum_macros = { workspace = true } serde_json = { workspace = true } diff --git a/crates/services/autumn/src/api.rs b/crates/services/autumn/src/api.rs index 49a61c43..c0e45d72 100644 --- a/crates/services/autumn/src/api.rs +++ b/crates/services/autumn/src/api.rs @@ -24,6 +24,7 @@ use sha2::Digest; use tempfile::NamedTempFile; use tokio::time::Instant; use tower_http::cors::{AllowHeaders, Any, CorsLayer}; +use url_escape::encode_component; use utoipa::ToSchema; use crate::{ @@ -479,8 +480,10 @@ async fn fetch_file( // Ensure filename is correct if file_name != file.filename { if file_name == "original" { + let safe_filename = encode_component(&file.filename); + return Ok( - Redirect::permanent(&format!("/{tag}/{file_id}/{}", file.filename)).into_response(), + Redirect::permanent(&format!("/{tag}/{file_id}/{}", safe_filename)).into_response(), ); } From ab5bd47a39ee889de0b5ae6e7b560620853daead Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 8 May 2026 14:39:16 -0700 Subject: [PATCH 08/39] feat: Rewrite acks (#741) * feat: rewrite ack system Signed-off-by: IAmTomahawkx * feat: rewrite acks to crond + rabbit task * fix: review changes --------- Signed-off-by: IAmTomahawkx --- Cargo.lock | 638 +++++++++++++++++- Cargo.toml | 2 + compose.yml | 14 +- crates/core/config/Revolt.toml | 4 + crates/core/config/src/lib.rs | 7 + crates/core/database/Cargo.toml | 1 + crates/core/database/src/amqp/amqp.rs | 83 ++- crates/core/database/src/events/rabbit.rs | 8 + .../database/src/models/channels/model.rs | 19 +- crates/core/database/src/tasks/ack.rs | 10 +- crates/core/database/src/util/acker.rs | 77 +++ crates/core/database/src/util/mod.rs | 1 + crates/daemons/crond/Cargo.toml | 14 + crates/daemons/crond/src/main.rs | 5 +- crates/daemons/crond/src/tasks/acks.rs | 129 ++++ crates/daemons/crond/src/tasks/mod.rs | 1 + crates/delta/src/main.rs | 22 +- .../delta/src/routes/channels/channel_ack.rs | 5 +- crates/delta/src/routes/servers/server_ack.rs | 16 +- 19 files changed, 993 insertions(+), 63 deletions(-) create mode 100644 crates/core/database/src/util/acker.rs create mode 100644 crates/daemons/crond/src/tasks/acks.rs diff --git a/Cargo.lock b/Cargo.lock index 03b3406f..671d6a9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -124,6 +124,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "amq-protocol" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b2e8843d88d935a75cbdb1c5b9ad80987da6e6472c2703914e41dc14560990" +dependencies = [ + "amq-protocol-tcp", + "amq-protocol-types", + "amq-protocol-uri", + "cookie-factory", + "nom 8.0.0", + "serde", +] + +[[package]] +name = "amq-protocol-tcp" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998fb81655e11de5a336bb609042c11633678fc01f90b0772fb9a7886b6cc4c2" +dependencies = [ + "amq-protocol-uri", + "async-rs", + "cfg-if", + "tcp-stream", + "tracing", +] + +[[package]] +name = "amq-protocol-types" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee5b3a9e458bd2e452536995c8cf861b01c17283f55c4bbe0a1b9626b8253add" +dependencies = [ + "cookie-factory", + "nom 8.0.0", + "serde", + "serde_json", +] + +[[package]] +name = "amq-protocol-uri" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca3316970d20cdcca9123f4e8feb7a2c1c8fdca572a9692fc10002db35407aa" +dependencies = [ + "amq-protocol-types", + "percent-encoding", + "url", +] + [[package]] name = "amqp_serde" version = "0.4.3" @@ -211,6 +261,45 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 2.0.117", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -244,6 +333,19 @@ dependencies = [ "pin-project-lite 0.2.17", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite 0.2.17", + "tokio 1.51.0", +] + [[package]] name = "async-executor" version = "1.14.0" @@ -275,6 +377,20 @@ dependencies = [ "tokio 1.51.0", ] +[[package]] +name = "async-global-executor" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f937e26114b93193065fd44f507aa2e9169ad0cdabbb996920b1fe1ddea7ba" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-lock 3.4.2", + "blocking", + "futures-lite", + "tokio 1.51.0", +] + [[package]] name = "async-io" version = "2.6.0" @@ -342,6 +458,22 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-rs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e32bd31386d41d0c06bd79b0397ec96e544d69d9dbd6db0236c7ceefe1ad61b" +dependencies = [ + "async-compat", + "async-global-executor 3.1.0", + "async-trait", + "futures-core", + "futures-io", + "hickory-resolver 0.26.1", + "tokio 1.51.0", + "tokio-stream", +] + [[package]] name = "async-signal" version = "0.2.13" @@ -368,7 +500,7 @@ checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ "async-attributes", "async-channel 1.9.0", - "async-global-executor", + "async-global-executor 2.4.1", "async-io", "async-lock 3.4.2", "async-process", @@ -1130,6 +1262,15 @@ dependencies = [ "ubyte", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand 2.4.0", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -1289,6 +1430,15 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "block2" version = "0.6.2" @@ -1441,6 +1591,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.59" @@ -1482,6 +1641,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -1514,6 +1684,18 @@ dependencies = [ "cc", ] +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "x509-cert", +] + [[package]] name = "coarsetime" version = "0.1.37" @@ -1699,6 +1881,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -1907,7 +2098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -2128,7 +2319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" dependencies = [ "const-oid 0.6.2", - "der_derive", + "der_derive 0.4.1", ] [[package]] @@ -2149,10 +2340,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", + "der_derive 0.7.3", + "flagset", "pem-rfc7468 0.7.0", "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der_derive" version = "0.4.1" @@ -2165,6 +2372,17 @@ dependencies = [ "synstructure 0.12.6", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "deranged" version = "0.5.8" @@ -2231,6 +2449,15 @@ dependencies = [ "unicode-xid 0.2.6", ] +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + [[package]] name = "devise" version = "0.4.2" @@ -2758,6 +2985,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.9" @@ -2777,6 +3010,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2973,6 +3217,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls 0.23.37", + "rustls-pki-types", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -3096,6 +3351,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -3362,6 +3618,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto 0.26.1", + "idna 1.1.0", + "ipnet", + "jni 0.22.4", + "rand 0.10.1", + "thiserror 2.0.18", + "tinyvec", + "tokio 1.51.0", + "tracing", + "url", +] + [[package]] name = "hickory-proto" version = "0.25.2" @@ -3387,6 +3667,26 @@ dependencies = [ "url", ] +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna 1.1.0", + "ipnet", + "jni 0.22.4", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + [[package]] name = "hickory-resolver" version = "0.25.2" @@ -3395,7 +3695,7 @@ checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", - "hickory-proto", + "hickory-proto 0.25.2", "ipconfig", "moka", "once_cell", @@ -3408,6 +3708,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto 0.26.1", + "ipconfig", + "ipnet", + "jni 0.22.4", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.1", + "resolv-conf", + "smallvec", + "system-configuration 0.7.0", + "thiserror 2.0.18", + "tokio 1.51.0", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -3977,6 +4303,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array 0.14.7", ] @@ -4018,6 +4345,9 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -4129,6 +4459,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -4460,6 +4820,24 @@ dependencies = [ "log", ] +[[package]] +name = "lapin" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478790661081f7434e111a31953acc1cf23654cf2a2d815d0e12cef9a2aed720" +dependencies = [ + "amq-protocol", + "async-rs", + "async-trait", + "atomic-waker", + "backon", + "cfg-if", + "flume", + "futures-core", + "futures-io", + "tracing", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -5008,8 +5386,8 @@ dependencies = [ "futures-io", "futures-util", "hex", - "hickory-proto", - "hickory-resolver", + "hickory-proto 0.25.2", + "hickory-resolver 0.25.2", "hmac", "macro_magic", "md-5", @@ -5118,6 +5496,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -5471,6 +5855,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -5575,6 +5968,29 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "p12-keystore" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb9bf5222606eb712d3bb30e01bc9420545b00859970897e70c682353a034f2" +dependencies = [ + "base64 0.22.1", + "cbc", + "cms", + "der 0.7.10", + "des", + "hex", + "hmac", + "pkcs12", + "pkcs5", + "rand 0.10.1", + "rc2", + "sha1", + "sha2", + "thiserror 2.0.18", + "x509-parser", +] + [[package]] name = "p256" version = "0.11.1" @@ -5701,6 +6117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", + "hmac", ] [[package]] @@ -6092,6 +6509,36 @@ dependencies = [ "spki 0.7.3", ] +[[package]] +name = "pkcs12" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b3df3d3cc1015f12d70235e35b6b79befc5fa7a9b95b951eab1dd07c9efc2" +dependencies = [ + "cms", + "const-oid 0.9.6", + "der 0.7.10", + "digest", + "spki 0.7.3", + "x509-cert", + "zeroize", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der 0.7.10", + "pbkdf2", + "scrypt", + "sha2", + "spki 0.7.3", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -6165,7 +6612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -6206,6 +6653,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prefix-trie" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_env_logger" version = "0.4.0" @@ -6609,6 +7067,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -6647,6 +7116,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rav1e" version = "0.8.1" @@ -6717,6 +7192,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" +dependencies = [ + "cipher", +] + [[package]] name = "redis" version = "0.23.3" @@ -6929,7 +7413,7 @@ dependencies = [ "quinn", "rustls 0.23.37", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.6.2", "serde", "serde_json", "serde_urlencoded", @@ -7069,11 +7553,19 @@ dependencies = [ name = "revolt-crond" version = "0.12.1" dependencies = [ + "futures-lite", + "iso8601-timestamp", + "lapin", "log", + "redis-kiss", "revolt-config", "revolt-database", "revolt-files", + "revolt-permissions", "revolt-result", + "revolt_optional_struct", + "serde", + "serde_json", "tokio 1.51.0", ] @@ -7108,6 +7600,7 @@ dependencies = [ "rand 0.8.5", "redis-kiss", "regex", + "revolt-coalesced", "revolt-config", "revolt-models", "revolt-parser", @@ -7793,6 +8286,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "0.38.44" @@ -7861,6 +8363,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-connector" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26bcb6901a3319d57589047c0da93a0f3228f13abf8dd949deef024749cb5e2" +dependencies = [ + "futures-io", + "futures-rustls", + "log", + "rustls 0.23.37", + "rustls-pki-types", + "rustls-platform-verifier 0.7.0", + "rustls-webpki 0.103.10", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -7921,7 +8438,28 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", + "log", + "once_cell", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.10", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", "log", "once_cell", "rustls 0.23.37", @@ -8003,6 +8541,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -8075,6 +8622,17 @@ dependencies = [ "tendril", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -8493,7 +9051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -8504,7 +9062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -8521,7 +9079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -8576,6 +9134,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -8679,6 +9247,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spin" @@ -8970,6 +9541,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tcp-stream" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7219422d3348cddeaf9073772997c452085c33e22d0b08cbd19652e1b16da5" +dependencies = [ + "async-rs", + "cfg-if", + "futures-io", + "p12-keystore", + "rustls-connector", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -10733,6 +11317,34 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/Cargo.toml b/Cargo.toml index f10bfb26..6ce826ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,6 +159,7 @@ authifier = "1.0.16" # RabbitMQ amqprs = "1.7.0" +lapin = "4.7.1" # Voice livekit-api = "0.4.4" @@ -184,6 +185,7 @@ url = "2.2.2" impl_ops = "0.1.1" lazy_static = "1.5.0" mime = "0.3.17" +futures-lite = "2.6.1" # Build Dependencies vergen = "7.5.0" diff --git a/compose.yml b/compose.yml index a01c965e..bb4cb8fe 100644 --- a/compose.yml +++ b/compose.yml @@ -8,10 +8,20 @@ services: # MongoDB database: image: mongo + command: mongod --replSet rs0 ports: - "27017:27017" volumes: - ./.data/db:/data/db + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'work-laptop.van-acoustic.ts.net:27017'}]}) }" | mongosh --port 27017 --quiet + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 ulimits: nofile: soft: 65536 @@ -19,8 +29,8 @@ services: # MinIO minio: - image: minio/minio - command: server /data + image: firstfinger/minio:latest + #command: server /data environment: MINIO_ROOT_USER: minioautumn MINIO_ROOT_PASSWORD: minioautumn diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index 2440bb31..ad2d6992 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -30,6 +30,10 @@ host = "rabbit" port = 5672 username = "rabbituser" password = "rabbitpass" +default_exchange = "revolt" + +[rabbit.queues] +acks = "internal.ack" [api] diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index 3dc2771a..1ba041ca 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -122,12 +122,19 @@ pub struct Database { pub redis_pubsub: Option, } +#[derive(Deserialize, Debug, Clone)] +pub struct RabbitQueues { + pub acks: String, +} + #[derive(Deserialize, Debug, Clone)] pub struct Rabbit { pub host: String, pub port: u16, pub username: String, pub password: String, + pub default_exchange: String, + pub queues: RabbitQueues, } #[derive(Deserialize, Debug, Clone)] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index ba5b7d1b..be3119c5 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -38,6 +38,7 @@ revolt-models = { workspace = true, features = ["validator"] } revolt-presence = { workspace = true } revolt-permissions = { workspace = true, features = ["serde", "bson"] } revolt-parser = { workspace = true } +revolt-coalesced = { workspace = true } # Utility log = { workspace = true } diff --git a/crates/core/database/src/amqp/amqp.rs b/crates/core/database/src/amqp/amqp.rs index b29105ed..5358bc34 100644 --- a/crates/core/database/src/amqp/amqp.rs +++ b/crates/core/database/src/amqp/amqp.rs @@ -2,7 +2,10 @@ use std::collections::HashSet; use crate::events::rabbit::*; use crate::User; -use amqprs::channel::{BasicPublishArguments, ExchangeDeclareArguments}; +use amqprs::channel::{ + BasicPublishArguments, ExchangeDeclareArguments, ExchangeType, QueueBindArguments, + QueueDeclareArguments, +}; use amqprs::connection::OpenConnectionArguments; use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError}; use amqprs::{BasicProperties, FieldTable}; @@ -55,6 +58,43 @@ impl AMQP { AMQP::new(connection, channel) } + pub async fn configure_channels(&self) -> revolt_result::Result<()> { + let config = revolt_config::config().await; + + self.channel + .exchange_declare( + ExchangeDeclareArguments::new( + &config.rabbit.default_exchange, + &ExchangeType::Topic.to_string(), + ) + .durable(true) + .finish(), + ) + .await + .expect("Failed to declare exchange"); + + // Configure acks channel & routing + self.channel + .queue_declare( + QueueDeclareArguments::new(&config.rabbit.queues.acks) + .durable(true) + .no_wait(true) + .finish(), + ) + .await + .expect("Failed to bind queue"); + + self.channel + .queue_bind(QueueBindArguments::new( + &config.rabbit.queues.acks, + &config.rabbit.default_exchange, + &config.rabbit.queues.acks, + )) + .await + .expect("Failed to bind channel"); + Ok(()) + } + pub async fn friend_request_accepted( &self, accepted_request_user: &User, @@ -232,7 +272,9 @@ impl AMQP { .await } - pub async fn ack_message( + /// # Sends an ack to pushd to update badges on iPhones. + /// Not to be confused with the process_ack function, which handles sending all acks to crond for processing. + pub async fn ack_notification_message( &self, user_id: String, channel_id: String, @@ -316,4 +358,41 @@ impl AMQP { ) .await } + + /// # Send an ack to crond for processing + pub async fn process_ack( + &self, + user_id: &str, + channel_id: Option<&str>, + server_id: Option<&str>, + ) -> Result<(), AMQPError> { + let config = revolt_config::config().await; + + let payload = AckEventPayload { + user_id: user_id.to_string(), + channel_id: channel_id.map(|value| value.to_string()), + server_id: server_id.map(|value| value.to_string()), + }; + let payload = to_string(&payload).unwrap(); + + info!( + "Sending ack processor event on exchange {}, channel {}: {}", + config.rabbit.default_exchange, config.rabbit.queues.acks, payload + ); + + self.channel + .basic_publish( + BasicProperties::default() + .with_content_type("application/json") + .with_persistence(true) + //.with_headers(headers) + .finish(), + payload.into(), + BasicPublishArguments::new( + &config.rabbit.default_exchange, + &config.rabbit.queues.acks, + ), + ) + .await + } } diff --git a/crates/core/database/src/events/rabbit.rs b/crates/core/database/src/events/rabbit.rs index 6f5c9ab3..1673f4a4 100644 --- a/crates/core/database/src/events/rabbit.rs +++ b/crates/core/database/src/events/rabbit.rs @@ -78,3 +78,11 @@ pub struct AckPayload { pub channel_id: String, pub message_id: String, } + +/// This is not the same as the AckPayload above, as the state for this event is stored in redis to allow for state updates while the event is queued. +#[derive(Serialize, Deserialize, Debug)] +pub struct AckEventPayload { + pub user_id: String, + pub channel_id: Option, + pub server_id: Option, +} diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs index 025b7934..74b56e11 100644 --- a/crates/core/database/src/models/channels/model.rs +++ b/crates/core/database/src/models/channels/model.rs @@ -1,6 +1,7 @@ #![allow(deprecated)] use std::{borrow::Cow, collections::HashMap}; +use redis_kiss::get_connection; use revolt_config::config; use revolt_models::v0::{self, MessageAuthor}; use revolt_permissions::OverrideField; @@ -212,7 +213,7 @@ impl Channel { role_permissions: HashMap::new(), nsfw: data.nsfw.unwrap_or(false), voice: data.voice.map(|voice| voice.into()), - slowmode: None + slowmode: None, }, v0::LegacyServerChannelType::Voice => Channel::TextChannel { id: id.clone(), @@ -225,7 +226,7 @@ impl Channel { role_permissions: HashMap::new(), nsfw: data.nsfw.unwrap_or(false), voice: Some(data.voice.unwrap_or_default().into()), - slowmode: None + slowmode: None, }, }; @@ -643,7 +644,7 @@ impl Channel { } /// Acknowledge a message - pub async fn ack(&self, user: &str, message: &str) -> Result<()> { + pub async fn ack(&self, user: &str, message: &str, amqp: &AMQP) -> Result<()> { EventV1::ChannelAck { id: self.id().to_string(), user: user.to_string(), @@ -652,17 +653,7 @@ impl Channel { .private(user.to_string()) .await; - #[cfg(feature = "tasks")] - crate::tasks::ack::queue_ack( - self.id().to_string(), - user.to_string(), - crate::tasks::ack::AckEvent::AckMessage { - id: message.to_string(), - }, - ) - .await; - - Ok(()) + crate::util::acker::ack_channel(user, self.id(), message, amqp).await } /// Remove user from a group diff --git a/crates/core/database/src/tasks/ack.rs b/crates/core/database/src/tasks/ack.rs index c26de03b..24e3805e 100644 --- a/crates/core/database/src/tasks/ack.rs +++ b/crates/core/database/src/tasks/ack.rs @@ -105,7 +105,11 @@ pub async fn handle_ack_event( if mentions_acked > 0 { if let Err(err) = amqp - .ack_message(user.to_string(), channel.to_string(), id.to_owned()) + .ack_notification_message( + user.to_string(), + channel.to_string(), + id.to_owned(), + ) .await { revolt_config::capture_error(&err); @@ -192,9 +196,7 @@ pub async fn handle_ack_event( .expect("Failed to fetch channel from db"); if let TextChannel { server, .. } = channel { - if let Err(err) = - amqp.mass_mention_message_sent(server, mass_mentions).await - { + if let Err(err) = amqp.mass_mention_message_sent(server, mass_mentions).await { revolt_config::capture_error(&err); } } else { diff --git a/crates/core/database/src/util/acker.rs b/crates/core/database/src/util/acker.rs new file mode 100644 index 00000000..8cc104c5 --- /dev/null +++ b/crates/core/database/src/util/acker.rs @@ -0,0 +1,77 @@ +use redis_kiss::{get_connection, AsyncCommands}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{Result, ToRevoltError}; + +use crate::{events::client::EventV1, Channel, Database, Server, User, AMQP}; + +pub async fn ack_channel(user: &str, channel: &str, message: &str, amqp: &AMQP) -> Result<()> { + let mut redis = get_connection() + .await + .map_err(|_| create_error!(InternalError))?; + + let old: Option = redis + .getset(format!("acker:{user}+{channel}"), message) + .await + .to_internal_error()?; + + if old.is_none() || old.unwrap() == message { + amqp.process_ack(user, Some(channel), None) + .await + .to_internal_error()?; + } + + Ok(()) +} + +pub async fn ack_server(user: &User, server: &Server, db: &Database, amqp: &AMQP) -> Result<()> { + let mut redis = get_connection() + .await + .map_err(|_| create_error!(InternalError))?; + + let channels = db.fetch_channels(&server.channels).await?; + let query = crate::util::permissions::DatabasePermissionQuery::new(db, user).server(server); + + for channel in channels { + let channel_id = channel.id(); + let mut q = query.clone().channel(&channel); + + if calculate_channel_permissions(&mut q) + .await + .has_channel_permission(ChannelPermission::ViewChannel) + { + let channel_last_msg = match &channel { + Channel::TextChannel { + last_message_id, .. + } => last_message_id, + _ => unreachable!(), + } + .clone(); + + if let Some(channel_last_msg) = channel_last_msg { + let old: Option = redis + .getset( + format!("acker:{}+{}", user.id, channel_id), + &channel_last_msg, + ) + .await + .to_internal_error()?; + + if old.is_none() || old.unwrap() == channel_last_msg { + amqp.process_ack(&user.id, Some(channel_id), Some(&server.id)) + .await + .to_internal_error()?; + + EventV1::ChannelAck { + id: channel_id.to_string(), + user: user.id.clone(), + message_id: channel_last_msg, + } + .private(user.id.clone()) + .await; + } + } + } + } + + Ok(()) +} diff --git a/crates/core/database/src/util/mod.rs b/crates/core/database/src/util/mod.rs index 1a03a176..ae2817ac 100644 --- a/crates/core/database/src/util/mod.rs +++ b/crates/core/database/src/util/mod.rs @@ -1,3 +1,4 @@ +pub mod acker; pub mod bridge; pub mod bulk_permissions; mod funcs; diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index fe33d2b7..6bd90052 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -16,8 +16,22 @@ log = { workspace = true } # Async tokio = { workspace = true } +# Redis +redis-kiss = { workspace = true } + +# RabbitMQ +lapin = { workspace = true } +futures-lite = { workspace = true } + +# Processing +serde_json = { workspace = true } +revolt_optional_struct = { workspace = true } +serde = { workspace = true } +iso8601-timestamp = { workspace = true, features = ["serde", "bson"] } + # Core revolt-database = { workspace = true } revolt-result = { workspace = true } revolt-config = { workspace = true } revolt-files = { workspace = true } +revolt-permissions = { workspace = true } diff --git a/crates/daemons/crond/src/main.rs b/crates/daemons/crond/src/main.rs index b232a10a..c45780d5 100644 --- a/crates/daemons/crond/src/main.rs +++ b/crates/daemons/crond/src/main.rs @@ -1,7 +1,7 @@ use revolt_config::configure; use revolt_database::DatabaseInfo; use revolt_result::Result; -use tasks::{file_deletion, prune_dangling_files, prune_members}; +use tasks::{acks, file_deletion, prune_dangling_files, prune_members}; use tokio::try_join; pub mod tasks; @@ -14,7 +14,8 @@ async fn main() -> Result<()> { try_join!( file_deletion::task(db.clone()), prune_dangling_files::task(db.clone()), - prune_members::task(db.clone()) + prune_members::task(db.clone()), + acks::task(db.clone()) ) .map(|_| ()) } diff --git a/crates/daemons/crond/src/tasks/acks.rs b/crates/daemons/crond/src/tasks/acks.rs new file mode 100644 index 00000000..68b13144 --- /dev/null +++ b/crates/daemons/crond/src/tasks/acks.rs @@ -0,0 +1,129 @@ +use futures_lite::stream::StreamExt; +use lapin::{ + options::*, + types::FieldTable, + uri::{AMQPAuthority, AMQPQueryString, AMQPUri, AMQPUserInfo}, + ConnectionBuilder, ConnectionProperties, +}; +use log::info; +use redis_kiss::{get_connection, AsyncCommands, Conn as RedisConnection}; +use revolt_config::config; +use revolt_database::{events::rabbit::AckEventPayload, Database}; +use revolt_result::{Result, ToRevoltError}; +use serde_json; + +pub async fn task(db: Database) -> Result<()> { + let config = config().await; + + let mut redis = get_connection() + .await + .expect("Failed to get redis connection"); + + let uri = AMQPUri { + scheme: lapin::uri::AMQPScheme::AMQP, + authority: AMQPAuthority { + userinfo: AMQPUserInfo { + username: config.rabbit.username, + password: config.rabbit.password, + }, + host: config.rabbit.host, + port: config.rabbit.port, + }, + vhost: "/".to_string(), + query: AMQPQueryString::default(), + }; + + let connection = ConnectionBuilder::new() + .expect("Builder") + .with_uri(uri) + .with_properties(ConnectionProperties::default()) + .connect() + .await + .expect("Failed to connect to rabbitmq"); + + let reader_channel = connection + .create_channel() + .await + .expect("Failed to create channel"); + + let mut consumer = reader_channel + .basic_consume( + config.rabbit.queues.acks.into(), + "crond-ack-consumer".into(), + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await + .expect("Failed to create consumer"); + + while let Some(delivery) = consumer.next().await { + if let Ok(delivery) = delivery { + let payload: std::result::Result = + serde_json::from_slice(&delivery.data); + if let Ok(payload) = payload { + info!("{:?}", payload); + if let Err(e) = process_channel_ack( + &db, + payload.user_id, + payload.channel_id.unwrap(), + &mut redis, + ) + .await + { + revolt_config::capture_error(&e); + _ = delivery.reject(BasicRejectOptions { requeue: false }).await; + } else { + _ = delivery.ack(BasicAckOptions { multiple: false }).await; + } + } else { + revolt_config::capture_message( + format!("Failed to decode ack data: {:?}", delivery.data).as_str(), + revolt_config::Level::Error, + ); + } + } + } + Ok(()) +} + +#[allow(clippy::disallowed_methods)] +async fn process_channel_ack( + db: &Database, + user: String, + channel: String, + redis: &mut RedisConnection, +) -> Result<()> { + let message_id: Option = redis + .get_del(format!("acker:{user}+{channel}")) + .await + .to_internal_error()?; + + if let Some(message_id) = message_id { + // This will be uncommented eventually, but we need to sort out the transition to lapin first. For now we'll simply disable the badge update logic. + // We also drop a db request as a bonus. + + //let unread = db.fetch_unread(&user, &channel).await?; + let _updated = db.acknowledge_message(&channel, &user, &message_id).await?; + info!("Set new state for ack: {}:{}:{}", channel, user, message_id); + + // if let (Some(before), Some(after)) = (unread, updated) { + // let before_mentions = before.mentions.unwrap_or_default().len(); + // let after_mentions = after.mentions.unwrap_or_default().len(); + + // let mentions_acked = before_mentions - after_mentions; + + // if mentions_acked > 0 { + // if let Err(err) = amqp + // .ack_message(user.to_string(), channel.to_string(), payload.message_id) + // .await + // { + // revolt_config::capture_error(&err); + // } + // }; + // } + + Ok(()) + } else { + Err(message_id.to_internal_error().expect_err("no err")) + } +} diff --git a/crates/daemons/crond/src/tasks/mod.rs b/crates/daemons/crond/src/tasks/mod.rs index 060f55d8..a7dd9040 100644 --- a/crates/daemons/crond/src/tasks/mod.rs +++ b/crates/daemons/crond/src/tasks/mod.rs @@ -1,3 +1,4 @@ +pub mod acks; pub mod file_deletion; pub mod prune_dangling_files; pub mod prune_members; diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 882609cc..e3522de1 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -24,8 +24,8 @@ use amqprs::{ }; use async_std::channel::unbounded; use authifier::AuthifierEvent; -use rocket::data::ToByteUnit; use revolt_database::voice::VoiceClient; +use rocket::data::ToByteUnit; pub async fn web() -> Rocket { // Get settings @@ -93,22 +93,6 @@ pub async fn web() -> Rocket { ) .into(); - let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui( - &revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { - url: "/0.8/openapi.json".to_owned(), - ..Default::default() - }, - ) - .into(); - - let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui( - &revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { - url: "/0.8/openapi.json".to_owned(), - ..Default::default() - }, - ) - .into(); - // Voice handler let voice_client = VoiceClient::new(config.api.livekit.nodes.clone()); // Configure Rabbit @@ -136,6 +120,9 @@ pub async fn web() -> Rocket { .expect("Failed to declare exchange"); let amqp = AMQP::new(connection, channel); + amqp.configure_channels() + .await + .expect("Failed to configure channels"); // Launch background task workers revolt_database::tasks::start_workers(db.clone(), amqp.clone()); @@ -153,7 +140,6 @@ pub async fn web() -> Rocket { .mount("/", rocket_cors::catch_all_options_routes()) .mount("/", ratelimiter::routes()) .mount("/swagger/", swagger) - .mount("/0.8/swagger/", swagger_0_8) .manage(authifier) .manage(db) .manage(amqp) diff --git a/crates/delta/src/routes/channels/channel_ack.rs b/crates/delta/src/routes/channels/channel_ack.rs index 2ae0d3b0..e20da642 100644 --- a/crates/delta/src/routes/channels/channel_ack.rs +++ b/crates/delta/src/routes/channels/channel_ack.rs @@ -1,6 +1,6 @@ use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, User, + Database, User, AMQP, }; use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_result::{create_error, Result}; @@ -14,6 +14,7 @@ use rocket_empty::EmptyResponse; #[put("//ack/")] pub async fn ack( db: &State, + amqp: &State, user: User, target: Reference<'_>, message: Reference<'_>, @@ -29,7 +30,7 @@ pub async fn ack( .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; channel - .ack(&user.id, message.id) + .ack(&user.id, message.id, amqp) .await .map(|_| EmptyResponse) } diff --git a/crates/delta/src/routes/servers/server_ack.rs b/crates/delta/src/routes/servers/server_ack.rs index 789617e1..81ae683e 100644 --- a/crates/delta/src/routes/servers/server_ack.rs +++ b/crates/delta/src/routes/servers/server_ack.rs @@ -1,6 +1,6 @@ use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, User, + util::{acker, permissions::DatabasePermissionQuery, reference::Reference}, + Database, User, AMQP, }; use revolt_permissions::PermissionQuery; use revolt_result::{create_error, Result}; @@ -12,7 +12,12 @@ use rocket_empty::EmptyResponse; /// Mark all channels in a server as read. #[openapi(tag = "Server Information")] #[put("//ack")] -pub async fn ack(db: &State, user: User, target: Reference<'_>) -> Result { +pub async fn ack( + db: &State, + amqp: &State, + user: User, + target: Reference<'_>, +) -> Result { if user.bot.is_some() { return Err(create_error!(IsBot)); } @@ -23,7 +28,6 @@ pub async fn ack(db: &State, user: User, target: Reference<'_>) -> Res return Err(create_error!(NotFound)); } - db.acknowledge_channels(&user.id, &server.channels) - .await - .map(|_| EmptyResponse) + acker::ack_server(&user, &server, db, amqp).await?; + Ok(EmptyResponse) } From 0719985ac5636590f91e6f9ec4b68f3eded70c13 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 8 May 2026 15:22:10 -0700 Subject: [PATCH 09/39] fix: docker compose file had personal url in it (#742) --- compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index bb4cb8fe..bf2c2ca1 100644 --- a/compose.yml +++ b/compose.yml @@ -16,7 +16,7 @@ services: extra_hosts: - "host.docker.internal:host-gateway" healthcheck: - test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'work-laptop.van-acoustic.ts.net:27017'}]}) }" | mongosh --port 27017 --quiet + test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'127.0.0.1:27017'}]}) }" | mongosh --port 27017 --quiet interval: 5s timeout: 30s start_period: 0s From d46c7f7f3c04524c0639c3e0a122626f8e0b3bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Sat, 9 May 2026 01:23:30 +0300 Subject: [PATCH 10/39] feat: add embed support for YouTube Shorts (#734) * feat: add embed support for YouTube Shorts Signed-off-by: ispik * feat: blacklist private ip ranges and add january domain blocklist Signed-off-by: IAmTomahawkx * fix: remove duplicates Signed-off-by: ispik --------- Signed-off-by: ispik Signed-off-by: IAmTomahawkx Co-authored-by: IAmTomahawkx --- crates/services/january/src/requests.rs | 10 ++++++++++ crates/services/january/src/website_embed.rs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/services/january/src/requests.rs b/crates/services/january/src/requests.rs index bec5bc1d..b63f203e 100644 --- a/crates/services/january/src/requests.rs +++ b/crates/services/january/src/requests.rs @@ -33,6 +33,9 @@ lazy_static! { /// Regex for matching new Reddit URLs static ref RE_URL_NEW_REDDIT: Regex = Regex::new("^(?:(?:new\\.|www\\.)?reddit).com").expect("valid regex"); + /// Regex for matching YouTube Shorts URLs + static ref RE_URL_YOUTUBE_SHORTS: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:www\\.)?youtube\\.com)/shorts/([a-zA-Z0-9_-]+)").expect("valid regex"); + /// Cache for proxy results static ref PROXY_CACHE: moka::future::Cache)>> = moka::future::Cache::builder() .weigher(|_key, value: &Result<(String, Vec)>| -> u32 { @@ -214,6 +217,13 @@ impl Request { .to_string(); } + // Re-map Youtube Shorts to regular Youtube links + if let Some(captures) = RE_URL_YOUTUBE_SHORTS.captures(&url) { + if let Some(video_id) = captures.get(1) { + url = format!("https://youtube.com/watch?v={}", video_id.as_str()); + } + } + // Generate the actual embed if let Some(hit) = EMBED_CACHE.get(&url).await { Ok(hit) diff --git a/crates/services/january/src/website_embed.rs b/crates/services/january/src/website_embed.rs index 6ab1af58..0e025b73 100644 --- a/crates/services/january/src/website_embed.rs +++ b/crates/services/january/src/website_embed.rs @@ -190,7 +190,7 @@ pub async fn create_website_embed(original_url: &str, document: &str) -> Option< pub async fn populate_special(original_url: String, metadata: &mut WebsiteMetadata) { lazy_static! { - static ref RE_YOUTUBE: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:www|m)\\.)?(?:(?:youtube\\.com|youtu.be))(?:/(?:[\\w\\-]+\\?v=|embed/|v/)?)([\\w\\-]+)(?:\\S+)?$").unwrap(); + static ref RE_YOUTUBE: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:www|m)\\.)?(?:(?:youtube\\.com|youtu.be))(?:/(?:[\\w\\-]+\\?v=|embed/|v/|shorts/)?)([\\w\\-]+)(?:\\S+)?$").unwrap(); static ref RE_LIGHTSPEED: Regex = Regex::new("^(?:https?://)?(?:[\\w]+\\.)?lightspeed\\.tv/([a-z0-9_]{4,25})").unwrap(); From 23ad1359834bb7d07a460b8678d6a6ebffc73eb0 Mon Sep 17 00:00:00 2001 From: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com> Date: Sat, 9 May 2026 00:23:57 +0200 Subject: [PATCH 11/39] feat: add emoji rename endpoint (#714) * feat: add rename endpoint with rename-only update Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com> * fix: enforce detached emoji edit restrictions Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com> * fix: always enforce emoji edit permissions Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com> --------- Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com> --- crates/core/database/src/events/client.rs | 8 +- .../core/database/src/models/emojis/model.rs | 27 +++ crates/core/database/src/models/emojis/ops.rs | 5 +- .../database/src/models/emojis/ops/mongodb.rs | 7 +- .../src/models/emojis/ops/reference.rs | 15 +- crates/core/models/src/v0/emojis.rs | 18 ++ .../src/routes/customisation/emoji_edit.rs | 212 ++++++++++++++++++ crates/delta/src/routes/customisation/mod.rs | 2 + docs/docs/developers/events/protocol.md | 16 ++ 9 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 crates/delta/src/routes/customisation/emoji_edit.rs diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index 4615273d..38c6f941 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -3,7 +3,7 @@ use revolt_result::Error; use serde::{Deserialize, Serialize}; use revolt_models::v0::{ - AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook + AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialEmoji, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook }; use crate::Database; @@ -219,6 +219,12 @@ pub enum EventV1 { /// New emoji EmojiCreate(Emoji), + /// Update existing emoji + EmojiUpdate { + id: String, + data: PartialEmoji, + }, + /// Delete emoji EmojiDelete { id: String }, diff --git a/crates/core/database/src/models/emojis/model.rs b/crates/core/database/src/models/emojis/model.rs index 8294d7c7..3f380716 100644 --- a/crates/core/database/src/models/emojis/model.rs +++ b/crates/core/database/src/models/emojis/model.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::str::FromStr; use once_cell::sync::Lazy; +use revolt_models::v0; use revolt_result::Result; use ulid::Ulid; @@ -41,6 +42,12 @@ auto_derived!( Server { id: String }, Detached, } + + /// Partial representation of an emoji + pub struct PartialEmoji { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + } ); #[allow(clippy::disallowed_methods)] @@ -75,6 +82,26 @@ impl Emoji { db.detach_emoji(&self).await } + /// Update an emoji + pub async fn update(&mut self, db: &Database, partial: PartialEmoji) -> Result<()> { + if let Some(name) = partial.name.clone() { + self.name = name; + } + + db.update_emoji(&self.id, &partial).await?; + + EventV1::EmojiUpdate { + id: self.id.clone(), + data: v0::PartialEmoji { + name: partial.name.clone(), + }, + } + .p(self.parent().to_string()) + .await; + + Ok(()) + } + /// Check whether we can use a given emoji pub async fn can_use(db: &Database, emoji: &str) -> Result { if Ulid::from_str(emoji).is_ok() { diff --git a/crates/core/database/src/models/emojis/ops.rs b/crates/core/database/src/models/emojis/ops.rs index 26e23aca..d28f30d4 100644 --- a/crates/core/database/src/models/emojis/ops.rs +++ b/crates/core/database/src/models/emojis/ops.rs @@ -1,6 +1,6 @@ use revolt_result::Result; -use crate::Emoji; +use crate::{Emoji, PartialEmoji}; #[cfg(feature = "mongodb")] mod mongodb; @@ -20,6 +20,9 @@ pub trait AbstractEmojis: Sync + Send { /// Fetch emoji by their parent ids async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result>; + /// Update emoji with new information + async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()>; + /// Detach an emoji by its id async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>; } diff --git a/crates/core/database/src/models/emojis/ops/mongodb.rs b/crates/core/database/src/models/emojis/ops/mongodb.rs index 6dd3137b..ad7b557c 100644 --- a/crates/core/database/src/models/emojis/ops/mongodb.rs +++ b/crates/core/database/src/models/emojis/ops/mongodb.rs @@ -1,7 +1,7 @@ use bson::Document; use revolt_result::Result; -use crate::Emoji; +use crate::{Emoji, PartialEmoji}; use crate::MongoDb; use super::AbstractEmojis; @@ -46,6 +46,11 @@ impl AbstractEmojis for MongoDb { ) } + /// Update emoji with new information + async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()> { + query!(self, update_one_by_id, COL, emoji_id, partial, vec![], None).map(|_| ()) + } + /// Detach an emoji by its id async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> { self.col::(COL) diff --git a/crates/core/database/src/models/emojis/ops/reference.rs b/crates/core/database/src/models/emojis/ops/reference.rs index 2f0c2a2d..2f9be435 100644 --- a/crates/core/database/src/models/emojis/ops/reference.rs +++ b/crates/core/database/src/models/emojis/ops/reference.rs @@ -1,6 +1,6 @@ use revolt_result::Result; -use crate::Emoji; +use crate::{Emoji, PartialEmoji}; use crate::EmojiParent; use crate::ReferenceDb; @@ -54,6 +54,19 @@ impl AbstractEmojis for ReferenceDb { .collect()) } + /// Update emoji with new information + async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()> { + let mut emojis = self.emojis.lock().await; + if let Some(emoji) = emojis.get_mut(emoji_id) { + if let Some(name) = partial.name.clone() { + emoji.name = name; + } + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + /// Detach an emoji by its id async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> { let mut emojis = self.emojis.lock().await; diff --git a/crates/core/models/src/v0/emojis.rs b/crates/core/models/src/v0/emojis.rs index 2d7c015e..58d47429 100644 --- a/crates/core/models/src/v0/emojis.rs +++ b/crates/core/models/src/v0/emojis.rs @@ -54,4 +54,22 @@ auto_derived!( #[serde(default)] pub nsfw: bool, } + + /// Partial emoji representation + #[derive(Default)] + pub struct PartialEmoji { + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub name: Option, + } + + /// Edit emoji information + #[cfg_attr(feature = "validator", derive(Validate))] + pub struct DataEditEmoji { + /// Emoji name + #[cfg_attr( + feature = "validator", + validate(length(min = 1, max = 32), regex = "RE_EMOJI") + )] + pub name: Option, + } ); diff --git a/crates/delta/src/routes/customisation/emoji_edit.rs b/crates/delta/src/routes/customisation/emoji_edit.rs new file mode 100644 index 00000000..72e99e34 --- /dev/null +++ b/crates/delta/src/routes/customisation/emoji_edit.rs @@ -0,0 +1,212 @@ +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, EmojiParent, PartialEmoji, User, +}; +use revolt_models::v0; +use revolt_permissions::{calculate_server_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use validator::Validate; + +/// # Edit Emoji +/// +/// Edit an emoji by its id. +#[openapi(tag = "Emojis")] +#[patch("/emoji/", data = "")] +pub async fn edit_emoji( + db: &State, + user: User, + emoji_id: Reference<'_>, + data: Json, +) -> Result> { + let data = data.into_inner(); + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let mut emoji = emoji_id.as_emoji(db).await?; + + match &emoji.parent { + EmojiParent::Server { id } => { + let server = db.fetch_server(id.as_str()).await?; + + let mut query = DatabasePermissionQuery::new(db, &user).server(&server); + calculate_server_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageCustomisation)?; + } + EmojiParent::Detached => return Err(create_error!(NotAuthenticated)), + } + + if data.name.is_none() { + return Ok(Json(emoji.into())); + } + + let partial = PartialEmoji { name: data.name }; + emoji.update(db, partial).await?; + + Ok(Json(emoji.into())) +} + +#[cfg(test)] +mod test { + use crate::util::test::TestHarness; + use revolt_database::{Emoji, EmojiParent, Member}; + use revolt_models::v0; + use rocket::http::{ContentType, Header, Status}; + use ulid::Ulid; + + #[rocket::async_test] + async fn edit_emoji_name_as_creator() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + let (server, _) = harness.new_server(&user).await; + + let emoji_id = Ulid::new().to_string(); + let emoji = Emoji { + id: emoji_id.clone(), + parent: EmojiParent::Server { + id: server.id.clone(), + }, + creator_id: user.id.clone(), + name: "initial_name".to_string(), + animated: false, + nsfw: false, + }; + emoji.create(&harness.db).await.expect("`Emoji` created"); + + let response = harness + .client + .patch(format!("/custom/emoji/{emoji_id}")) + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataEditEmoji { + name: Some("renamed_emoji".to_string()), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Ok); + + let edited: v0::Emoji = response.into_json().await.expect("`Emoji`"); + assert_eq!(edited.name, "renamed_emoji"); + } + + #[rocket::async_test] + async fn reject_invalid_emoji_name() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + let (server, _) = harness.new_server(&user).await; + + let emoji_id = Ulid::new().to_string(); + let emoji = Emoji { + id: emoji_id.clone(), + parent: EmojiParent::Server { + id: server.id.clone(), + }, + creator_id: user.id.clone(), + name: "valid_name".to_string(), + animated: false, + nsfw: false, + }; + emoji.create(&harness.db).await.expect("`Emoji` created"); + + let response = harness + .client + .patch(format!("/custom/emoji/{emoji_id}")) + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataEditEmoji { + name: Some("Invalid Name".to_string()), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::BadRequest); + } + + #[rocket::async_test] + async fn reject_edit_for_detached_emoji() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let emoji_id = Ulid::new().to_string(); + let emoji = Emoji { + id: emoji_id.clone(), + parent: EmojiParent::Detached, + creator_id: user.id.clone(), + name: "detached_name".to_string(), + animated: false, + nsfw: false, + }; + emoji.create(&harness.db).await.expect("`Emoji` created"); + + let response = harness + .client + .patch(format!("/custom/emoji/{emoji_id}")) + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataEditEmoji { + name: Some("should_not_apply".to_string()), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Unauthorized); + } + + #[rocket::async_test] + async fn reject_edit_for_creator_without_manage_customisation() { + let harness = TestHarness::new().await; + let (_, _, owner) = harness.new_user().await; + let (_, creator_session, creator) = harness.new_user().await; + let (server, _) = harness.new_server(&owner).await; + + Member::create(&harness.db, &server, &creator, None) + .await + .expect("`Member` created"); + + let emoji_id = Ulid::new().to_string(); + let emoji = Emoji { + id: emoji_id.clone(), + parent: EmojiParent::Server { + id: server.id.clone(), + }, + creator_id: creator.id.clone(), + name: "member_uploaded_name".to_string(), + animated: false, + nsfw: false, + }; + emoji.create(&harness.db).await.expect("`Emoji` created"); + + let response = harness + .client + .patch(format!("/custom/emoji/{emoji_id}")) + .header(Header::new( + "x-session-token", + creator_session.token.to_string(), + )) + .header(ContentType::JSON) + .body( + json!(v0::DataEditEmoji { + name: Some("renamed_without_permission".to_string()), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Forbidden); + } +} diff --git a/crates/delta/src/routes/customisation/mod.rs b/crates/delta/src/routes/customisation/mod.rs index 56d40995..5ff6f699 100644 --- a/crates/delta/src/routes/customisation/mod.rs +++ b/crates/delta/src/routes/customisation/mod.rs @@ -3,12 +3,14 @@ use rocket::Route; mod emoji_create; mod emoji_delete; +mod emoji_edit; mod emoji_fetch; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ emoji_create::create_emoji, emoji_delete::delete_emoji, + emoji_edit::edit_emoji, emoji_fetch::fetch_emoji ] } diff --git a/docs/docs/developers/events/protocol.md b/docs/docs/developers/events/protocol.md index 1c8a8618..6e0c8d5e 100644 --- a/docs/docs/developers/events/protocol.md +++ b/docs/docs/developers/events/protocol.md @@ -539,6 +539,22 @@ Emoji created, the event object has the same schema as the Emoji object in the A } ``` +### EmojiUpdate + +Emoji has been updated. + +```json +{ + "type": "EmojiUpdate", + "id": "{emoji_id}", + "data": { + "name"?: "{emoji_name}" + } +} +``` + +- `data` field contains a partial Emoji object. + ### EmojiDelete Emoji has been deleted. From 6f3441cf4acac2a8e6e1bf07a279a153b80f7956 Mon Sep 17 00:00:00 2001 From: Taureon <45183108+Taureon@users.noreply.github.com> Date: Sat, 9 May 2026 00:37:00 +0200 Subject: [PATCH 12/39] feat: Add webhook endpoints for editing and deleting messages (#682) * feat: ErrorType.CannotDeleteMessage, needed later Signed-off-by: Taureon * feat: webhook edit/delete message endpoints Signed-off-by: Taureon * lol, lmao even Signed-off-by: Taureon * fix contradictory comment Signed-off-by: Taureon --------- Signed-off-by: Taureon Co-authored-by: Taureon --- crates/core/result/src/axum.rs | 1 + crates/core/result/src/lib.rs | 1 + crates/core/result/src/rocket.rs | 1 + crates/delta/src/routes/webhooks/mod.rs | 10 ++- .../routes/webhooks/webhook_delete_message.rs | 27 ++++++ .../routes/webhooks/webhook_edit_message.rs | 84 +++++++++++++++++++ 6 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 crates/delta/src/routes/webhooks/webhook_delete_message.rs create mode 100644 crates/delta/src/routes/webhooks/webhook_edit_message.rs diff --git a/crates/core/result/src/axum.rs b/crates/core/result/src/axum.rs index f465854b..440c4fc4 100644 --- a/crates/core/result/src/axum.rs +++ b/crates/core/result/src/axum.rs @@ -24,6 +24,7 @@ impl IntoResponse for Error { ErrorType::UnknownChannel => StatusCode::NOT_FOUND, ErrorType::UnknownMessage => StatusCode::NOT_FOUND, ErrorType::UnknownAttachment => StatusCode::BAD_REQUEST, + ErrorType::CannotDeleteMessage => StatusCode::FORBIDDEN, ErrorType::CannotEditMessage => StatusCode::FORBIDDEN, ErrorType::CannotJoinCall => StatusCode::BAD_REQUEST, ErrorType::TooManyAttachments { .. } => StatusCode::BAD_REQUEST, diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index e4907a6b..4124a047 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -78,6 +78,7 @@ pub enum ErrorType { UnknownChannel, UnknownAttachment, UnknownMessage, + CannotDeleteMessage, CannotEditMessage, CannotJoinCall, TooManyAttachments { diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index 649375e2..76719149 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -30,6 +30,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::UnknownChannel => Status::NotFound, ErrorType::UnknownMessage => Status::NotFound, ErrorType::UnknownAttachment => Status::BadRequest, + ErrorType::CannotDeleteMessage => Status::Forbidden, ErrorType::CannotEditMessage => Status::Forbidden, ErrorType::CannotJoinCall => Status::BadRequest, ErrorType::TooManyAttachments { .. } => Status::BadRequest, diff --git a/crates/delta/src/routes/webhooks/mod.rs b/crates/delta/src/routes/webhooks/mod.rs index 6b0ef448..1aec16f4 100644 --- a/crates/delta/src/routes/webhooks/mod.rs +++ b/crates/delta/src/routes/webhooks/mod.rs @@ -1,19 +1,23 @@ -use rocket::Route; use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; +use rocket::Route; mod webhook_delete; +mod webhook_delete_message; mod webhook_delete_token; mod webhook_edit; +mod webhook_edit_message; mod webhook_edit_token; mod webhook_execute; -mod webhook_fetch_token; -mod webhook_fetch; mod webhook_execute_github; +mod webhook_fetch; +mod webhook_fetch_token; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ + webhook_delete_message::webhook_delete_message, webhook_delete_token::webhook_delete_token, webhook_delete::webhook_delete, + webhook_edit_message::webhook_edit_message, webhook_edit_token::webhook_edit_token, webhook_edit::webhook_edit, webhook_execute_github::webhook_execute_github, diff --git a/crates/delta/src/routes/webhooks/webhook_delete_message.rs b/crates/delta/src/routes/webhooks/webhook_delete_message.rs new file mode 100644 index 00000000..be4cb63b --- /dev/null +++ b/crates/delta/src/routes/webhooks/webhook_delete_message.rs @@ -0,0 +1,27 @@ +use revolt_database::{util::reference::Reference, Database}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +/// # Deletes a webhook message +/// +/// Deletes a message sent by a webhook +#[openapi(tag = "Webhooks")] +#[delete("///")] +pub async fn webhook_delete_message( + db: &State, + webhook_id: Reference<'_>, + token: String, + message_id: Reference<'_>, +) -> Result { + let webhook = webhook_id.as_webhook(db).await?; + webhook.assert_token(&token)?; + + let message = message_id.as_message(db).await?; + + if message.author != webhook.id { + return Err(create_error!(CannotDeleteMessage)); + } + + message.delete(db).await.map(|_| EmptyResponse) +} diff --git a/crates/delta/src/routes/webhooks/webhook_edit_message.rs b/crates/delta/src/routes/webhooks/webhook_edit_message.rs new file mode 100644 index 00000000..a6e4036f --- /dev/null +++ b/crates/delta/src/routes/webhooks/webhook_edit_message.rs @@ -0,0 +1,84 @@ +use iso8601_timestamp::Timestamp; +use revolt_config::config; +use revolt_database::{ + tasks::process_embeds::queue, util::reference::Reference, Database, Message, PartialMessage, +}; +use revolt_models::v0::{self, DataEditMessage, Embed}; +use revolt_models::validator::Validate; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +/// # Edits a webhook message +/// +/// Edits a message sent by a webhook +#[openapi(tag = "Webhooks")] +#[patch("///", data = "")] +pub async fn webhook_edit_message( + db: &State, + webhook_id: Reference<'_>, + token: String, + message_id: Reference<'_>, + data: Json, +) -> Result> { + let edit = data.into_inner(); + edit.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + Message::validate_sum( + &edit.content, + edit.embeds.as_deref().unwrap_or_default(), + config().await.features.limits.default.message_length, + )?; + + let webhook = webhook_id.as_webhook(db).await?; + webhook.assert_token(&token)?; + + let mut message = message_id.as_message(db).await?; + if message.author != webhook.id { + return Err(create_error!(CannotEditMessage)); + } + + message.edited = Some(Timestamp::now_utc()); + let mut partial = PartialMessage { + edited: message.edited, + ..Default::default() + }; + + // 1. Handle content update + if let Some(content) = &edit.content { + partial.content = Some(content.clone()); + } + + // 2. Clear any auto generated embeds + let mut new_embeds = vec![]; + if let Some(embeds) = &message.embeds { + for embed in embeds { + if let Embed::Text(embed) = embed { + new_embeds.push(Embed::Text(embed.clone())) + } + } + } + + // 3. Replace if we are given new embeds + if let Some(embeds) = edit.embeds { + new_embeds.clear(); + + for embed in embeds { + new_embeds.push(message.create_embed(db, embed).await?); + } + } + + partial.embeds = Some(new_embeds); + + message.update(db, partial, vec![]).await?; + + // Queue up a task for processing embeds + if let Some(content) = edit.content { + queue(message.channel.to_string(), message.id.to_string(), content).await; + } + + Ok(Json(message.into_model(None, None))) +} From d76a71141f3e508f6308ba52fa28eaeb56fb3438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Sat, 9 May 2026 01:49:31 +0300 Subject: [PATCH 13/39] fix: don't strip ICC from exif (#735) * fix: don't strip ICC from exif Signed-off-by: ispik * fix: refactor ICC profile handling in image processing Signed-off-by: ispik --------- Signed-off-by: ispik --- Cargo.lock | 55 +++++++++++- Cargo.toml | 1 + crates/services/autumn/Cargo.toml | 1 + crates/services/autumn/src/exif.rs | 112 +++++++++++++++++-------- crates/services/autumn/src/main.rs | 1 + crates/services/autumn/src/metadata.rs | 31 ++++--- crates/services/autumn/src/utils.rs | 31 +++++++ 7 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 crates/services/autumn/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 671d6a9d..b55dab06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3068,7 +3068,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -3077,6 +3098,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -4847,6 +4874,29 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "lcms2" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75877b724685dd49310bdbadbf973fc69b1d01992a6d4a861b928fc3943f87b" +dependencies = [ + "bytemuck", + "foreign-types 0.5.0", + "lcms2-sys", +] + +[[package]] +name = "lcms2-sys" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2604b23848ca80b2add60f0fb2270fd980e622c25029b6597fa01cfd5f8d5f" +dependencies = [ + "cc", + "dunce", + "libc", + "pkg-config", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -5894,7 +5944,7 @@ checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.11.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -7466,6 +7516,7 @@ dependencies = [ "jxl-oxide", "kamadak-exif", "lazy_static", + "lcms2", "moka", "nanoid", "revolt-config", diff --git a/Cargo.toml b/Cargo.toml index 6ce826ef..f7a3e83a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ kamadak-exif = "0.5.4" webp = "0.3.0" image = "0.25.2" # avif encode requires dav1d system library: features = ["avif-native"] thumbhash = "0.1.0" +lcms2 = "6.1.1" # for color profile processing # File processing revolt_clamav-client = "0.1.5" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index 1636a8ec..62b0bbcf 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -18,6 +18,7 @@ kamadak-exif = { workspace = true } # revolt_little_exif = "0.5.1" image = { workspace = true } thumbhash = { workspace = true } +lcms2 = { workspace = true } # File processing revolt_clamav-client = { workspace = true } diff --git a/crates/services/autumn/src/exif.rs b/crates/services/autumn/src/exif.rs index af02953e..be8e6150 100644 --- a/crates/services/autumn/src/exif.rs +++ b/crates/services/autumn/src/exif.rs @@ -1,13 +1,24 @@ use std::io::{Cursor, Read}; +use crate::utils::apply_icc_profile; use exif::Reader; -use image::{ImageFormat, ImageReader}; +use image::{ImageEncoder, ImageReader}; use revolt_config::report_internal_error; use revolt_database::Metadata; use revolt_result::{create_error, Result}; use tempfile::NamedTempFile; use tokio::process::Command; +macro_rules! encode_with_icc { + ($encoder:expr, $icc:expr, $image:expr, $width:expr, $height:expr, $color:expr) => {{ + let mut encoder = $encoder; + if let Some(icc) = $icc { + let _ = encoder.set_icc_profile(icc.clone()); + } + encoder.write_image($image, $width, $height, $color) + }}; +} + /// Strip EXIF data from given file and produce new file and metadata pub async fn strip_metadata( file: NamedTempFile, @@ -17,8 +28,8 @@ pub async fn strip_metadata( ) -> Result<(Vec, Metadata)> { match &metadata { Metadata::Image { - width, - height, + width: _, + height: _, thumbhash, animated, } => match mime { @@ -46,11 +57,12 @@ pub async fn strip_metadata( let mut cursor = Cursor::new(buf); // Decode the image - let image = report_internal_error!(report_internal_error!(ImageReader::new( - &mut cursor - ) - .with_guessed_format())? - .decode()); + let reader = + report_internal_error!(ImageReader::new(&mut cursor).with_guessed_format())?; + let mut decoder = report_internal_error!(reader.into_decoder())?; + let mut icc_profile = + report_internal_error!(image::ImageDecoder::icc_profile(&mut decoder))?; + let mut image = report_internal_error!(image::DynamicImage::from_decoder(decoder))?; // Reset read position cursor.set_position(0); @@ -71,38 +83,68 @@ pub async fn strip_metadata( // Apply the EXIF rotation // See https://jdhao.github.io/2019/07/31/image_rotation_exif_info/ - report_internal_error!(match &rotation { - 2 => image?.fliph(), - 3 => image?.rotate180(), - 4 => image?.rotate180().fliph(), - 5 => image?.rotate90().fliph(), - 6 => image?.rotate90(), - 7 => image?.rotate270().fliph(), - 8 => image?.rotate270(), - _ => image?, - } - .write_to( - &mut writer, - match mime { - "image/jpeg" => ImageFormat::Jpeg, - "image/png" => ImageFormat::Png, - "image/avif" => ImageFormat::Avif, - "image/tiff" => ImageFormat::Tiff, - _ => todo!(), - }, - ))?; - - // Calculate dimensions after rotation. - let (width, height) = match &rotation { - 2 | 4 | 5 | 7 => (*height, *width), - _ => (*width, *height), + image = match &rotation { + 2 => image.fliph(), + 3 => image.rotate180(), + 4 => image.rotate180().fliph(), + 5 => image.rotate90().fliph(), + 6 => image.rotate90(), + 7 => image.rotate270().fliph(), + 8 => image.rotate270(), + _ => image, }; + if let Some(icc) = &icc_profile { + image = apply_icc_profile(image, icc); + icc_profile = None; + } + + let color_type = image.color(); + let width = image.width(); + let height = image.height(); + + report_internal_error!(match mime { + "image/jpeg" => encode_with_icc!( + image::codecs::jpeg::JpegEncoder::new(&mut writer), + &icc_profile, + image.as_bytes(), + width, + height, + color_type.into() + ), + "image/png" => encode_with_icc!( + image::codecs::png::PngEncoder::new(&mut writer), + &icc_profile, + image.as_bytes(), + width, + height, + color_type.into() + ), + "image/avif" => { + // avif encoder doesn't implement set_icc_profile currently + image::codecs::avif::AvifEncoder::new(&mut writer).write_image( + image.as_bytes(), + width, + height, + color_type.into(), + ) + } + "image/tiff" => encode_with_icc!( + image::codecs::tiff::TiffEncoder::new(&mut writer), + &icc_profile, + image.as_bytes(), + width, + height, + color_type.into() + ), + _ => unreachable!(), + })?; + Ok(( bytes, Metadata::Image { - width, - height, + width: width as isize, + height: height as isize, thumbhash: thumbhash.clone(), animated: *animated, }, diff --git a/crates/services/autumn/src/main.rs b/crates/services/autumn/src/main.rs index cb936e0c..e6234318 100644 --- a/crates/services/autumn/src/main.rs +++ b/crates/services/autumn/src/main.rs @@ -18,6 +18,7 @@ pub mod exif; pub mod metadata; pub mod mime_type; mod ratelimits; +mod utils; #[derive(FromRef, Clone)] struct AppState { diff --git a/crates/services/autumn/src/metadata.rs b/crates/services/autumn/src/metadata.rs index 8e0c89cd..c476eac4 100644 --- a/crates/services/autumn/src/metadata.rs +++ b/crates/services/autumn/src/metadata.rs @@ -1,5 +1,6 @@ use std::io::Cursor; +use crate::utils::apply_icc_profile; use image::{GenericImageView, ImageError, ImageReader}; use revolt_database::Metadata; use revolt_files::{image_size, is_animated, video_size}; @@ -27,16 +28,26 @@ pub fn generate_metadata(f: &NamedTempFile, mime_type: &str) -> Metadata { .map(|(width, height)| Metadata::Image { width: width as isize, height: height as isize, - thumbhash: ImageReader::open(f) - .and_then(|r| r.with_guessed_format()) - .map_err(ImageError::from) - .and_then(|r| r.decode()) - .map(|img| img.thumbnail(100, 100)) - .map(|img| (img.dimensions(), img.to_rgba8().into_raw())) - .map(|((width, height), rgba)| { - thumbhash::rgba_to_thumb_hash(width as usize, height as usize, &rgba) - }) - .ok(), + thumbhash: (|| { + let reader = ImageReader::open(f).ok()?.with_guessed_format().ok()?; + let mut decoder = reader.into_decoder().ok()?; + let icc_profile = image::ImageDecoder::icc_profile(&mut decoder) + .ok() + .flatten(); + let mut img = image::DynamicImage::from_decoder(decoder).ok()?; + + if let Some(icc) = icc_profile { + img = apply_icc_profile(img, &icc); + } + + let img = img.thumbnail(100, 100); + let (width, height) = img.dimensions(); + Some(thumbhash::rgba_to_thumb_hash( + width as usize, + height as usize, + &img.into_rgba8().into_raw(), + )) + })(), animated: is_animated(f, mime_type).or(Some(false)), }) .unwrap_or_default() diff --git a/crates/services/autumn/src/utils.rs b/crates/services/autumn/src/utils.rs new file mode 100644 index 00000000..5e2fbfb8 --- /dev/null +++ b/crates/services/autumn/src/utils.rs @@ -0,0 +1,31 @@ +/// Convert image to sRGB using the provided ICC profile. +/// Returns the converted image, or the original if conversion fails. +pub fn apply_icc_profile(image: image::DynamicImage, icc: &[u8]) -> image::DynamicImage { + let Ok(src_profile) = lcms2::Profile::new_icc(icc) else { + return image; + }; + let dst_profile = lcms2::Profile::new_srgb(); + let format = if image.color().has_alpha() { + lcms2::PixelFormat::RGBA_8 + } else { + lcms2::PixelFormat::RGB_8 + }; + let Ok(t) = lcms2::Transform::new( + &src_profile, + format, + &dst_profile, + format, + lcms2::Intent::Perceptual, + ) else { + return image; + }; + if image.color().has_alpha() { + let mut rgba_image = image.into_rgba8(); + t.transform_in_place(rgba_image.as_mut()); + image::DynamicImage::ImageRgba8(rgba_image) + } else { + let mut rgb_image = image.into_rgb8(); + t.transform_in_place(rgb_image.as_mut()); + image::DynamicImage::ImageRgb8(rgb_image) + } +} From d52e84c5d3afec0d7d843ef03b7cb807a54f98a8 Mon Sep 17 00:00:00 2001 From: "stoat-release[bot]" <245062572+stoat-release[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 17:26:38 +0100 Subject: [PATCH 14/39] chore(main): release 0.13.0 (#722) Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .release-please-manifest.json | 2 +- CHANGELOG.md | 27 +++++++++++++++++++ Cargo.lock | 36 ++++++++++++------------- Cargo.toml | 20 +++++++------- crates/bonfire/Cargo.toml | 2 +- crates/core/coalesced/Cargo.toml | 2 +- crates/core/config/Cargo.toml | 2 +- crates/core/database/Cargo.toml | 2 +- crates/core/files/Cargo.toml | 2 +- crates/core/models/Cargo.toml | 2 +- crates/core/parser/Cargo.toml | 2 +- crates/core/permissions/Cargo.toml | 2 +- crates/core/presence/Cargo.toml | 2 +- crates/core/ratelimits/Cargo.toml | 2 +- crates/core/result/Cargo.toml | 2 +- crates/daemons/crond/Cargo.toml | 2 +- crates/daemons/pushd/Cargo.toml | 2 +- crates/daemons/voice-ingress/Cargo.toml | 2 +- crates/delta/Cargo.toml | 2 +- crates/services/autumn/Cargo.toml | 2 +- crates/services/gifbox/Cargo.toml | 2 +- crates/services/january/Cargo.toml | 2 +- version.txt | 2 +- 23 files changed, 75 insertions(+), 48 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8950f712..04733b0e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.12.1" + ".": "0.13.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cc7f7328..bfb6b326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## [0.13.0](https://github.com/stoatchat/stoatchat/compare/v0.12.1...v0.13.0) (2026-05-08) + + +### Features + +* add embed support for YouTube Shorts ([#734](https://github.com/stoatchat/stoatchat/issues/734)) ([d46c7f7](https://github.com/stoatchat/stoatchat/commit/d46c7f7f3c04524c0639c3e0a122626f8e0b3bf7)) +* add emoji rename endpoint ([#714](https://github.com/stoatchat/stoatchat/issues/714)) ([23ad135](https://github.com/stoatchat/stoatchat/commit/23ad1359834bb7d07a460b8678d6a6ebffc73eb0)) +* add legal links to root payload ([#733](https://github.com/stoatchat/stoatchat/issues/733)) ([21d8201](https://github.com/stoatchat/stoatchat/commit/21d82018cf84ab0fdd10613d254b9562aea8eea3)) +* add role icon support ([#724](https://github.com/stoatchat/stoatchat/issues/724)) ([841985d](https://github.com/stoatchat/stoatchat/commit/841985d3b994df1c6eefab2fc7ecbd77ab22c493)) +* Add webhook endpoints for editing and deleting messages ([#682](https://github.com/stoatchat/stoatchat/issues/682)) ([6f3441c](https://github.com/stoatchat/stoatchat/commit/6f3441cf4acac2a8e6e1bf07a279a153b80f7956)) +* automatically sanitise usernames on create/update ([#689](https://github.com/stoatchat/stoatchat/issues/689)) ([e937697](https://github.com/stoatchat/stoatchat/commit/e93769786c7669485a659ee471630740d3cea702)) +* blacklist private ip ranges and add january domain blocklist ([#731](https://github.com/stoatchat/stoatchat/issues/731)) ([6b41db9](https://github.com/stoatchat/stoatchat/commit/6b41db984bb491b2e58324309cc70d8c14e0b814)) +* Rewrite acks ([#741](https://github.com/stoatchat/stoatchat/issues/741)) ([ab5bd47](https://github.com/stoatchat/stoatchat/commit/ab5bd47a39ee889de0b5ae6e7b560620853daead)) + + +### Bug Fixes + +* add new_user_hours to configuration limits ([#729](https://github.com/stoatchat/stoatchat/issues/729)) ([279f5d5](https://github.com/stoatchat/stoatchat/commit/279f5d5fd7af2df55902c706859ec07f569cdb1e)) +* add reconnection policy to Redis subscriber to prevent ghost state ([#708](https://github.com/stoatchat/stoatchat/issues/708)) ([057f2bb](https://github.com/stoatchat/stoatchat/commit/057f2bb8b359f8b942741a30ff54eeb8fbe3e0b1)) +* docker compose file had personal url in it ([#742](https://github.com/stoatchat/stoatchat/issues/742)) ([0719985](https://github.com/stoatchat/stoatchat/commit/0719985ac5636590f91e6f9ec4b68f3eded70c13)) +* don't strip ICC from exif ([#735](https://github.com/stoatchat/stoatchat/issues/735)) ([d76a711](https://github.com/stoatchat/stoatchat/commit/d76a71141f3e508f6308ba52fa28eaeb56fb3438)) +* dont send notification in fcm ([#721](https://github.com/stoatchat/stoatchat/issues/721)) ([89171e9](https://github.com/stoatchat/stoatchat/commit/89171e9bd0f15711157e78c6eec0fe7b480de93a)) +* encode filenames in redirects ([#737](https://github.com/stoatchat/stoatchat/issues/737)) ([9fd7128](https://github.com/stoatchat/stoatchat/commit/9fd7128f800badbd184baf943d4f799e601201e4)) +* january ip redirects & domain resolver ([#738](https://github.com/stoatchat/stoatchat/issues/738)) ([356491e](https://github.com/stoatchat/stoatchat/commit/356491e934b274f9e895df883dd63ef0b3123510)) +* update message length validation to remove upper limit ([#723](https://github.com/stoatchat/stoatchat/issues/723)) ([ed4fd5e](https://github.com/stoatchat/stoatchat/commit/ed4fd5ebfe6d0ea534a0898da4afdc1f4e2cd6c5)) +* use correct response for NoEffect errors ([#732](https://github.com/stoatchat/stoatchat/issues/732)) ([5378cd2](https://github.com/stoatchat/stoatchat/commit/5378cd22b4c7d85f44c31a6af0dda00941b80d5c)) + ## [0.12.1](https://github.com/stoatchat/stoatchat/compare/v0.12.0...v0.12.1) (2026-04-10) diff --git a/Cargo.lock b/Cargo.lock index b55dab06..ecc716a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7504,7 +7504,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.12.1" +version = "0.13.0" dependencies = [ "axum", "axum-macros", @@ -7545,7 +7545,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.12.1" +version = "0.13.0" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7576,7 +7576,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.12.1" +version = "0.13.0" dependencies = [ "indexmap 2.13.1", "lru", @@ -7585,7 +7585,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.12.1" +version = "0.13.0" dependencies = [ "async-std", "cached", @@ -7602,7 +7602,7 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.12.1" +version = "0.13.0" dependencies = [ "futures-lite", "iso8601-timestamp", @@ -7622,7 +7622,7 @@ dependencies = [ [[package]] name = "revolt-database" -version = "0.12.1" +version = "0.13.0" dependencies = [ "amqprs", "async-lock 2.8.0", @@ -7673,7 +7673,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.12.1" +version = "0.13.0" dependencies = [ "amqprs", "async-channel 2.5.0", @@ -7722,7 +7722,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.12.1" +version = "0.13.0" dependencies = [ "aes-gcm", "anyhow", @@ -7750,7 +7750,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.12.1" +version = "0.13.0" dependencies = [ "axum", "axum-extra", @@ -7773,7 +7773,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.12.1" +version = "0.13.0" dependencies = [ "async-recursion", "axum", @@ -7803,7 +7803,7 @@ dependencies = [ [[package]] name = "revolt-models" -version = "0.12.1" +version = "0.13.0" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7822,14 +7822,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.12.1" +version = "0.13.0" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.12.1" +version = "0.13.0" dependencies = [ "async-std", "async-trait", @@ -7844,7 +7844,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.12.1" +version = "0.13.0" dependencies = [ "async-std", "log", @@ -7856,7 +7856,7 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.12.1" +version = "0.13.0" dependencies = [ "amqprs", "anyhow", @@ -7887,7 +7887,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.12.1" +version = "0.13.0" dependencies = [ "async-trait", "authifier", @@ -7904,7 +7904,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.12.1" +version = "0.13.0" dependencies = [ "axum", "log", @@ -7920,7 +7920,7 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.12.1" +version = "0.13.0" dependencies = [ "amqprs", "async-std", diff --git a/Cargo.toml b/Cargo.toml index f7a3e83a..f49dd3f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,13 +192,13 @@ futures-lite = "2.6.1" vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.12.0", path = "crates/core/coalesced" } -revolt-config = { version = "0.12.0", path = "crates/core/config" } -revolt-database = { version = "0.12.0", path = "crates/core/database" } -revolt-files = { version = "0.12.0", path = "crates/core/files" } -revolt-models = { version = "0.12.0", path = "crates/core/models" } -revolt-parser = { version = "0.12.0", path = "crates/core/parser" } -revolt-permissions = { version = "0.12.0", path = "crates/core/permissions" } -revolt-presence = { version = "0.12.0", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.12.0", path = "crates/core/ratelimits" } -revolt-result = { version = "0.12.0", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.0", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.0", path = "crates/core/config" } +revolt-database = { version = "0.13.0", path = "crates/core/database" } +revolt-files = { version = "0.13.0", path = "crates/core/files" } +revolt-models = { version = "0.13.0", path = "crates/core/models" } +revolt-parser = { version = "0.13.0", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.0", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.0", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.0", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.0", path = "crates/core/result" } diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 1e1cc8ca..401c2fa0 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.12.1" +version = "0.13.0" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index e6277bd0..17f7839f 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index 7b33b5e9..f4f70e58 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index be3119c5..c42c8e8f 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index 9f30649d..416d57d9 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index f8a4c697..41d08d78 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index 1bc35301..79f65d70 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index 838b10b2..0fb22d94 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index 547dcd79..e2cfe001 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index 0e862c80..3e9bb620 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.12.1" +version = "0.13.0" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 9a1d1c13..461659dc 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index 6bd90052..9a7319ab 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.12.1" +version = "0.13.0" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index b26c80e1..28aa7fd8 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index b8d15250..054698df 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.12.1" +version = "0.13.0" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 2919ff56..9f8a931c 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.12.1" +version = "0.13.0" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index 62b0bbcf..1f24bb4b 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 6e7b467c..b9edcf1b 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index 7059cca0..f40d128e 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.12.1" +version = "0.13.0" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/version.txt b/version.txt index 34a83616..54d1a4f2 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.12.1 +0.13.0 From 1100eaf46f849f2509ae01ac497556ca33bde778 Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 10 May 2026 07:22:26 -0700 Subject: [PATCH 15/39] fix: amqprs startup bug (#744) --- crates/core/database/src/amqp/amqp.rs | 29 ++++++++++++++++----------- crates/delta/src/main.rs | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/core/database/src/amqp/amqp.rs b/crates/core/database/src/amqp/amqp.rs index 5358bc34..16326045 100644 --- a/crates/core/database/src/amqp/amqp.rs +++ b/crates/core/database/src/amqp/amqp.rs @@ -29,7 +29,7 @@ impl AMQP { } } - pub async fn new_auto() -> AMQP { + pub async fn new_auto() -> revolt_result::Result { let config = revolt_config::config().await; let connection = Connection::open(&OpenConnectionArguments::new( @@ -46,21 +46,26 @@ impl AMQP { .await .expect("Failed to open RabbitMQ channel"); - channel - .exchange_declare( - ExchangeDeclareArguments::new(&config.pushd.exchange, "direct") - .durable(true) - .finish(), - ) - .await - .expect("Failed to declare exchange"); - - AMQP::new(connection, channel) + let mut resp = AMQP::new(connection, channel); + resp.configure_channels().await?; + Ok(resp) } - pub async fn configure_channels(&self) -> revolt_result::Result<()> { + pub async fn repoen_channel(&mut self) { + self.channel = self + .connection + .open_channel(None) + .await + .expect("Failed to open RabbitMQ channel"); + } + + pub async fn configure_channels(&mut self) -> revolt_result::Result<()> { let config = revolt_config::config().await; + if !self.channel.is_open() { + self.repoen_channel().await; + } + self.channel .exchange_declare( ExchangeDeclareArguments::new( diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index e3522de1..e3cf43a4 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -119,7 +119,7 @@ pub async fn web() -> Rocket { .await .expect("Failed to declare exchange"); - let amqp = AMQP::new(connection, channel); + let mut amqp = AMQP::new(connection, channel); amqp.configure_channels() .await .expect("Failed to configure channels"); From 260036488d85807c74e0ed47ba4dd93d41b65349 Mon Sep 17 00:00:00 2001 From: "stoat-release[bot]" <245062572+stoat-release[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 15:34:41 +0100 Subject: [PATCH 16/39] chore(main): release 0.13.1 (#745) Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++ Cargo.lock | 36 ++++++++++++------------- Cargo.toml | 20 +++++++------- crates/bonfire/Cargo.toml | 2 +- crates/core/coalesced/Cargo.toml | 2 +- crates/core/config/Cargo.toml | 2 +- crates/core/database/Cargo.toml | 2 +- crates/core/files/Cargo.toml | 2 +- crates/core/models/Cargo.toml | 2 +- crates/core/parser/Cargo.toml | 2 +- crates/core/permissions/Cargo.toml | 2 +- crates/core/presence/Cargo.toml | 2 +- crates/core/ratelimits/Cargo.toml | 2 +- crates/core/result/Cargo.toml | 2 +- crates/daemons/crond/Cargo.toml | 2 +- crates/daemons/pushd/Cargo.toml | 2 +- crates/daemons/voice-ingress/Cargo.toml | 2 +- crates/delta/Cargo.toml | 2 +- crates/services/autumn/Cargo.toml | 2 +- crates/services/gifbox/Cargo.toml | 2 +- crates/services/january/Cargo.toml | 2 +- version.txt | 2 +- 23 files changed, 55 insertions(+), 48 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 04733b0e..7f052622 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.0" + ".": "0.13.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index bfb6b326..31c46f2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.13.1](https://github.com/stoatchat/stoatchat/compare/v0.13.0...v0.13.1) (2026-05-10) + + +### Bug Fixes + +* amqprs startup bug ([#744](https://github.com/stoatchat/stoatchat/issues/744)) ([1100eaf](https://github.com/stoatchat/stoatchat/commit/1100eaf46f849f2509ae01ac497556ca33bde778)) + ## [0.13.0](https://github.com/stoatchat/stoatchat/compare/v0.12.1...v0.13.0) (2026-05-08) diff --git a/Cargo.lock b/Cargo.lock index ecc716a1..8eeb17c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7504,7 +7504,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.13.0" +version = "0.13.1" dependencies = [ "axum", "axum-macros", @@ -7545,7 +7545,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.13.0" +version = "0.13.1" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7576,7 +7576,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.13.0" +version = "0.13.1" dependencies = [ "indexmap 2.13.1", "lru", @@ -7585,7 +7585,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.13.0" +version = "0.13.1" dependencies = [ "async-std", "cached", @@ -7602,7 +7602,7 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.13.0" +version = "0.13.1" dependencies = [ "futures-lite", "iso8601-timestamp", @@ -7622,7 +7622,7 @@ dependencies = [ [[package]] name = "revolt-database" -version = "0.13.0" +version = "0.13.1" dependencies = [ "amqprs", "async-lock 2.8.0", @@ -7673,7 +7673,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.13.0" +version = "0.13.1" dependencies = [ "amqprs", "async-channel 2.5.0", @@ -7722,7 +7722,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.13.0" +version = "0.13.1" dependencies = [ "aes-gcm", "anyhow", @@ -7750,7 +7750,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.13.0" +version = "0.13.1" dependencies = [ "axum", "axum-extra", @@ -7773,7 +7773,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.13.0" +version = "0.13.1" dependencies = [ "async-recursion", "axum", @@ -7803,7 +7803,7 @@ dependencies = [ [[package]] name = "revolt-models" -version = "0.13.0" +version = "0.13.1" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7822,14 +7822,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.13.0" +version = "0.13.1" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.13.0" +version = "0.13.1" dependencies = [ "async-std", "async-trait", @@ -7844,7 +7844,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.13.0" +version = "0.13.1" dependencies = [ "async-std", "log", @@ -7856,7 +7856,7 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.13.0" +version = "0.13.1" dependencies = [ "amqprs", "anyhow", @@ -7887,7 +7887,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.13.0" +version = "0.13.1" dependencies = [ "async-trait", "authifier", @@ -7904,7 +7904,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.13.0" +version = "0.13.1" dependencies = [ "axum", "log", @@ -7920,7 +7920,7 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.13.0" +version = "0.13.1" dependencies = [ "amqprs", "async-std", diff --git a/Cargo.toml b/Cargo.toml index f49dd3f1..3ee8db8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,13 +192,13 @@ futures-lite = "2.6.1" vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.13.0", path = "crates/core/coalesced" } -revolt-config = { version = "0.13.0", path = "crates/core/config" } -revolt-database = { version = "0.13.0", path = "crates/core/database" } -revolt-files = { version = "0.13.0", path = "crates/core/files" } -revolt-models = { version = "0.13.0", path = "crates/core/models" } -revolt-parser = { version = "0.13.0", path = "crates/core/parser" } -revolt-permissions = { version = "0.13.0", path = "crates/core/permissions" } -revolt-presence = { version = "0.13.0", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.13.0", path = "crates/core/ratelimits" } -revolt-result = { version = "0.13.0", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.1", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.1", path = "crates/core/config" } +revolt-database = { version = "0.13.1", path = "crates/core/database" } +revolt-files = { version = "0.13.1", path = "crates/core/files" } +revolt-models = { version = "0.13.1", path = "crates/core/models" } +revolt-parser = { version = "0.13.1", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.1", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.1", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.1", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.1", path = "crates/core/result" } diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 401c2fa0..9018e965 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.13.0" +version = "0.13.1" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index 17f7839f..589afa70 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index f4f70e58..48921d77 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index c42c8e8f..dd60ccd7 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index 416d57d9..6910f6c1 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index 41d08d78..696107e8 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index 79f65d70..8bd9cf03 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index 0fb22d94..675b569b 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index e2cfe001..86043d64 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index 3e9bb620..c917269d 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.13.0" +version = "0.13.1" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 461659dc..12e358f1 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index 9a7319ab..f9b09ef1 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.13.0" +version = "0.13.1" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index 28aa7fd8..2f1ba2f7 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index 054698df..ebd90a58 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.13.0" +version = "0.13.1" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 9f8a931c..ef007dae 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.13.0" +version = "0.13.1" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index 1f24bb4b..549ecdcd 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index b9edcf1b..355952cf 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index f40d128e..76d8e5c3 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.13.0" +version = "0.13.1" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/version.txt b/version.txt index 54d1a4f2..c317a918 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.13.0 +0.13.1 From fcb8091cd7a00d7f26c798daa33aae4b923b2a8b Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Mon, 11 May 2026 15:21:34 +0100 Subject: [PATCH 17/39] fix: update default exchange to `revolt.default` (#746) Signed-off-by: Paul Makles --- crates/core/config/Revolt.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index ad2d6992..e598a3dc 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -30,7 +30,7 @@ host = "rabbit" port = 5672 username = "rabbituser" password = "rabbitpass" -default_exchange = "revolt" +default_exchange = "revolt.default" [rabbit.queues] acks = "internal.ack" From 8157e1f6e9ddc0e0290f06b9aa625d639397b69e Mon Sep 17 00:00:00 2001 From: "stoat-release[bot]" <245062572+stoat-release[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 15:26:03 +0100 Subject: [PATCH 18/39] chore(main): release 0.13.2 (#747) * chore(main): release 0.13.2 * chore: update Cargo.lock Signed-off-by: github-actions[bot] --------- Signed-off-by: github-actions[bot] Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++ Cargo.lock | 36 ++++++++++++------------- Cargo.toml | 20 +++++++------- crates/bonfire/Cargo.toml | 2 +- crates/core/coalesced/Cargo.toml | 2 +- crates/core/config/Cargo.toml | 2 +- crates/core/database/Cargo.toml | 2 +- crates/core/files/Cargo.toml | 2 +- crates/core/models/Cargo.toml | 2 +- crates/core/parser/Cargo.toml | 2 +- crates/core/permissions/Cargo.toml | 2 +- crates/core/presence/Cargo.toml | 2 +- crates/core/ratelimits/Cargo.toml | 2 +- crates/core/result/Cargo.toml | 2 +- crates/daemons/crond/Cargo.toml | 2 +- crates/daemons/pushd/Cargo.toml | 2 +- crates/daemons/voice-ingress/Cargo.toml | 2 +- crates/delta/Cargo.toml | 2 +- crates/services/autumn/Cargo.toml | 2 +- crates/services/gifbox/Cargo.toml | 2 +- crates/services/january/Cargo.toml | 2 +- version.txt | 2 +- 23 files changed, 55 insertions(+), 48 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 7f052622..ad0fd276 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.1" + ".": "0.13.2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c46f2e..d44ac826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.13.2](https://github.com/stoatchat/stoatchat/compare/v0.13.1...v0.13.2) (2026-05-11) + + +### Bug Fixes + +* update default exchange to `revolt.default` ([#746](https://github.com/stoatchat/stoatchat/issues/746)) ([fcb8091](https://github.com/stoatchat/stoatchat/commit/fcb8091cd7a00d7f26c798daa33aae4b923b2a8b)) + ## [0.13.1](https://github.com/stoatchat/stoatchat/compare/v0.13.0...v0.13.1) (2026-05-10) diff --git a/Cargo.lock b/Cargo.lock index 8eeb17c5..619c5afd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7504,7 +7504,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.13.1" +version = "0.13.2" dependencies = [ "axum", "axum-macros", @@ -7545,7 +7545,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.13.1" +version = "0.13.2" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7576,7 +7576,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.13.1" +version = "0.13.2" dependencies = [ "indexmap 2.13.1", "lru", @@ -7585,7 +7585,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.13.1" +version = "0.13.2" dependencies = [ "async-std", "cached", @@ -7602,7 +7602,7 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.13.1" +version = "0.13.2" dependencies = [ "futures-lite", "iso8601-timestamp", @@ -7622,7 +7622,7 @@ dependencies = [ [[package]] name = "revolt-database" -version = "0.13.1" +version = "0.13.2" dependencies = [ "amqprs", "async-lock 2.8.0", @@ -7673,7 +7673,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.13.1" +version = "0.13.2" dependencies = [ "amqprs", "async-channel 2.5.0", @@ -7722,7 +7722,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.13.1" +version = "0.13.2" dependencies = [ "aes-gcm", "anyhow", @@ -7750,7 +7750,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.13.1" +version = "0.13.2" dependencies = [ "axum", "axum-extra", @@ -7773,7 +7773,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.13.1" +version = "0.13.2" dependencies = [ "async-recursion", "axum", @@ -7803,7 +7803,7 @@ dependencies = [ [[package]] name = "revolt-models" -version = "0.13.1" +version = "0.13.2" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7822,14 +7822,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.13.1" +version = "0.13.2" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.13.1" +version = "0.13.2" dependencies = [ "async-std", "async-trait", @@ -7844,7 +7844,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.13.1" +version = "0.13.2" dependencies = [ "async-std", "log", @@ -7856,7 +7856,7 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.13.1" +version = "0.13.2" dependencies = [ "amqprs", "anyhow", @@ -7887,7 +7887,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.13.1" +version = "0.13.2" dependencies = [ "async-trait", "authifier", @@ -7904,7 +7904,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.13.1" +version = "0.13.2" dependencies = [ "axum", "log", @@ -7920,7 +7920,7 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.13.1" +version = "0.13.2" dependencies = [ "amqprs", "async-std", diff --git a/Cargo.toml b/Cargo.toml index 3ee8db8f..89d36808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,13 +192,13 @@ futures-lite = "2.6.1" vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.13.1", path = "crates/core/coalesced" } -revolt-config = { version = "0.13.1", path = "crates/core/config" } -revolt-database = { version = "0.13.1", path = "crates/core/database" } -revolt-files = { version = "0.13.1", path = "crates/core/files" } -revolt-models = { version = "0.13.1", path = "crates/core/models" } -revolt-parser = { version = "0.13.1", path = "crates/core/parser" } -revolt-permissions = { version = "0.13.1", path = "crates/core/permissions" } -revolt-presence = { version = "0.13.1", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.13.1", path = "crates/core/ratelimits" } -revolt-result = { version = "0.13.1", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.2", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.2", path = "crates/core/config" } +revolt-database = { version = "0.13.2", path = "crates/core/database" } +revolt-files = { version = "0.13.2", path = "crates/core/files" } +revolt-models = { version = "0.13.2", path = "crates/core/models" } +revolt-parser = { version = "0.13.2", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.2", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.2", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.2", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.2", path = "crates/core/result" } diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 9018e965..3d3dca66 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.13.1" +version = "0.13.2" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index 589afa70..1f071bb8 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index 48921d77..8c8ed893 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index dd60ccd7..ba4b80ea 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index 6910f6c1..0b4f0333 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index 696107e8..5bfffbe0 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index 8bd9cf03..9637e86a 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index 675b569b..e9cd8511 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index 86043d64..07fe66e6 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index c917269d..bedcdd42 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.13.1" +version = "0.13.2" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 12e358f1..8543a07a 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index f9b09ef1..065e0dc6 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.13.1" +version = "0.13.2" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index 2f1ba2f7..2500d53e 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index ebd90a58..4bd679e6 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.13.1" +version = "0.13.2" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index ef007dae..b3914455 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.13.1" +version = "0.13.2" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index 549ecdcd..a12e07cd 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 355952cf..7e804be9 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index 76d8e5c3..749ceac7 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.13.1" +version = "0.13.2" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/version.txt b/version.txt index c317a918..9beb74d4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.13.1 +0.13.2 From 7647cfc8d93aba99f5faef13eb3d970097540d76 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 15 May 2026 12:17:36 -0700 Subject: [PATCH 19/39] fix: don't automatically set up rabbitmq in delta (#749) fix: don't declare queues which seem to cause the backend to crash in prod for now, these exchanges/queues/bindings will need to be declared manually. Hopefully the lapin rewrite will fix this. Signed-off-by: IAmTomahawkx --- crates/core/database/src/amqp/amqp.rs | 2 +- crates/delta/src/main.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/core/database/src/amqp/amqp.rs b/crates/core/database/src/amqp/amqp.rs index 16326045..e0c644e5 100644 --- a/crates/core/database/src/amqp/amqp.rs +++ b/crates/core/database/src/amqp/amqp.rs @@ -47,7 +47,7 @@ impl AMQP { .expect("Failed to open RabbitMQ channel"); let mut resp = AMQP::new(connection, channel); - resp.configure_channels().await?; + //resp.configure_channels().await?; Ok(resp) } diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index e3cf43a4..15aa2d3b 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -120,9 +120,9 @@ pub async fn web() -> Rocket { .expect("Failed to declare exchange"); let mut amqp = AMQP::new(connection, channel); - amqp.configure_channels() - .await - .expect("Failed to configure channels"); + // amqp.configure_channels() + // .await + // .expect("Failed to configure channels"); // Launch background task workers revolt_database::tasks::start_workers(db.clone(), amqp.clone()); From ab9b8ccfca5235973605406ace483b3f7dfe9ba1 Mon Sep 17 00:00:00 2001 From: "stoat-release[bot]" <245062572+stoat-release[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 12:22:27 -0700 Subject: [PATCH 20/39] chore(main): release 0.13.3 (#750) * chore(main): release 0.13.3 * chore: update Cargo.lock Signed-off-by: github-actions[bot] --------- Signed-off-by: github-actions[bot] Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++ Cargo.lock | 36 ++++++++++++------------- Cargo.toml | 20 +++++++------- crates/bonfire/Cargo.toml | 2 +- crates/core/coalesced/Cargo.toml | 2 +- crates/core/config/Cargo.toml | 2 +- crates/core/database/Cargo.toml | 2 +- crates/core/files/Cargo.toml | 2 +- crates/core/models/Cargo.toml | 2 +- crates/core/parser/Cargo.toml | 2 +- crates/core/permissions/Cargo.toml | 2 +- crates/core/presence/Cargo.toml | 2 +- crates/core/ratelimits/Cargo.toml | 2 +- crates/core/result/Cargo.toml | 2 +- crates/daemons/crond/Cargo.toml | 2 +- crates/daemons/pushd/Cargo.toml | 2 +- crates/daemons/voice-ingress/Cargo.toml | 2 +- crates/delta/Cargo.toml | 2 +- crates/services/autumn/Cargo.toml | 2 +- crates/services/gifbox/Cargo.toml | 2 +- crates/services/january/Cargo.toml | 2 +- version.txt | 2 +- 23 files changed, 56 insertions(+), 48 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ad0fd276..e408c370 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.2" + ".": "0.13.3" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d44ac826..9581ec4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.13.3](https://github.com/stoatchat/stoatchat/compare/v0.13.2...v0.13.3) (2026-05-15) + + +### Bug Fixes + +* don't automatically set up rabbitmq in delta ([#749](https://github.com/stoatchat/stoatchat/issues/749)) ([7647cfc](https://github.com/stoatchat/stoatchat/commit/7647cfc8d93aba99f5faef13eb3d970097540d76)) +* don't declare queues which seem to cause the backend to crash in prod ([7647cfc](https://github.com/stoatchat/stoatchat/commit/7647cfc8d93aba99f5faef13eb3d970097540d76)) + ## [0.13.2](https://github.com/stoatchat/stoatchat/compare/v0.13.1...v0.13.2) (2026-05-11) diff --git a/Cargo.lock b/Cargo.lock index 619c5afd..c7050011 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7504,7 +7504,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.13.2" +version = "0.13.3" dependencies = [ "axum", "axum-macros", @@ -7545,7 +7545,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.13.2" +version = "0.13.3" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7576,7 +7576,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.13.2" +version = "0.13.3" dependencies = [ "indexmap 2.13.1", "lru", @@ -7585,7 +7585,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.13.2" +version = "0.13.3" dependencies = [ "async-std", "cached", @@ -7602,7 +7602,7 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.13.2" +version = "0.13.3" dependencies = [ "futures-lite", "iso8601-timestamp", @@ -7622,7 +7622,7 @@ dependencies = [ [[package]] name = "revolt-database" -version = "0.13.2" +version = "0.13.3" dependencies = [ "amqprs", "async-lock 2.8.0", @@ -7673,7 +7673,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.13.2" +version = "0.13.3" dependencies = [ "amqprs", "async-channel 2.5.0", @@ -7722,7 +7722,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.13.2" +version = "0.13.3" dependencies = [ "aes-gcm", "anyhow", @@ -7750,7 +7750,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.13.2" +version = "0.13.3" dependencies = [ "axum", "axum-extra", @@ -7773,7 +7773,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.13.2" +version = "0.13.3" dependencies = [ "async-recursion", "axum", @@ -7803,7 +7803,7 @@ dependencies = [ [[package]] name = "revolt-models" -version = "0.13.2" +version = "0.13.3" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7822,14 +7822,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.13.2" +version = "0.13.3" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.13.2" +version = "0.13.3" dependencies = [ "async-std", "async-trait", @@ -7844,7 +7844,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.13.2" +version = "0.13.3" dependencies = [ "async-std", "log", @@ -7856,7 +7856,7 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.13.2" +version = "0.13.3" dependencies = [ "amqprs", "anyhow", @@ -7887,7 +7887,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.13.2" +version = "0.13.3" dependencies = [ "async-trait", "authifier", @@ -7904,7 +7904,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.13.2" +version = "0.13.3" dependencies = [ "axum", "log", @@ -7920,7 +7920,7 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.13.2" +version = "0.13.3" dependencies = [ "amqprs", "async-std", diff --git a/Cargo.toml b/Cargo.toml index 89d36808..7bc0d352 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,13 +192,13 @@ futures-lite = "2.6.1" vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.13.2", path = "crates/core/coalesced" } -revolt-config = { version = "0.13.2", path = "crates/core/config" } -revolt-database = { version = "0.13.2", path = "crates/core/database" } -revolt-files = { version = "0.13.2", path = "crates/core/files" } -revolt-models = { version = "0.13.2", path = "crates/core/models" } -revolt-parser = { version = "0.13.2", path = "crates/core/parser" } -revolt-permissions = { version = "0.13.2", path = "crates/core/permissions" } -revolt-presence = { version = "0.13.2", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.13.2", path = "crates/core/ratelimits" } -revolt-result = { version = "0.13.2", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.3", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.3", path = "crates/core/config" } +revolt-database = { version = "0.13.3", path = "crates/core/database" } +revolt-files = { version = "0.13.3", path = "crates/core/files" } +revolt-models = { version = "0.13.3", path = "crates/core/models" } +revolt-parser = { version = "0.13.3", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.3", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.3", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.3", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.3", path = "crates/core/result" } diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 3d3dca66..83a02513 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.13.2" +version = "0.13.3" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index 1f071bb8..8ed16584 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index 8c8ed893..464c4e1b 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index ba4b80ea..26e12e8d 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index 0b4f0333..8c106597 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index 5bfffbe0..e27a0eb8 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index 9637e86a..5612294e 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index e9cd8511..d39b4a01 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index 07fe66e6..4d3505c7 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index bedcdd42..30d32e2f 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.13.2" +version = "0.13.3" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 8543a07a..7ff40fca 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index 065e0dc6..8946ea73 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.13.2" +version = "0.13.3" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index 2500d53e..65fb5cf4 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index 4bd679e6..5b680a04 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.13.2" +version = "0.13.3" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index b3914455..d57b2d1f 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.13.2" +version = "0.13.3" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index a12e07cd..11987096 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 7e804be9..32a93770 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index 749ceac7..fd89d808 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.13.2" +version = "0.13.3" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/version.txt b/version.txt index 9beb74d4..288adf53 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.13.2 +0.13.3 From 6cfee1f601c1e084df7c8f1e7a5e8a560d1dd514 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sat, 16 May 2026 12:23:45 -0500 Subject: [PATCH 21/39] fix: add TLS feature to livekit-api crate (#753) --- Cargo.lock | 47 ++++++++++++++++++++++++++++++++- crates/core/database/Cargo.toml | 2 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7050011..d34b40dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -570,7 +570,7 @@ dependencies = [ "futures-util", "log", "pin-project-lite 0.2.17", - "tungstenite", + "tungstenite 0.17.3", ] [[package]] @@ -5048,11 +5048,14 @@ dependencies = [ "prost", "rand 0.9.2", "reqwest 0.12.28", + "rustls-native-certs 0.6.3", "scopeguard", "serde", "serde_json", "sha2", "thiserror 2.0.18", + "tokio-rustls 0.24.1", + "tokio-tungstenite", "url", ] @@ -7419,16 +7422,22 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.9.0", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite 0.2.17", + "quinn", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", "tokio 1.51.0", + "tokio-rustls 0.26.4", "tower", "tower-http 0.6.8", "tower-service", @@ -7665,6 +7674,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "tokio 1.51.0", "ulid 1.2.1", "unicode-segmentation", "url-escape", @@ -9891,6 +9901,21 @@ dependencies = [ "tokio 1.51.0", ] +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio 1.51.0", + "tokio-rustls 0.24.1", + "tungstenite 0.20.1", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -10146,6 +10171,26 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes 1.11.1", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.21.12", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + [[package]] name = "typed-builder" version = "0.22.0" diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index 26e12e8d..dbaf3575 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -98,6 +98,6 @@ authifier = { workspace = true } amqprs = { workspace = true } # Voice -livekit-api = { workspace = true, optional = true } +livekit-api = { workspace = true, features = ["rustls-tls-native-roots"], optional = true } livekit-protocol = { workspace = true, optional = true } livekit-runtime = { workspace = true, features = ["tokio"], optional = true } From ee4575470bbad9608031f574e9e432535530f4dd Mon Sep 17 00:00:00 2001 From: "stoat-release[bot]" <245062572+stoat-release[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 18:27:44 +0100 Subject: [PATCH 22/39] chore(main): release 0.13.4 (#754) Co-authored-by: github-actions[bot] Signed-off-by: github-actions[bot] --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++ Cargo.lock | 37 ++++++++++++------------- Cargo.toml | 20 ++++++------- crates/bonfire/Cargo.toml | 2 +- crates/core/coalesced/Cargo.toml | 2 +- crates/core/config/Cargo.toml | 2 +- crates/core/database/Cargo.toml | 2 +- crates/core/files/Cargo.toml | 2 +- crates/core/models/Cargo.toml | 2 +- crates/core/parser/Cargo.toml | 2 +- crates/core/permissions/Cargo.toml | 2 +- crates/core/presence/Cargo.toml | 2 +- crates/core/ratelimits/Cargo.toml | 2 +- crates/core/result/Cargo.toml | 2 +- crates/daemons/crond/Cargo.toml | 2 +- crates/daemons/pushd/Cargo.toml | 2 +- crates/daemons/voice-ingress/Cargo.toml | 2 +- crates/delta/Cargo.toml | 2 +- crates/services/autumn/Cargo.toml | 2 +- crates/services/gifbox/Cargo.toml | 2 +- crates/services/january/Cargo.toml | 2 +- version.txt | 2 +- 23 files changed, 55 insertions(+), 49 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e408c370..60633e47 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.3" + ".": "0.13.4" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9581ec4a..18d08823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.13.4](https://github.com/stoatchat/stoatchat/compare/v0.13.3...v0.13.4) (2026-05-16) + + +### Bug Fixes + +* add TLS feature to livekit-api crate ([#753](https://github.com/stoatchat/stoatchat/issues/753)) ([6cfee1f](https://github.com/stoatchat/stoatchat/commit/6cfee1f601c1e084df7c8f1e7a5e8a560d1dd514)) + ## [0.13.3](https://github.com/stoatchat/stoatchat/compare/v0.13.2...v0.13.3) (2026-05-15) diff --git a/Cargo.lock b/Cargo.lock index d34b40dc..e3484166 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7513,7 +7513,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.13.3" +version = "0.13.4" dependencies = [ "axum", "axum-macros", @@ -7554,7 +7554,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.13.3" +version = "0.13.4" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7585,7 +7585,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.13.3" +version = "0.13.4" dependencies = [ "indexmap 2.13.1", "lru", @@ -7594,7 +7594,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.13.3" +version = "0.13.4" dependencies = [ "async-std", "cached", @@ -7611,7 +7611,7 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.13.3" +version = "0.13.4" dependencies = [ "futures-lite", "iso8601-timestamp", @@ -7631,7 +7631,7 @@ dependencies = [ [[package]] name = "revolt-database" -version = "0.13.3" +version = "0.13.4" dependencies = [ "amqprs", "async-lock 2.8.0", @@ -7674,7 +7674,6 @@ dependencies = [ "schemars", "serde", "serde_json", - "tokio 1.51.0", "ulid 1.2.1", "unicode-segmentation", "url-escape", @@ -7683,7 +7682,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.13.3" +version = "0.13.4" dependencies = [ "amqprs", "async-channel 2.5.0", @@ -7732,7 +7731,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.13.3" +version = "0.13.4" dependencies = [ "aes-gcm", "anyhow", @@ -7760,7 +7759,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.13.3" +version = "0.13.4" dependencies = [ "axum", "axum-extra", @@ -7783,7 +7782,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.13.3" +version = "0.13.4" dependencies = [ "async-recursion", "axum", @@ -7813,7 +7812,7 @@ dependencies = [ [[package]] name = "revolt-models" -version = "0.13.3" +version = "0.13.4" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7832,14 +7831,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.13.3" +version = "0.13.4" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.13.3" +version = "0.13.4" dependencies = [ "async-std", "async-trait", @@ -7854,7 +7853,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.13.3" +version = "0.13.4" dependencies = [ "async-std", "log", @@ -7866,7 +7865,7 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.13.3" +version = "0.13.4" dependencies = [ "amqprs", "anyhow", @@ -7897,7 +7896,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.13.3" +version = "0.13.4" dependencies = [ "async-trait", "authifier", @@ -7914,7 +7913,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.13.3" +version = "0.13.4" dependencies = [ "axum", "log", @@ -7930,7 +7929,7 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.13.3" +version = "0.13.4" dependencies = [ "amqprs", "async-std", diff --git a/Cargo.toml b/Cargo.toml index 7bc0d352..5cb9b347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,13 +192,13 @@ futures-lite = "2.6.1" vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.13.3", path = "crates/core/coalesced" } -revolt-config = { version = "0.13.3", path = "crates/core/config" } -revolt-database = { version = "0.13.3", path = "crates/core/database" } -revolt-files = { version = "0.13.3", path = "crates/core/files" } -revolt-models = { version = "0.13.3", path = "crates/core/models" } -revolt-parser = { version = "0.13.3", path = "crates/core/parser" } -revolt-permissions = { version = "0.13.3", path = "crates/core/permissions" } -revolt-presence = { version = "0.13.3", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.13.3", path = "crates/core/ratelimits" } -revolt-result = { version = "0.13.3", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.4", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.4", path = "crates/core/config" } +revolt-database = { version = "0.13.4", path = "crates/core/database" } +revolt-files = { version = "0.13.4", path = "crates/core/files" } +revolt-models = { version = "0.13.4", path = "crates/core/models" } +revolt-parser = { version = "0.13.4", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.4", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.4", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.4", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.4", path = "crates/core/result" } diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 83a02513..b67410dd 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.13.3" +version = "0.13.4" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index 8ed16584..f8e3e011 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index 464c4e1b..df4cbf37 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index dbaf3575..987bfa86 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index 8c106597..1b0252a8 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index e27a0eb8..2139f159 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index 5612294e..2f0d6278 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index d39b4a01..c9e775fe 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index 4d3505c7..faa0f0a2 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index 30d32e2f..bf367893 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.13.3" +version = "0.13.4" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 7ff40fca..736fbd33 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index 8946ea73..f17e5e77 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.13.3" +version = "0.13.4" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index 65fb5cf4..94f610f7 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index 5b680a04..50218d48 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.13.3" +version = "0.13.4" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index d57b2d1f..431a80a6 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.13.3" +version = "0.13.4" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index 11987096..aa77a337 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 32a93770..03e3ee6e 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index fd89d808..d8f1dac9 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.13.3" +version = "0.13.4" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/version.txt b/version.txt index 288adf53..dffa40ec 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.13.3 +0.13.4 From 19ee535f45e76512c17b38206bf3b61b39c6d4df Mon Sep 17 00:00:00 2001 From: Tom Date: Sun, 17 May 2026 10:55:38 -0700 Subject: [PATCH 23/39] Merge commit from fork * fix: cache dns & block more ranges * fix: idle time instead of ttl Signed-off-by: IAmTomahawkx --------- Signed-off-by: IAmTomahawkx --- crates/services/january/src/requests.rs | 120 ++++++++++++++++++++---- 1 file changed, 104 insertions(+), 16 deletions(-) diff --git a/crates/services/january/src/requests.rs b/crates/services/january/src/requests.rs index b63f203e..c818619b 100644 --- a/crates/services/january/src/requests.rs +++ b/crates/services/january/src/requests.rs @@ -4,6 +4,7 @@ use mime::Mime; use pdk_ip_filter_lib::IpFilter; use regex::Regex; use reqwest::{ + dns::{Addrs, Name, Resolve}, header::{self, CONTENT_TYPE}, redirect, Client, Response, }; @@ -11,6 +12,7 @@ use revolt_config::{config, report_internal_error}; use revolt_files::{create_thumbnail, decode_image, image_size_vec, is_valid_image, video_size}; use revolt_models::v0::{Embed, Image, ImageSize, Video}; use revolt_result::{create_error, Error, Result, ToRevoltError}; +use std::net::{IpAddr, SocketAddr}; use std::{ io::{Cursor, Write}, str::FromStr, @@ -21,6 +23,7 @@ use url::{Host, Url}; lazy_static! { /// Request client static ref CLIENT: Client = reqwest::Client::builder() + .dns_resolver(CachedDnsResolver {}) .timeout(Duration::from_secs(10)) // TODO config .connect_timeout(Duration::from_secs(5)) // TODO config .redirect(redirect::Policy::none()) @@ -58,18 +61,73 @@ lazy_static! { .time_to_live(Duration::from_secs(60)) // For up to 1 minute .build(); + static ref DNS_CACHE: moka::future::Cache> = moka::future::Cache::builder() + .max_capacity(10_000) + .time_to_idle(Duration::from_secs(30)) + .build(); + static ref IP_BLOCKLIST: IpFilter = IpFilter::block(&[ + "0.0.0.0/8", "10.0.0.0/8", "192.168.0.0/16", "127.0.0.0/8", "172.16.0.0/12", "169.254.0.0/16", "::1", - "fc00::/7" + "fc00::/7", ] ).unwrap(); } +#[derive(Clone)] +pub struct IPRequest { + url: Url, + ip: IpAddr, + pub blocked: bool, +} + +impl From for Url { + fn from(value: IPRequest) -> Self { + let mut url = value.url.clone(); + url.set_host(Some(&value.ip.to_string())) + .map(|_| url) + .unwrap_or(value.url) + } +} + +struct CachedDnsResolver {} + +impl reqwest::dns::Resolve for CachedDnsResolver { + fn resolve(&self, name: Name) -> reqwest::dns::Resolving { + Box::pin(async move { + { + if let Some(addrs) = DNS_CACHE.get(&name.as_str().to_string()).await { + let resp: Addrs = Box::new(addrs.clone().into_iter()); + return Ok(resp); + } + } + + let mut lookup = name.as_str().to_string(); + if !lookup.contains(":") { + lookup += ":0"; + } + + let fallback: Vec = tokio::net::lookup_host(&lookup) + .await + .map_err(|e| -> Box { Box::new(e) })? + .collect(); + + { + DNS_CACHE + .insert(name.as_str().to_string().clone(), fallback.clone()) + .await; + let addrs: Addrs = Box::new(fallback.clone().into_iter()); + Ok(addrs) + } + }) + } +} + /// Information about a successful request pub struct Request { response: Response, @@ -275,7 +333,12 @@ impl Request { let mut url = url; let url_host_str = url.host_str().ok_or(create_error!(ProxyError))?.to_string(); - Request::url_is_blacklisted(&url).await?; + let mut blocker = Request::url_is_blacklisted(&url).await?; + + if blocker.blocked { + return Err(create_error!(InvalidOperation)); + } + let mut redirect_count = 0; loop { @@ -304,9 +367,13 @@ impl Request { let location = location.to_str().map_err(|_| create_error!(ProxyError))?; url = Url::from_str(location).to_internal_error()?; - if !Request::url_is_blacklisted(&url).await? { - continue; + blocker = Request::url_is_blacklisted(&url).await?; + + if blocker.blocked { + return Err(create_error!(InvalidOperation)); } + + continue; } else { return Err(create_error!(ProxyError)); } @@ -351,17 +418,25 @@ impl Request { Ok(Request::exists(proper_url).await) } - pub async fn url_is_blacklisted(url: &Url) -> Result { + pub async fn url_is_blacklisted(url: &Url) -> Result { + let resolved_address: IpAddr; + if let Some(host) = url.host() { match host { Host::Ipv4(ipv4) => { - let url_str = ipv4.to_string(); - if !IP_BLOCKLIST.is_allowed(&url_str) { + resolved_address = ipv4.into(); + if !IP_BLOCKLIST.is_allowed(&ipv4.to_string()) { + return Err(create_error!(InvalidOperation)); + } + } + Host::Ipv6(ipv6) => { + resolved_address = ipv6.into(); + if !IP_BLOCKLIST.is_allowed(&ipv6.to_string()) { return Err(create_error!(InvalidOperation)); } } Host::Domain(domain) => { - let mut domain = domain.to_string(); + let domain = domain.to_string(); let config = config().await; @@ -372,14 +447,22 @@ impl Request { return Err(create_error!(InvalidOperation)); } - if !domain.contains(":") { - domain += ":80"; - } - // Second step: resolve the IP and check the blocklist - if let Ok(mut resolved_ip) = tokio::net::lookup_host(domain.clone()).await { + let resolver = CachedDnsResolver {}; + if let Ok(mut resolved_ip) = resolver + .resolve( + Name::from_str(&domain) + .map_err(|_| create_error!(ProxyError)) + .unwrap(), + ) + .await + { if let Some(resolved_ip) = resolved_ip.next() { - if !IP_BLOCKLIST.is_allowed(&resolved_ip.ip().to_string()) { + resolved_address = resolved_ip.ip(); + let resolved_string = resolved_address.to_string(); + if !IP_BLOCKLIST.is_allowed(&resolved_string) + || resolved_string.contains("::ffff:") + { return Err(create_error!(InvalidOperation)); } } else { @@ -389,10 +472,15 @@ impl Request { return Err(create_error!(ProxyError)); } } - _ => (), } + } else { + return Err(create_error!(ProxyError)); }; - Ok(false) + Ok(IPRequest { + url: url.clone(), + ip: resolved_address, + blocked: false, + }) } } From c902077cf51076fee11712eb732dc8a8f786fc4b Mon Sep 17 00:00:00 2001 From: Angelo Kontaxis Date: Sun, 17 May 2026 18:59:56 +0100 Subject: [PATCH 24/39] fix: dont panic on hash missing when deleting files (#755) Signed-off-by: Zomatree --- .../daemons/crond/src/tasks/file_deletion.rs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/daemons/crond/src/tasks/file_deletion.rs b/crates/daemons/crond/src/tasks/file_deletion.rs index 912173b3..42942bf3 100644 --- a/crates/daemons/crond/src/tasks/file_deletion.rs +++ b/crates/daemons/crond/src/tasks/file_deletion.rs @@ -11,22 +11,24 @@ pub async fn task(db: Database) -> Result<()> { let files = db.fetch_deleted_attachments().await?; for file in files { - let count = db - .count_file_hash_references(file.hash.as_ref().expect("no `hash` present")) - .await?; - - // No other files reference this file on disk anymore - if count <= 1 { - let file_hash = db - .fetch_attachment_hash(file.hash.as_ref().expect("no `hash` present")) + if let Some(hash) = &file.hash { + let count = db + .count_file_hash_references(hash) .await?; - // Delete from S3 - delete_from_s3(&file_hash.bucket_id, &file_hash.path).await?; + // No other files reference this file on disk anymore + if count <= 1 { + let file_hash = db + .fetch_attachment_hash(hash) + .await?; - // Delete the hash - db.delete_attachment_hash(&file_hash.id).await?; - info!("Deleted file hash {}", file_hash.id); + // Delete from S3 + delete_from_s3(&file_hash.bucket_id, &file_hash.path).await?; + + // Delete the hash + db.delete_attachment_hash(&file_hash.id).await?; + info!("Deleted file hash {}", file_hash.id); + } } // Delete the file From 6c920de03a275e73f1f632b6cd6bc924acb66324 Mon Sep 17 00:00:00 2001 From: "stoat-release[bot]" <245062572+stoat-release[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 11:02:21 -0700 Subject: [PATCH 25/39] chore(main): release 0.13.5 (#759) * chore(main): release 0.13.5 * chore: update Cargo.lock Signed-off-by: github-actions[bot] --------- Signed-off-by: github-actions[bot] Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++ Cargo.lock | 36 ++++++++++++------------- Cargo.toml | 20 +++++++------- crates/bonfire/Cargo.toml | 2 +- crates/core/coalesced/Cargo.toml | 2 +- crates/core/config/Cargo.toml | 2 +- crates/core/database/Cargo.toml | 2 +- crates/core/files/Cargo.toml | 2 +- crates/core/models/Cargo.toml | 2 +- crates/core/parser/Cargo.toml | 2 +- crates/core/permissions/Cargo.toml | 2 +- crates/core/presence/Cargo.toml | 2 +- crates/core/ratelimits/Cargo.toml | 2 +- crates/core/result/Cargo.toml | 2 +- crates/daemons/crond/Cargo.toml | 2 +- crates/daemons/pushd/Cargo.toml | 2 +- crates/daemons/voice-ingress/Cargo.toml | 2 +- crates/delta/Cargo.toml | 2 +- crates/services/autumn/Cargo.toml | 2 +- crates/services/gifbox/Cargo.toml | 2 +- crates/services/january/Cargo.toml | 2 +- version.txt | 2 +- 23 files changed, 55 insertions(+), 48 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 60633e47..429a83fc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.4" + ".": "0.13.5" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 18d08823..1921756b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.13.5](https://github.com/stoatchat/stoatchat/compare/v0.13.4...v0.13.5) (2026-05-17) + + +### Bug Fixes + +* dont panic on hash missing when deleting files ([#755](https://github.com/stoatchat/stoatchat/issues/755)) ([c902077](https://github.com/stoatchat/stoatchat/commit/c902077cf51076fee11712eb732dc8a8f786fc4b)) + ## [0.13.4](https://github.com/stoatchat/stoatchat/compare/v0.13.3...v0.13.4) (2026-05-16) diff --git a/Cargo.lock b/Cargo.lock index e3484166..9bccaccd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7513,7 +7513,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.13.4" +version = "0.13.5" dependencies = [ "axum", "axum-macros", @@ -7554,7 +7554,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.13.4" +version = "0.13.5" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7585,7 +7585,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.13.4" +version = "0.13.5" dependencies = [ "indexmap 2.13.1", "lru", @@ -7594,7 +7594,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.13.4" +version = "0.13.5" dependencies = [ "async-std", "cached", @@ -7611,7 +7611,7 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.13.4" +version = "0.13.5" dependencies = [ "futures-lite", "iso8601-timestamp", @@ -7631,7 +7631,7 @@ dependencies = [ [[package]] name = "revolt-database" -version = "0.13.4" +version = "0.13.5" dependencies = [ "amqprs", "async-lock 2.8.0", @@ -7682,7 +7682,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.13.4" +version = "0.13.5" dependencies = [ "amqprs", "async-channel 2.5.0", @@ -7731,7 +7731,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.13.4" +version = "0.13.5" dependencies = [ "aes-gcm", "anyhow", @@ -7759,7 +7759,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.13.4" +version = "0.13.5" dependencies = [ "axum", "axum-extra", @@ -7782,7 +7782,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.13.4" +version = "0.13.5" dependencies = [ "async-recursion", "axum", @@ -7812,7 +7812,7 @@ dependencies = [ [[package]] name = "revolt-models" -version = "0.13.4" +version = "0.13.5" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7831,14 +7831,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.13.4" +version = "0.13.5" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.13.4" +version = "0.13.5" dependencies = [ "async-std", "async-trait", @@ -7853,7 +7853,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.13.4" +version = "0.13.5" dependencies = [ "async-std", "log", @@ -7865,7 +7865,7 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.13.4" +version = "0.13.5" dependencies = [ "amqprs", "anyhow", @@ -7896,7 +7896,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.13.4" +version = "0.13.5" dependencies = [ "async-trait", "authifier", @@ -7913,7 +7913,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.13.4" +version = "0.13.5" dependencies = [ "axum", "log", @@ -7929,7 +7929,7 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.13.4" +version = "0.13.5" dependencies = [ "amqprs", "async-std", diff --git a/Cargo.toml b/Cargo.toml index 5cb9b347..3b497784 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,13 +192,13 @@ futures-lite = "2.6.1" vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.13.4", path = "crates/core/coalesced" } -revolt-config = { version = "0.13.4", path = "crates/core/config" } -revolt-database = { version = "0.13.4", path = "crates/core/database" } -revolt-files = { version = "0.13.4", path = "crates/core/files" } -revolt-models = { version = "0.13.4", path = "crates/core/models" } -revolt-parser = { version = "0.13.4", path = "crates/core/parser" } -revolt-permissions = { version = "0.13.4", path = "crates/core/permissions" } -revolt-presence = { version = "0.13.4", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.13.4", path = "crates/core/ratelimits" } -revolt-result = { version = "0.13.4", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.5", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.5", path = "crates/core/config" } +revolt-database = { version = "0.13.5", path = "crates/core/database" } +revolt-files = { version = "0.13.5", path = "crates/core/files" } +revolt-models = { version = "0.13.5", path = "crates/core/models" } +revolt-parser = { version = "0.13.5", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.5", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.5", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.5", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.5", path = "crates/core/result" } diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index b67410dd..c0fbe500 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.13.4" +version = "0.13.5" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index f8e3e011..7c8bdcb9 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index df4cbf37..c8be6a4f 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index 987bfa86..5031009d 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index 1b0252a8..a17be124 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index 2139f159..75976fc5 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index 2f0d6278..e73e0994 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index c9e775fe..3ffbf1b1 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index faa0f0a2..f48182f7 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index bf367893..7d5be0c1 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.13.4" +version = "0.13.5" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 736fbd33..25caa496 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index f17e5e77..8b1d0376 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.13.4" +version = "0.13.5" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index 94f610f7..c47eb11a 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index 50218d48..05ea4a60 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.13.4" +version = "0.13.5" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 431a80a6..a74c5547 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.13.4" +version = "0.13.5" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index aa77a337..c45d8522 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 03e3ee6e..5fbcba08 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index d8f1dac9..55ae526e 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.13.4" +version = "0.13.5" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/version.txt b/version.txt index dffa40ec..c37136a8 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.13.4 +0.13.5 From 298742dbad4eafae356f976c56b9db23904b0c3a Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 17 May 2026 14:41:44 -0500 Subject: [PATCH 26/39] fix: include `minio` region as tests need it (#761) --- compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/compose.yml b/compose.yml index bf2c2ca1..b7e73689 100644 --- a/compose.yml +++ b/compose.yml @@ -34,6 +34,7 @@ services: environment: MINIO_ROOT_USER: minioautumn MINIO_ROOT_PASSWORD: minioautumn + MINIO_REGION: minio volumes: - ./.data/minio:/data ports: From 26a8692677c5eeeff37f35f1f267f2ae3eb0d81b Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Sun, 17 May 2026 14:54:57 -0500 Subject: [PATCH 27/39] ci: ignore test errors on main (#763) --- .github/workflows/rust.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 08e210a3..3b7a78d2 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -37,6 +37,7 @@ jobs: - name: Reference Test env: TEST_DB: REFERENCE + continue-on-error: ${{ github.ref_name == 'main' }} run: | mise test @@ -44,6 +45,7 @@ jobs: env: TEST_DB: MONGODB MONGODB: mongodb://localhost + continue-on-error: ${{ github.ref_name == 'main' }} run: | mise test From 494c8b7cabaae2a51039a7a5b559d5e2e5279554 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 18 May 2026 10:51:19 -0700 Subject: [PATCH 28/39] fix: Use proper headers to determine IP when not behind cloudflare (#764) Signed-off-by: IAmTomahawkx --- crates/core/ratelimits/src/rocket.rs | 6 +++--- crates/delta/src/main.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/core/ratelimits/src/rocket.rs b/crates/core/ratelimits/src/rocket.rs index 046317bd..f790abb9 100644 --- a/crates/core/ratelimits/src/rocket.rs +++ b/crates/core/ratelimits/src/rocket.rs @@ -1,12 +1,12 @@ use async_trait::async_trait; use log::info; +use revolt_config::config; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::uri::Origin; use rocket::http::{Method, Status}; use rocket::request::{FromRequest, Outcome}; use rocket::serde::json::Json; use rocket::{Data, Request, Response, State}; -use revolt_config::config; use revolt_rocket_okapi::r#gen::OpenApiGenerator; use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; @@ -28,8 +28,8 @@ pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage) -> String { request - .remote() - .map(|x| x.ip().to_string()) + .client_ip() + .map(|r| r.to_string()) .unwrap_or_default() } diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 15aa2d3b..41f8fc02 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -152,6 +152,7 @@ pub async fn web() -> Rocket { limits: rocket::data::Limits::default().limit("string", 5.megabytes()), address: Ipv4Addr::new(0, 0, 0, 0).into(), port: 14702, + ip_header: Some("X-Forwarded-For".into()), ..Default::default() }) } From 2871632382395cb20cbe0047c542d3ac31ff3f03 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 18 May 2026 10:56:01 -0700 Subject: [PATCH 29/39] fix: voice ingress crashing due to new Result in AMQP::new_auto() (#765) Signed-off-by: IAmTomahawkx --- crates/daemons/voice-ingress/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/daemons/voice-ingress/src/main.rs b/crates/daemons/voice-ingress/src/main.rs index 45191c2c..727e1a25 100644 --- a/crates/daemons/voice-ingress/src/main.rs +++ b/crates/daemons/voice-ingress/src/main.rs @@ -13,7 +13,7 @@ mod guard; async fn main() -> Result<(), rocket::Error> { revolt_config::configure!(voice_ingress); - let amqp = AMQP::new_auto().await; + let amqp = AMQP::new_auto().await.unwrap(); let database = DatabaseInfo::Auto.connect().await.unwrap(); let voice_client = VoiceClient::from_revolt_config().await; From acbc087982e9aeb05cabc5ab4c9b1291f67490ad Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 18 May 2026 10:57:23 -0700 Subject: [PATCH 30/39] feat: Update FCM payload for android notifications (#766) * feat: modify fcm payload to jens will Signed-off-by: IAmTomahawkx * fix: add message id Signed-off-by: IAmTomahawkx * fix: rename field Signed-off-by: IAmTomahawkx * fix: whitespace Signed-off-by: IAmTomahawkx --------- Signed-off-by: IAmTomahawkx --- .../pushd/src/consumers/outbound/fcm.rs | 45 +++++++------------ 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/crates/daemons/pushd/src/consumers/outbound/fcm.rs b/crates/daemons/pushd/src/consumers/outbound/fcm.rs index 9ff70fc8..4ea21e0b 100644 --- a/crates/daemons/pushd/src/consumers/outbound/fcm.rs +++ b/crates/daemons/pushd/src/consumers/outbound/fcm.rs @@ -11,7 +11,6 @@ use fcm_v1::{ }; use revolt_config::config; use revolt_database::{events::rabbit::*, Database}; -use revolt_models::v0::{Channel, PushNotification}; use serde_json::Value; /// Custom notification data @@ -31,10 +30,12 @@ pub enum NotificationData { image: Option, }, Message { - title: String, + message: String, body: String, image: String, - tag: String, + channel: String, + author: String, + author_name: String, }, DmCallStartEnd { initiator_id: String, @@ -81,15 +82,19 @@ impl NotificationData { } } NotificationData::Message { - title, + message, body, image, - tag, + channel, + author, + author_name, } => { - data.insert("title".to_string(), Value::String(title)); + data.insert("message".to_string(), Value::String(message)); data.insert("body".to_string(), Value::String(body)); data.insert("image".to_string(), Value::String(image)); - data.insert("tag".to_string(), Value::String(tag)); + data.insert("channel".to_string(), Value::String(channel)); + data.insert("author".to_string(), Value::String(author)); + data.insert("author_name".to_string(), Value::String(author_name)); } NotificationData::DmCallStartEnd { initiator_id, @@ -115,26 +120,6 @@ pub struct FcmOutboundConsumer { client: Client, } -impl FcmOutboundConsumer { - fn format_title(&self, notification: &PushNotification) -> String { - // ideally this changes depending on context - // in a server, it would look like "Sendername, #channelname in servername" - // in a group, it would look like "Sendername in groupname" - // in a dm it should just be "Sendername". - // not sure how feasible all those are given the PushNotification object as it currently stands. - - #[allow(deprecated)] - match ¬ification.channel { - Channel::DirectMessage { .. } => notification.author.clone(), - Channel::Group { name, .. } => format!("{}, #{}", notification.author, name), - Channel::TextChannel { name, .. } => { - format!("{} in #{}", notification.author, name) - } - _ => "Unknown".to_string(), - } - } -} - impl FcmOutboundConsumer { pub async fn new(db: Database) -> Result { let config = revolt_config::config().await; @@ -244,10 +229,12 @@ impl FcmOutboundConsumer { PayloadKind::MessageNotification(alert) => { let data = NotificationData::Message { - title: self.format_title(&alert), + message: alert.message.id, body: alert.body, image: alert.icon, - tag: alert.tag, + channel: alert.message.channel, + author: alert.message.author, + author_name: alert.author, }; let msg = Message { From af0d8aad14dc68d88159d0e1c714077d362e21e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Tue, 19 May 2026 00:51:43 +0300 Subject: [PATCH 31/39] feat: user slowmode events (#760) * feat: user slowmode events Signed-off-by: ispik * fix: remove debug print statement for slowmodes Signed-off-by: ispik * refactor: Send user slowmodes as websocket connects instead of trying to send it in ready payload Signed-off-by: ispik * refactor: optimize user slowmode handling with bulk operations Signed-off-by: ispik * chore: specify release version Release-As: 0.13.6 --------- Signed-off-by: ispik --- crates/bonfire/src/events/impl.rs | 1 + crates/bonfire/src/websocket.rs | 52 ++++++++- crates/core/database/src/events/client.rs | 100 ++++++++++++++---- crates/core/models/src/v0/channels.rs | 6 ++ .../delta/src/routes/channels/message_send.rs | 31 ++++++ 5 files changed, 166 insertions(+), 24 deletions(-) diff --git a/crates/bonfire/src/events/impl.rs b/crates/bonfire/src/events/impl.rs index 12113537..96a0f2ab 100644 --- a/crates/bonfire/src/events/impl.rs +++ b/crates/bonfire/src/events/impl.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use futures::future::join_all; +use redis_kiss::AsyncCommands; use revolt_database::{ events::client::{EventV1, ReadyPayloadFields}, util::permissions::DatabasePermissionQuery, diff --git a/crates/bonfire/src/websocket.rs b/crates/bonfire/src/websocket.rs index b5611bd0..ee551289 100644 --- a/crates/bonfire/src/websocket.rs +++ b/crates/bonfire/src/websocket.rs @@ -13,7 +13,7 @@ use futures::{ stream::{SplitSink, SplitStream}, FutureExt, SinkExt, StreamExt, TryStreamExt, }; -use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI}; +use redis_kiss::{get_connection, AsyncCommands, PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI}; use revolt_config::report_internal_error; use revolt_database::{ events::{client::EventV1, server::ClientMessage}, @@ -32,6 +32,7 @@ use sentry::Level; use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback}; use crate::events::state::{State, SubscriptionStateChange}; +use revolt_models::v0; type WsReader = SplitStream>; type WsWriter = SplitSink, async_tungstenite::tungstenite::Message>; @@ -128,6 +129,14 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr) return; } + let slowmodes = fetch_user_slowmodes(&user_id).await.unwrap_or_default(); + if !slowmodes.is_empty() { + let event = EventV1::UserSlowmodes { slowmodes }; + if report_internal_error!(write.send(config.encode(&event)).await).is_err() { + return; + } + } + // Create presence session. let (first_session, session_id) = create_session(&user_id, 0).await; @@ -527,4 +536,43 @@ async fn worker( } } } -} \ No newline at end of file +} + +async fn fetch_user_slowmodes(user_id: &str) -> Option> { + let mut conn = get_connection().await.ok()?.into_inner(); + let idx_key = format!("slowmode_idx:{}", user_id); + + let channel_ids: Vec = conn.smembers(&idx_key).await.unwrap_or_default(); + if channel_ids.is_empty() { + return Some(vec![]); + } + + // Bulk fetch all TTLs in one round trip + let mut pipe = redis_kiss::redis::pipe(); + for channel_id in &channel_ids { + pipe.ttl(format!("slowmode:{}:{}", user_id, channel_id)); + } + let ttls: Vec = pipe.query_async(&mut conn).await.unwrap_or_default(); + + // Partition into alive/expired in one pass + let mut slowmodes = vec![]; + let mut expired = vec![]; + for (channel_id, ttl) in channel_ids.iter().zip(ttls.iter()) { + if *ttl > 0 { + slowmodes.push(v0::ChannelSlowmode { + channel_id: channel_id.clone(), + duration: *ttl as u64, + retry_after: *ttl as u64, + }); + } else { + expired.push(channel_id.as_str()); + } + } + + // Bulk remove all expired members in one SREM call + if !expired.is_empty() { + conn.srem::<_, _, ()>(&idx_key, expired).await.ok(); + } + + Some(slowmodes) +} diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index 38c6f941..31864cee 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -3,7 +3,12 @@ use revolt_result::Error; use serde::{Deserialize, Serialize}; use revolt_models::v0::{ - AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialEmoji, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook + AppendMessage, Channel, ChannelSlowmode, ChannelUnread, ChannelVoiceState, Emoji, + FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, + FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialEmoji, + PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, + PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, + UserVoiceState, Webhook, }; use crate::Database; @@ -51,9 +56,13 @@ impl Default for ReadyPayloadFields { #[serde(tag = "type")] pub enum EventV1 { /// Multiple events - Bulk { v: Vec }, + Bulk { + v: Vec, + }, /// Error event - Error { data: Error }, + Error { + data: Error, + }, /// Successfully authenticated Authenticated, @@ -84,7 +93,9 @@ pub enum EventV1 { }, /// Ping response - Pong { data: Ping }, + Pong { + data: Ping, + }, /// New message Message(Message), @@ -105,7 +116,10 @@ pub enum EventV1 { }, /// Delete message - MessageDelete { id: String, channel: String }, + MessageDelete { + id: String, + channel: String, + }, /// New reaction to a message MessageReact { @@ -131,7 +145,10 @@ pub enum EventV1 { }, /// Bulk delete messages - BulkMessageDelete { channel: String, ids: Vec }, + BulkMessageDelete { + channel: String, + ids: Vec, + }, /// New server ServerCreate { @@ -139,7 +156,7 @@ pub enum EventV1 { server: Server, channels: Vec, emojis: Vec, - voice_states: Vec + voice_states: Vec, }, /// Update existing server @@ -151,7 +168,9 @@ pub enum EventV1 { }, /// Delete server - ServerDelete { id: String }, + ServerDelete { + id: String, + }, /// Update existing server member ServerMemberUpdate { @@ -187,10 +206,16 @@ pub enum EventV1 { }, /// Server role deleted - ServerRoleDelete { id: String, role_id: String }, + ServerRoleDelete { + id: String, + role_id: String, + }, /// Server roles ranks updated - ServerRoleRanksUpdate { id: String, ranks: Vec }, + ServerRoleRanksUpdate { + id: String, + ranks: Vec, + }, /// Update existing user UserUpdate { @@ -202,9 +227,15 @@ pub enum EventV1 { }, /// Relationship with another user changed - UserRelationship { id: String, user: User }, + UserRelationship { + id: String, + user: User, + }, /// Settings updated remotely - UserSettingsUpdate { id: String, update: UserSettings }, + UserSettingsUpdate { + id: String, + update: UserSettings, + }, /// User has been platform banned or deleted their account /// @@ -215,7 +246,10 @@ pub enum EventV1 { /// - Server Memberships /// /// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear. - UserPlatformWipe { user_id: String, flags: i32 }, + UserPlatformWipe { + user_id: String, + flags: i32, + }, /// New emoji EmojiCreate(Emoji), @@ -226,7 +260,9 @@ pub enum EventV1 { }, /// Delete emoji - EmojiDelete { id: String }, + EmojiDelete { + id: String, + }, /// New report ReportCreate(Report), @@ -242,19 +278,33 @@ pub enum EventV1 { }, /// Delete channel - ChannelDelete { id: String }, + ChannelDelete { + id: String, + }, /// User joins a group - ChannelGroupJoin { id: String, user: String }, + ChannelGroupJoin { + id: String, + user: String, + }, /// User leaves a group - ChannelGroupLeave { id: String, user: String }, + ChannelGroupLeave { + id: String, + user: String, + }, /// User started typing in a channel - ChannelStartTyping { id: String, user: String }, + ChannelStartTyping { + id: String, + user: String, + }, /// User stopped typing in a channel - ChannelStopTyping { id: String, user: String }, + ChannelStopTyping { + id: String, + user: String, + }, /// User acknowledged message in channel ChannelAck { @@ -274,7 +324,9 @@ pub enum EventV1 { }, /// Delete webhook - WebhookDelete { id: String }, + WebhookDelete { + id: String, + }, /// Auth events Auth(AuthifierEvent), @@ -292,7 +344,7 @@ pub enum EventV1 { user: String, from: String, to: String, - state: UserVoiceState + state: UserVoiceState, }, UserVoiceStateUpdate { id: String, @@ -304,7 +356,11 @@ pub enum EventV1 { from: String, to: String, token: String, - } + }, + /// User's active slowmodes + UserSlowmodes { + slowmodes: Vec, + }, } impl EventV1 { diff --git a/crates/core/models/src/v0/channels.rs b/crates/core/models/src/v0/channels.rs index d9fe8e14..faddee74 100644 --- a/crates/core/models/src/v0/channels.rs +++ b/crates/core/models/src/v0/channels.rs @@ -314,6 +314,12 @@ auto_derived!( /// Only used when the user is the first one connected. pub recipients: Option>, } + + pub struct ChannelSlowmode { + pub channel_id: String, + pub duration: u64, + pub retry_after: u64, + } ); impl Channel { diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index 7406b605..e6551d0c 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -1,12 +1,14 @@ use std::time::Duration; use redis_kiss::{get_connection, redis, AsyncCommands}; +use revolt_database::events::client::EventV1; use revolt_database::util::permissions::DatabasePermissionQuery; use revolt_database::{ util::idempotency::IdempotencyKey, util::reference::Reference, Database, User, }; use revolt_database::{Channel, Interactions, Message, AMQP}; use revolt_models::v0; +use revolt_models::v0::ChannelSlowmode; use revolt_permissions::PermissionQuery; use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_result::{create_error, Result}; @@ -84,6 +86,16 @@ pub async fn message_send( .await .unwrap_or(None); + if set_result.is_some() { + let idx_key = format!("slowmode_idx:{}", user.id); + conn.sadd::<_, _, ()>(&idx_key, channel_id.as_str()) + .await + .ok(); + conn.expire::<_, ()>(&idx_key, *channel_slowmode as usize) + .await + .ok(); + } + // If `set_result` is None, the `NX` condition failed because the key already exists. // This means the user is currently in slowmode. if set_result.is_none() { @@ -92,10 +104,29 @@ pub async fn message_send( // Redis returns positive integers for valid TTLs if ttl > 0 { + EventV1::UserSlowmodes { + slowmodes: vec![ChannelSlowmode { + channel_id: channel_id.to_string(), + duration: *channel_slowmode, + retry_after: ttl as u64, + }], + } + .private(user.id.clone()) + .await; return Err(create_error!(InSlowmode { retry_after: ttl as u64 })); } + } else { + EventV1::UserSlowmodes { + slowmodes: vec![ChannelSlowmode { + channel_id: channel_id.to_string(), + duration: *channel_slowmode, + retry_after: *channel_slowmode, + }], + } + .private(user.id.clone()) + .await; } } // If Redis connection fails, just skip the slowmode check From 018afaf38f6330d92dad2a68b640c0cb3f6b639a Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 18 May 2026 15:11:44 -0700 Subject: [PATCH 32/39] fix: set env var for publishing crates (#768) * fix: set env var for publishing crates Signed-off-by: IAmTomahawkx Release-As: 0.13.6 --- .github/workflows/publish-crates.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index ce02d841..5e35d358 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -21,4 +21,6 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: mise publish --workspace From 5b1985381ae829a92c80a19e91a414cd9dc4de93 Mon Sep 17 00:00:00 2001 From: Angelo Kontaxis Date: Mon, 18 May 2026 23:46:17 +0100 Subject: [PATCH 33/39] chore: switch to lapin (#767) * chore: begin switching to lapin fully Signed-off-by: Zomatree * chore: update rest of pushd to lapin Signed-off-by: Zomatree * chore: cleanup code Signed-off-by: Zomatree * chore: cleanup code Signed-off-by: Zomatree * fix: github webui sucks Signed-off-by: IAmTomahawkx --------- Signed-off-by: Zomatree Signed-off-by: Tom Signed-off-by: IAmTomahawkx Co-authored-by: Tom Release-As: 0.13.6 --- Cargo.lock | 41 +-- Cargo.toml | 1 - crates/core/database/Cargo.toml | 2 +- crates/core/database/src/amqp/amqp.rs | 301 ++++++++---------- crates/daemons/crond/src/tasks/acks.rs | 38 ++- crates/daemons/pushd/Cargo.toml | 2 +- .../pushd/src/consumers/inbound/ack.rs | 137 +++----- .../pushd/src/consumers/inbound/dm_call.rs | 140 +++----- .../src/consumers/inbound/fr_accepted.rs | 136 +++----- .../src/consumers/inbound/fr_received.rs | 136 +++----- .../pushd/src/consumers/inbound/generic.rs | 136 +++----- .../pushd/src/consumers/inbound/internal.rs | 53 --- .../src/consumers/inbound/mass_mention.rs | 149 +++------ .../pushd/src/consumers/inbound/message.rs | 143 +++------ .../pushd/src/consumers/inbound/mod.rs | 1 - .../pushd/src/consumers/outbound/apn.rs | 159 +++++---- .../pushd/src/consumers/outbound/fcm.rs | 111 +++---- .../pushd/src/consumers/outbound/vapid.rs | 143 ++++----- crates/daemons/pushd/src/main.rs | 251 +++++++++------ crates/daemons/pushd/src/utils/consumer.rs | 91 ++++++ crates/daemons/pushd/src/utils/mod.rs | 3 + crates/daemons/voice-ingress/Cargo.toml | 3 - crates/daemons/voice-ingress/src/main.rs | 2 +- crates/delta/Cargo.toml | 2 +- crates/delta/src/main.rs | 35 +- crates/delta/src/util/test.rs | 18 +- 26 files changed, 911 insertions(+), 1323 deletions(-) delete mode 100644 crates/daemons/pushd/src/consumers/inbound/internal.rs create mode 100644 crates/daemons/pushd/src/utils/consumer.rs diff --git a/Cargo.lock b/Cargo.lock index 9bccaccd..c65f954e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,31 +174,6 @@ dependencies = [ "url", ] -[[package]] -name = "amqp_serde" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f450f572a1ec4cdb4af7af09cbd0c7c3e1b9da2bfc7414c059a780993a8e16" -dependencies = [ - "bytes 1.11.1", - "serde", - "serde_bytes", -] - -[[package]] -name = "amqprs" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1b4afcbd862e16c272b7625b6b057930b052d63c720bc90f6afab0d9abe8a8" -dependencies = [ - "amqp_serde", - "async-trait", - "bytes 1.11.1", - "serde", - "serde_bytes_ng", - "tokio 1.51.0", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -7633,7 +7608,6 @@ dependencies = [ name = "revolt-database" version = "0.13.5" dependencies = [ - "amqprs", "async-lock 2.8.0", "async-recursion", "async-std", @@ -7648,6 +7622,7 @@ dependencies = [ "indexmap 2.13.1", "isahc", "iso8601-timestamp", + "lapin", "linkify", "livekit-api", "livekit-protocol", @@ -7684,7 +7659,6 @@ dependencies = [ name = "revolt-delta" version = "0.13.5" dependencies = [ - "amqprs", "async-channel 2.5.0", "async-std", "authifier", @@ -7694,6 +7668,7 @@ dependencies = [ "futures", "impl_ops", "iso8601-timestamp", + "lapin", "lettre", "linkify", "livekit-api", @@ -7867,7 +7842,6 @@ dependencies = [ name = "revolt-pushd" version = "0.13.5" dependencies = [ - "amqprs", "anyhow", "async-trait", "authifier", @@ -7875,6 +7849,7 @@ dependencies = [ "fcm_v1", "isahc", "iso8601-timestamp", + "lapin", "log", "pretty_env_logger", "redis-kiss", @@ -7931,7 +7906,6 @@ dependencies = [ name = "revolt-voice-ingress" version = "0.13.5" dependencies = [ - "amqprs", "async-std", "chrono", "futures", @@ -8987,15 +8961,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_bytes_ng" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb0ebce8684e2253f964e8b6ce51f0ccc6666bbb448fb4a6788088bda6544b6" -dependencies = [ - "serde", -] - [[package]] name = "serde_core" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index 3b497784..3b2b92fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -159,7 +159,6 @@ opentelemetry-appender-tracing = "0.31.1" authifier = "1.0.16" # RabbitMQ -amqprs = "1.7.0" lapin = "4.7.1" # Voice diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index 5031009d..dc213fd0 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -95,7 +95,7 @@ revolt_rocket_okapi = { workspace = true, optional = true } authifier = { workspace = true } # RabbitMQ -amqprs = { workspace = true } +lapin = { workspace = true, features = ["tokio"] } # Voice livekit-api = { workspace = true, features = ["rustls-tls-native-roots"], optional = true } diff --git a/crates/core/database/src/amqp/amqp.rs b/crates/core/database/src/amqp/amqp.rs index e0c644e5..b0e5e175 100644 --- a/crates/core/database/src/amqp/amqp.rs +++ b/crates/core/database/src/amqp/amqp.rs @@ -1,103 +1,77 @@ use std::collections::HashSet; +use std::sync::Arc; use crate::events::rabbit::*; use crate::User; -use amqprs::channel::{ - BasicPublishArguments, ExchangeDeclareArguments, ExchangeType, QueueBindArguments, - QueueDeclareArguments, +use lapin::{ + options::BasicPublishOptions, + protocol::basic::AMQPProperties, + types::{AMQPValue, FieldTable}, + Channel, Connection, ConnectionProperties, Error as AMQPError, }; -use amqprs::connection::OpenConnectionArguments; -use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError}; -use amqprs::{BasicProperties, FieldTable}; use revolt_models::v0::PushNotification; use revolt_presence::filter_online; +use revolt_result::Result; use serde_json::to_string; #[derive(Clone)] pub struct AMQP { + friend_request_accepted: Arc, + friend_request_received: Arc, + generic_message: Arc, + message_sent: Arc, + mass_mention_message_sent: Arc, + ack_notification_message: Arc, + dm_call_updated: Arc, + process_ack: Arc, #[allow(unused)] - connection: Connection, - channel: Channel, + connection: Arc, } impl AMQP { - pub fn new(connection: Connection, channel: Channel) -> AMQP { - AMQP { + pub async fn new(connection: Arc) -> Self { + Self { + friend_request_accepted: Self::create_channel(&connection).await, + friend_request_received: Self::create_channel(&connection).await, + generic_message: Self::create_channel(&connection).await, + message_sent: Self::create_channel(&connection).await, + mass_mention_message_sent: Self::create_channel(&connection).await, + ack_notification_message: Self::create_channel(&connection).await, + dm_call_updated: Self::create_channel(&connection).await, + process_ack: Self::create_channel(&connection).await, connection, - channel, } } - pub async fn new_auto() -> revolt_result::Result { + pub async fn new_auto() -> Self { let config = revolt_config::config().await; - let connection = Connection::open(&OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - )) - .await - .expect("Failed to connect to RabbitMQ"); - - let channel = connection - .open_channel(None) - .await - .expect("Failed to open RabbitMQ channel"); - - let mut resp = AMQP::new(connection, channel); - //resp.configure_channels().await?; - Ok(resp) - } - - pub async fn repoen_channel(&mut self) { - self.channel = self - .connection - .open_channel(None) - .await - .expect("Failed to open RabbitMQ channel"); - } - - pub async fn configure_channels(&mut self) -> revolt_result::Result<()> { - let config = revolt_config::config().await; - - if !self.channel.is_open() { - self.repoen_channel().await; - } - - self.channel - .exchange_declare( - ExchangeDeclareArguments::new( - &config.rabbit.default_exchange, - &ExchangeType::Topic.to_string(), - ) - .durable(true) - .finish(), + let connection = Arc::new( + Connection::connect( + &format!( + "amqp://{}:{}@{}:{}", + &config.rabbit.username, + &config.rabbit.password, + &config.rabbit.host, + &config.rabbit.port, + ), + ConnectionProperties::default(), ) .await - .expect("Failed to declare exchange"); + .expect("Failed to connect to RabbitMQ"), + ); - // Configure acks channel & routing - self.channel - .queue_declare( - QueueDeclareArguments::new(&config.rabbit.queues.acks) - .durable(true) - .no_wait(true) - .finish(), - ) - .await - .expect("Failed to bind queue"); + Self::new(connection).await + } - self.channel - .queue_bind(QueueBindArguments::new( - &config.rabbit.queues.acks, - &config.rabbit.default_exchange, - &config.rabbit.queues.acks, - )) - .await - .expect("Failed to bind channel"); - Ok(()) + async fn create_channel(connection: &Connection) -> Arc { + Arc::new( + connection + .create_channel() + .await + .expect("Failed to create channel"), + ) } pub async fn friend_request_accepted( @@ -117,19 +91,20 @@ impl AMQP { config.pushd.get_fr_accepted_routing_key(), payload ); - self.channel + + self.friend_request_accepted .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_fr_accepted_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_fr_accepted_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } pub async fn friend_request_received( @@ -150,19 +125,19 @@ impl AMQP { payload ); - self.channel + self.friend_request_received .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_fr_received_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_fr_received_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } pub async fn generic_message( @@ -187,19 +162,19 @@ impl AMQP { payload ); - self.channel + self.generic_message .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_generic_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_generic_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } pub async fn message_sent( @@ -230,19 +205,19 @@ impl AMQP { payload ); - self.channel + self.message_sent .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_message_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_message_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } pub async fn mass_mention_message_sent( @@ -265,16 +240,19 @@ impl AMQP { routing_key, payload ); - self.channel + self.mass_mention_message_sent .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new(&config.pushd.exchange, routing_key.as_str()), + config.pushd.exchange.clone().into(), + routing_key.into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } /// # Sends an ack to pushd to update badges on iPhones. @@ -299,23 +277,25 @@ impl AMQP { config.pushd.ack_queue, payload ); - let mut headers = FieldTable::new(); + let mut headers = FieldTable::default(); headers.insert( - "x-deduplication-header".try_into().unwrap(), - format!("{}-{}", &user_id, &channel_id).into(), + "x-deduplication-header".into(), + AMQPValue::LongString(format!("{}-{}", &user_id, &channel_id).into()), ); - self.channel + self.ack_notification_message .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - //.with_headers(headers) - .finish(), - payload.into(), - BasicPublishArguments::new(&config.pushd.exchange, &config.pushd.ack_queue), + config.pushd.exchange.clone().into(), + config.pushd.ack_queue.into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } /// # DM Call Update @@ -349,19 +329,19 @@ impl AMQP { payload ); - self.channel + self.dm_call_updated .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_dm_call_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_dm_call_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } /// # Send an ack to crond for processing @@ -385,19 +365,18 @@ impl AMQP { config.rabbit.default_exchange, config.rabbit.queues.acks, payload ); - self.channel + self.process_ack .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - //.with_headers(headers) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.rabbit.default_exchange, - &config.rabbit.queues.acks, - ), + config.rabbit.default_exchange.clone().into(), + config.rabbit.queues.acks.into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } } diff --git a/crates/daemons/crond/src/tasks/acks.rs b/crates/daemons/crond/src/tasks/acks.rs index 68b13144..2a3dc3a6 100644 --- a/crates/daemons/crond/src/tasks/acks.rs +++ b/crates/daemons/crond/src/tasks/acks.rs @@ -3,7 +3,7 @@ use lapin::{ options::*, types::FieldTable, uri::{AMQPAuthority, AMQPQueryString, AMQPUri, AMQPUserInfo}, - ConnectionBuilder, ConnectionProperties, + ConnectionBuilder, ConnectionProperties, ExchangeKind, }; use log::info; use redis_kiss::{get_connection, AsyncCommands, Conn as RedisConnection}; @@ -46,6 +46,42 @@ pub async fn task(db: Database) -> Result<()> { .await .expect("Failed to create channel"); + reader_channel + .exchange_declare( + config.rabbit.default_exchange.clone().into(), + ExchangeKind::Topic, + ExchangeDeclareOptions { + durable: true, + ..Default::default() + }, + FieldTable::default(), + ) + .await + .expect("Failed to declare exchange"); + + reader_channel + .queue_declare( + config.rabbit.queues.acks.clone().into(), + QueueDeclareOptions { + durable: true, + ..Default::default() + }, + FieldTable::default(), + ) + .await + .expect("Failed to bind queue"); + + reader_channel + .queue_bind( + config.rabbit.queues.acks.clone().into(), + config.rabbit.default_exchange.into(), + config.rabbit.queues.acks.clone().into(), + QueueBindOptions::default(), + FieldTable::default(), + ) + .await + .expect("Failed to bind channel"); + let mut consumer = reader_channel .basic_consume( config.rabbit.queues.acks.into(), diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index c47eb11a..8fb22999 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -15,7 +15,7 @@ revolt-parser = { workspace = true } anyhow = { workspace = true } -amqprs = { workspace = true } +lapin = { workspace = true } fcm_v1 = { workspace = true } web-push = { workspace = true } isahc = { workspace = true, features = ["json"], optional = true } diff --git a/crates/daemons/pushd/src/consumers/inbound/ack.rs b/crates/daemons/pushd/src/consumers/inbound/ack.rs index 77dd5d44..77df8b27 100644 --- a/crates/daemons/pushd/src/consumers/inbound/ack.rs +++ b/crates/daemons/pushd/src/consumers/inbound/ack.rs @@ -1,96 +1,69 @@ -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use std::sync::Arc; + +use crate::utils::Consumer; +use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct AckConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for AckConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl AckConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> AckConsumer { - AckConsumer { +#[async_trait] +impl Consumer for AckConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } -} -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for AckConsumer { + fn channel(&self) -> &Arc { + &self.channel + } + /// This consumer processes all acks the platform receives, and sends relevant badge updates to apple platforms. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - let content = String::from_utf8(content).unwrap(); - let payload: AckPayload = serde_json::from_str(content.as_str()).unwrap(); + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: AckPayload = serde_json::from_slice(&delivery.data)?; // Step 1: fetch unreads and don't continue if there's no unreads - #[allow(clippy::disallowed_methods)] - let unreads = self.db.fetch_unread_mentions(&payload.user_id).await; + // #[allow(clippy::disallowed_methods)] debug!("Processing unreads for {:}", &payload.user_id); - if let Ok(u) = &unreads { + let unreads = if let Ok(u) = self.db.fetch_unread_mentions(&payload.user_id).await { if u.is_empty() { debug!( "Discarding unread task (no mentions found) for {:}", &payload.user_id ); - return; - } + return Ok(()); + }; + + u } else { - return; - } + return Ok(()); + }; if let Ok(sessions) = self.authifier_db.find_sessions(&payload.user_id).await { let config = revolt_config::config().await; // Step 2: find any apple sessions, since we don't need to calculate this for anything else. // If there's no apple sessions, we can return early - let apple_sessions: Vec<&authifier::models::Session> = sessions - .iter() + let mut apple_sessions = sessions + .into_iter() .filter(|session| { if let Some(sub) = &session.subscription { sub.endpoint == "apn" @@ -98,19 +71,19 @@ impl AsyncConsumer for AckConsumer { false } }) - .collect(); + .peekable(); - if apple_sessions.is_empty() { + if apple_sessions.peek().is_none() { debug!( "Discarding unread task (no apn sessions found) for {:}", &payload.user_id ); - return; + return Ok(()); } // Step 3: calculate the actual mention count, since we have to send it out let mut mention_count = 0; - for u in &unreads.unwrap() { + for u in &unreads { mention_count += u.mentions.as_ref().unwrap().len() } @@ -123,26 +96,22 @@ impl AsyncConsumer for AckConsumer { token: session.subscription.as_ref().unwrap().auth.clone(), extras: Default::default(), }; - let raw_service_payload = serde_json::to_string(&service_payload); + let payload = serde_json::to_string(&service_payload)?; - if let Ok(p) = raw_service_payload { - let args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); + log::debug!( + "Publishing ack to apn session {}", + session.subscription.as_ref().unwrap().auth + ); - log::debug!( - "Publishing ack to apn session {}", - session.subscription.as_ref().unwrap().auth - ); - - publish_message(self, p.into(), args).await; - } else { - log::warn!("Failed to serialize ack badge update payload!"); - revolt_config::capture_error(&raw_service_payload.unwrap_err()); - } + self.publish_message( + payload.as_bytes(), + &config.pushd.exchange, + &config.pushd.apn.queue, + ) + .await?; } } + + Ok(()) } } diff --git a/crates/daemons/pushd/src/consumers/inbound/dm_call.rs b/crates/daemons/pushd/src/consumers/inbound/dm_call.rs index 873b55cc..3175d8d8 100644 --- a/crates/daemons/pushd/src/consumers/inbound/dm_call.rs +++ b/crates/daemons/pushd/src/consumers/inbound/dm_call.rs @@ -1,70 +1,44 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::Consumer; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct DmCallConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for DmCallConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl DmCallConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> DmCallConsumer { - DmCallConsumer { +#[async_trait] +impl Consumer for DmCallConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let _p: InternalDmCallPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let _p: InternalDmCallPayload = serde_json::from_slice(&delivery.data)?; let payload = _p.payload; debug!("Received dm call start/stop event"); @@ -107,36 +81,27 @@ impl DmCallConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message( + payload.as_bytes(), + &config.pushd.exchange, + routing_key, + ) + .await?; } } } @@ -145,24 +110,3 @@ impl DmCallConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for DmCallConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - warn!("Failed to process dm call start/stop event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs b/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs index 115e4a26..bcfa1c2f 100644 --- a/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs +++ b/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs @@ -1,70 +1,44 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::Consumer; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct FRAcceptedConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for FRAcceptedConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl FRAcceptedConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> FRAcceptedConsumer { - FRAcceptedConsumer { +#[async_trait] +impl Consumer for FRAcceptedConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: FRAcceptedPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: FRAcceptedPayload = serde_json::from_slice(&delivery.data)?; debug!("Received FR accept event"); @@ -80,36 +54,23 @@ impl FRAcceptedConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } @@ -117,24 +78,3 @@ impl FRAcceptedConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for FRAcceptedConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process friend request accepted event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/fr_received.rs b/crates/daemons/pushd/src/consumers/inbound/fr_received.rs index c611dfaf..66fce72d 100644 --- a/crates/daemons/pushd/src/consumers/inbound/fr_received.rs +++ b/crates/daemons/pushd/src/consumers/inbound/fr_received.rs @@ -1,70 +1,44 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::Consumer; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct FRReceivedConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for FRReceivedConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl FRReceivedConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> FRReceivedConsumer { - FRReceivedConsumer { +#[async_trait] +impl Consumer for FRReceivedConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: FRReceivedPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: FRReceivedPayload = serde_json::from_slice(&delivery.data)?; debug!("Received FR received event"); @@ -80,36 +54,23 @@ impl FRReceivedConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } @@ -117,24 +78,3 @@ impl FRReceivedConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for FRReceivedConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process friend request received event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/generic.rs b/crates/daemons/pushd/src/consumers/inbound/generic.rs index 302b1700..f413661d 100644 --- a/crates/daemons/pushd/src/consumers/inbound/generic.rs +++ b/crates/daemons/pushd/src/consumers/inbound/generic.rs @@ -1,70 +1,44 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::Consumer; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct GenericConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for GenericConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl GenericConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> GenericConsumer { - GenericConsumer { +#[async_trait] +impl Consumer for GenericConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: MessageSentPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: MessageSentPayload = serde_json::from_slice(&delivery.data)?; debug!("Received message event on origin"); @@ -86,36 +60,23 @@ impl GenericConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } @@ -123,24 +84,3 @@ impl GenericConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for GenericConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process generic event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/internal.rs b/crates/daemons/pushd/src/consumers/inbound/internal.rs deleted file mode 100644 index 387c08b5..00000000 --- a/crates/daemons/pushd/src/consumers/inbound/internal.rs +++ /dev/null @@ -1,53 +0,0 @@ -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::{Connection, OpenConnectionArguments}, - BasicProperties, -}; -use log::{debug, warn}; - -pub(crate) trait Channeled { - #[allow(unused)] - fn get_connection(&self) -> Option<&Connection>; - fn get_channel(&self) -> Option<&Channel>; - fn set_connection(&mut self, conn: Connection); - fn set_channel(&mut self, channel: Channel); -} - -pub(crate) async fn make_channel(consumer: &mut T) { - let config = revolt_config::config().await; - - let args = OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - ); - let conn = amqprs::connection::Connection::open(&args).await.unwrap(); - - let channel = conn.open_channel(None).await.unwrap(); - - consumer.set_connection(conn); - consumer.set_channel(channel); -} - -pub(crate) async fn publish_message( - consumer: &mut T, - payload: Vec, - args: BasicPublishArguments, -) { - let routing_key = &args.routing_key.clone(); - let mut channel = consumer.get_channel(); - if channel.is_none() { - make_channel(consumer).await; - channel = consumer.get_channel(); - } - - if let Some(chnl) = channel { - chnl.basic_publish(BasicProperties::default(), payload.clone(), args.clone()) - .await - .unwrap(); - debug!("Sent message to queue for target {}", routing_key); - } else { - warn!("Failed to unwrap channel (including attempt to make a channel)!") - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs b/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs index 8d43cce8..55ab984f 100644 --- a/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs +++ b/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs @@ -1,17 +1,13 @@ use std::{ collections::{HashMap, HashSet}, hash::RandomState, + sync::Arc, }; -use crate::{consumers::inbound::internal::*, utils}; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::{render_notification_content, Consumer}; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use revolt_database::{ events::rabbit::*, util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member, MessageFlagsValue, @@ -19,52 +15,18 @@ use revolt_database::{ use revolt_models::v0::{MessageFlags, PushNotification}; use revolt_result::ToRevoltError; +#[derive(Clone)] +#[allow(unused)] pub struct MassMessageConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, -} - -impl Channeled for MassMessageConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } + connection: Arc, + channel: Arc, } impl MassMessageConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> MassMessageConsumer { - MassMessageConsumer { - db, - authifier_db, - conn: None, - channel: None, - } - } - async fn fire_notification_for_users( - &mut self, + &self, push: &PushNotification, users: &[String], ) -> Result<()> { @@ -84,56 +46,58 @@ impl MassMessageConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } Ok(()) } +} - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { +#[async_trait] +impl Consumer for MassMessageConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { + db, + authifier_db, + connection, + channel, + } + } + + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let mut payload: MassMessageSentPayload = serde_json::from_slice(&delivery.data)?; let config = revolt_config::config().await; - let content = String::from_utf8(content)?; - let mut payload: MassMessageSentPayload = serde_json::from_str(content.as_str())?; for push in payload.notifications.iter_mut() { - if let Ok(body) = utils::render_notification_content(push, &self.db) + if let Ok(body) = render_notification_content(push, &self.db) .await .to_internal_error() { @@ -280,24 +244,3 @@ impl MassMessageConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for MassMessageConsumer { - /// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process mass message event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/message.rs b/crates/daemons/pushd/src/consumers/inbound/message.rs index 45de0b0b..e4aa04d5 100644 --- a/crates/daemons/pushd/src/consumers/inbound/message.rs +++ b/crates/daemons/pushd/src/consumers/inbound/message.rs @@ -1,76 +1,46 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::{consumers::inbound::internal::*, utils}; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::{render_notification_content, Consumer}; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; -use revolt_result::ToRevoltError; +#[derive(Clone)] +#[allow(unused)] pub struct MessageConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for MessageConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl MessageConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> MessageConsumer { - MessageConsumer { +#[async_trait] +impl Consumer for MessageConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let mut payload: MessageSentPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } - if let Ok(body) = utils::render_notification_content(&payload.notification, &self.db) - .await - .to_internal_error() - { + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let mut payload: MessageSentPayload = serde_json::from_slice(&delivery.data)?; + + if let Ok(body) = render_notification_content(&payload.notification, &self.db).await { payload.notification.raw_body = Some(payload.notification.body); payload.notification.body = body; } @@ -95,36 +65,22 @@ impl MessageConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } @@ -132,24 +88,3 @@ impl MessageConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for MessageConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process message event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/mod.rs b/crates/daemons/pushd/src/consumers/inbound/mod.rs index 4d6d36b2..7c93cdb6 100644 --- a/crates/daemons/pushd/src/consumers/inbound/mod.rs +++ b/crates/daemons/pushd/src/consumers/inbound/mod.rs @@ -3,6 +3,5 @@ pub mod dm_call; pub mod fr_accepted; pub mod fr_received; pub mod generic; -mod internal; pub mod mass_mention; pub mod message; diff --git a/crates/daemons/pushd/src/consumers/outbound/apn.rs b/crates/daemons/pushd/src/consumers/outbound/apn.rs index ec478c1f..feb6cb94 100644 --- a/crates/daemons/pushd/src/consumers/outbound/apn.rs +++ b/crates/daemons/pushd/src/consumers/outbound/apn.rs @@ -1,12 +1,13 @@ -use std::{borrow::Cow, collections::BTreeMap, io::Cursor}; +use std::{borrow::Cow, collections::BTreeMap, io::Cursor, sync::Arc}; -use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; -use anyhow::{anyhow, Result}; +use crate::utils::Consumer; +use anyhow::Result; use async_trait::async_trait; use base64::{ engine::{self}, Engine as _, }; +use lapin::{message::Delivery, Channel as AMQPChannel, Connection}; use revolt_a2::{ request::{ notification::{DefaultAlert, NotificationOptions}, @@ -42,7 +43,7 @@ impl<'a> PayloadLike for MessagePayload<'a> { fn get_device_token(&self) -> &'a str { self.device_token } - fn get_options(&self) -> &NotificationOptions { + fn get_options(&self) -> &NotificationOptions<'a> { &self.options } } @@ -68,16 +69,20 @@ impl<'a> PayloadLike for CallStartStopPayload<'a> { fn get_device_token(&self) -> &'a str { self.device_token } - fn get_options(&self) -> &NotificationOptions { + fn get_options(&self) -> &NotificationOptions<'a> { &self.options } } // region: consumer +#[derive(Clone)] +#[allow(unused)] pub struct ApnsOutboundConsumer { - #[allow(dead_code)] db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, client: Client, } @@ -117,15 +122,21 @@ impl ApnsOutboundConsumer { } } -impl ApnsOutboundConsumer { - pub async fn new(db: Database) -> Result { +#[async_trait] +impl Consumer for ApnsOutboundConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { let config = revolt_config::config().await; if config.pushd.apn.pkcs8.is_empty() || config.pushd.apn.key_id.is_empty() || config.pushd.apn.team_id.is_empty() { - return Err("Missing APN keys."); + panic!("Missing APN keys."); } let endpoint = if config.pushd.apn.sandbox { @@ -148,18 +159,21 @@ impl ApnsOutboundConsumer { ) .expect("could not create APN client"); - Ok(ApnsOutboundConsumer { db, client }) + Self { + db, + authifier_db, + connection, + channel, + client, + } } - async fn consume_event( - &mut self, - _channel: &AmqpChannel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: PayloadToService = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: PayloadToService = serde_json::from_slice(&delivery.data)?; let payload_options = NotificationOptions { apns_id: None, @@ -170,20 +184,15 @@ impl ApnsOutboundConsumer { apns_collapse_id: None, }; - let resp: Result; - - match payload.notification { + let resp = match payload.notification { PayloadKind::FRReceived(alert) => { let loc_args = vec![Cow::from( - alert - .from_user - .display_name - .or(Some(format!( + alert.from_user.display_name.clone().unwrap_or_else(|| { + format!( "{}#{}", alert.from_user.username, alert.from_user.discriminator - ))) - .clone() - .ok_or_else(|| anyhow!("missing name"))?, + ) + }), )]; let apn_payload = Payload { @@ -216,20 +225,17 @@ impl ApnsOutboundConsumer { "Sending friend request received for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::FRAccepted(alert) => { let loc_args = vec![Cow::from( - alert - .accepted_user - .display_name - .or(Some(format!( + alert.accepted_user.display_name.clone().unwrap_or_else(|| { + format!( "{}#{}", alert.accepted_user.username, alert.accepted_user.discriminator - ))) - .clone() - .ok_or_else(|| anyhow!("missing name"))?, + ) + }), )]; let apn_payload = Payload { @@ -262,7 +268,7 @@ impl ApnsOutboundConsumer { "Sending friend request accept for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::Generic(alert) => { let apn_payload = Payload { @@ -295,7 +301,7 @@ impl ApnsOutboundConsumer { "Sending generic notification for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::MessageNotification(alert) => { @@ -334,7 +340,7 @@ impl ApnsOutboundConsumer { "Sending message notification for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::BadgeUpdate(badge) => { @@ -349,7 +355,7 @@ impl ApnsOutboundConsumer { }; debug!("Sending badge update for user: {:}", &payload.user_id); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::DmCallStartEnd(alert) => { @@ -378,58 +384,37 @@ impl ApnsOutboundConsumer { "Sending call start/stop notification for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } - } + }; - if let Err(err) = resp { - match err { - Error::ResponseError(Response { - error: - Some(ErrorBody { - reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered, - .. - }), - .. - }) => { - info!( - "Removing APNS subscription id {:} (user: {:}) due to invalid token", - &payload.session_id, &payload.user_id - ); - if let Err(err) = self - .db - .remove_push_subscription_by_session_id(&payload.session_id) - .await - { - revolt_config::capture_error(&err); - } - } - err => { + match resp { + Err(Error::ResponseError(Response { + error: + Some(ErrorBody { + reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered, + .. + }), + .. + })) => { + info!( + "Removing APNS subscription id {:} (user: {:}) due to invalid token", + &payload.session_id, &payload.user_id + ); + + if let Err(err) = self + .db + .remove_push_subscription_by_session_id(&payload.session_id) + .await + { revolt_config::capture_error(&err); } } - } + resp => { + resp?; + } + }; Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for ApnsOutboundConsumer { - async fn consume( - &mut self, - channel: &AmqpChannel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process APN event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/outbound/fcm.rs b/crates/daemons/pushd/src/consumers/outbound/fcm.rs index 4ea21e0b..e3bd185c 100644 --- a/crates/daemons/pushd/src/consumers/outbound/fcm.rs +++ b/crates/daemons/pushd/src/consumers/outbound/fcm.rs @@ -1,14 +1,14 @@ -use std::{collections::HashMap, time::Duration}; +use std::{collections::HashMap, sync::Arc, time::Duration}; -use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; - -use anyhow::{anyhow, bail, Result}; +use crate::utils::Consumer; +use anyhow::{bail, Result}; use async_trait::async_trait; use fcm_v1::{ auth::{Authenticator, ServiceAccountKey}, message::Message, Client, Error as FcmError, }; +use lapin::{message::Delivery, Channel as AMQPChannel, Connection}; use revolt_config::config; use revolt_database::{events::rabbit::*, Database}; use serde_json::Value; @@ -115,17 +115,31 @@ impl NotificationData { } } +#[derive(Clone)] +#[allow(unused)] pub struct FcmOutboundConsumer { db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, client: Client, } -impl FcmOutboundConsumer { - pub async fn new(db: Database) -> Result { +#[async_trait] +impl Consumer for FcmOutboundConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { let config = revolt_config::config().await; - Ok(FcmOutboundConsumer { + Self { db, + authifier_db, + connection, + channel, client: Client::new( Authenticator::service_account::<&str>(ServiceAccountKey { key_type: Some(config.pushd.fcm.key_type), @@ -145,33 +159,27 @@ impl FcmOutboundConsumer { false, Duration::from_secs(5), ), - }) + } } - async fn consume_event( - &mut self, - _channel: &AmqpChannel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: PayloadToService = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: PayloadToService = serde_json::from_slice(&delivery.data)?; #[allow(clippy::needless_late_init)] let resp: Result; match payload.notification { PayloadKind::FRReceived(alert) => { - let name = alert - .from_user - .display_name - .or(Some(format!( + let name = alert.from_user.display_name.clone().unwrap_or_else(|| { + format!( "{}#{}", alert.from_user.username, alert.from_user.discriminator - ))) - .clone() - .ok_or_else(|| anyhow!("missing name"))?; + ) + }); let data = NotificationData::FRReceived { id: alert.from_user.id, @@ -188,15 +196,12 @@ impl FcmOutboundConsumer { } PayloadKind::FRAccepted(alert) => { - let name = alert - .accepted_user - .display_name - .or(Some(format!( + let name = alert.accepted_user.display_name.clone().unwrap_or_else(|| { + format!( "{}#{}", alert.accepted_user.username, alert.accepted_user.discriminator - ))) - .clone() - .ok_or_else(|| anyhow!("missing name"))?; + ) + }); let data = NotificationData::FRAccepted { id: alert.accepted_user.id, @@ -269,43 +274,21 @@ impl FcmOutboundConsumer { } } - if let Err(err) = resp { - match err { - FcmError::Auth => { - if let Err(err) = self - .db - .remove_push_subscription_by_session_id(&payload.session_id) - .await - { - revolt_config::capture_error(&err); - } - } - err => { + match resp { + Err(FcmError::Auth) => { + if let Err(err) = self + .db + .remove_push_subscription_by_session_id(&payload.session_id) + .await + { revolt_config::capture_error(&err); } } - } + res => { + res?; + } + }; Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for FcmOutboundConsumer { - async fn consume( - &mut self, - channel: &AmqpChannel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process FCM event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/outbound/vapid.rs b/crates/daemons/pushd/src/consumers/outbound/vapid.rs index 67ec31fb..0acd8be9 100644 --- a/crates/daemons/pushd/src/consumers/outbound/vapid.rs +++ b/crates/daemons/pushd/src/consumers/outbound/vapid.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; +use crate::utils::Consumer; use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; @@ -8,46 +8,60 @@ use base64::{ engine::{self}, Engine as _, }; +use lapin::{message::Delivery, Channel as AMQPChannel, Connection}; use revolt_database::{events::rabbit::*, util::format_display_name, Database}; use web_push::{ ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient, WebPushError, WebPushMessageBuilder, }; +#[derive(Clone)] +#[allow(unused)] pub struct VapidOutboundConsumer { db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, client: IsahcWebPushClient, - pkey: Vec, + pkey: Arc>, } -impl VapidOutboundConsumer { - pub async fn new(db: Database) -> Result { +#[async_trait] +impl Consumer for VapidOutboundConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { let config = revolt_config::config().await; - if config.pushd.vapid.private_key.is_empty() | config.pushd.vapid.public_key.is_empty() { - bail!("no Vapid keys present"); + if config.pushd.vapid.private_key.is_empty() || config.pushd.vapid.public_key.is_empty() { + panic!("no Vapid keys present"); } - let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD - .decode(config.pushd.vapid.private_key) - .expect("valid `VAPID_PRIVATE_KEY`"); + let web_push_private_key = Arc::new( + engine::general_purpose::URL_SAFE_NO_PAD + .decode(config.pushd.vapid.private_key) + .expect("valid `VAPID_PRIVATE_KEY`"), + ); - Ok(VapidOutboundConsumer { + Self { db, + authifier_db, + connection, + channel, client: IsahcWebPushClient::new().unwrap(), pkey: web_push_private_key, - }) + } } - async fn consume_event( - &mut self, - _channel: &AmqpChannel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: PayloadToService = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: PayloadToService = serde_json::from_slice(&delivery.data)?; let subscription = SubscriptionInfo { endpoint: payload @@ -65,10 +79,7 @@ impl VapidOutboundConsumer { }, }; - #[allow(clippy::needless_late_init)] - let payload_body: String; - - match payload.notification { + let payload_body = match payload.notification { PayloadKind::FRReceived(alert) => { let name = alert .from_user @@ -83,7 +94,7 @@ impl VapidOutboundConsumer { let mut body = HashMap::new(); body.insert("body", format!("{} sent you a friend request", name)); - payload_body = serde_json::to_string(&body)?; + serde_json::to_string(&body)? } PayloadKind::FRAccepted(alert) => { let name = alert @@ -99,14 +110,10 @@ impl VapidOutboundConsumer { let mut body = HashMap::new(); body.insert("body", format!("{} accepted your friend request", name)); - payload_body = serde_json::to_string(&body)?; - } - PayloadKind::Generic(alert) => { - payload_body = serde_json::to_string(&alert)?; - } - PayloadKind::MessageNotification(alert) => { - payload_body = serde_json::to_string(&alert)?; + serde_json::to_string(&body)? } + PayloadKind::Generic(alert) => serde_json::to_string(&alert)?, + PayloadKind::MessageNotification(alert) => serde_json::to_string(&alert)?, PayloadKind::DmCallStartEnd(alert) => { let initiator_name = if let Some(server_id) = self.db.fetch_channel(&alert.channel_id).await?.server() @@ -132,59 +139,41 @@ impl VapidOutboundConsumer { _ => bail!("Invalid DmCallStart/End channel type"), } - payload_body = serde_json::to_string(&body)?; + serde_json::to_string(&body)? } PayloadKind::BadgeUpdate(_) => { bail!("Vapid cannot handle badge updates and they should not be sent here."); } - } + }; - match VapidSignatureBuilder::from_pem(std::io::Cursor::new(&self.pkey), &subscription) { - Ok(sig_builder) => match sig_builder.build() { - Ok(signature) => { - let mut builder = WebPushMessageBuilder::new(&subscription); - builder.set_vapid_signature(signature); + let signature = VapidSignatureBuilder::from_pem( + std::io::Cursor::new(self.pkey.as_ref()), + &subscription, + )? + .build()?; - builder.set_payload(ContentEncoding::AesGcm, payload_body.as_bytes()); + let mut builder = WebPushMessageBuilder::new(&subscription); + builder.set_vapid_signature(signature); - match builder.build() { - Ok(msg) => { - if let Err(err) = self.client.send(msg).await { - if err == WebPushError::Unauthorized { - self.db - .remove_push_subscription_by_session_id(&payload.session_id) - .await?; - } - } + builder.set_payload(ContentEncoding::AesGcm, payload_body.as_bytes()); - Ok(()) - } - Err(err) => Err(err.into()), - } + let msg = builder.build()?; + + match self.client.send(msg).await { + Err(WebPushError::Unauthorized) => { + if let Err(err) = self + .db + .remove_push_subscription_by_session_id(&payload.session_id) + .await + { + revolt_config::capture_error(&err); } - Err(err) => Err(err.into()), - }, - Err(err) => Err(err.into()), - } - } -} + } + res => { + res?; + } + }; -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for VapidOutboundConsumer { - async fn consume( - &mut self, - channel: &AmqpChannel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process Vapid event: {err:?}"); - } + Ok(()) } } diff --git a/crates/daemons/pushd/src/main.rs b/crates/daemons/pushd/src/main.rs index eaa2e1b8..b4824b65 100644 --- a/crates/daemons/pushd/src/main.rs +++ b/crates/daemons/pushd/src/main.rs @@ -1,17 +1,16 @@ #[macro_use] extern crate log; -use amqprs::{ - channel::{ - BasicConsumeArguments, Channel, ExchangeDeclareArguments, QueueBindArguments, - QueueDeclareArguments, - }, - connection::{Connection, OpenConnectionArguments}, - consumer::AsyncConsumer, - FieldTable, +use std::sync::Arc; + +use lapin::{ + options::{BasicConsumeOptions, ExchangeDeclareOptions, QueueBindOptions, QueueDeclareOptions}, + types::{AMQPValue, FieldTable}, + Channel, Connection, ConnectionProperties, }; use revolt_config::{config, Settings}; -use tokio::sync::Notify; +use revolt_database::Database; +use tokio::signal::ctrl_c; mod consumers; mod utils; @@ -24,6 +23,8 @@ use consumers::{ outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer}, }; +use crate::utils::{Consumer, Delegate}; + #[tokio::main(flavor = "multi_thread", worker_threads = 2)] async fn main() { // Configure logging and environment @@ -43,7 +44,24 @@ async fn main() { panic!("Mongo is not in use, can't connect via authifier!") } - let mut connections: Vec<(Channel, Connection)> = Vec::new(); + let config = config().await; + + let connection = Arc::new( + Connection::connect( + &format!( + "amqp://{}:{}@{}:{}", + &config.rabbit.username, + &config.rabbit.password, + &config.rabbit.host, + &config.rabbit.port, + ), + ConnectionProperties::default(), + ) + .await + .expect("Failed to connect to RabbitMQ"), + ); + + let mut channels = Vec::new(); // An explainer of how this works: // The inbound connections are on separate routing keys, such that they only receive the proper payload @@ -54,171 +72,178 @@ async fn main() { // This'll require some interesting shimming if we need to add more events once this is in prod (different payloads between prod and test), // but that sounds like a problem for future us. - let config = config().await; - - // inbound: generic - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.generic_queue, - config.pushd.get_generic_routing_key().as_str(), + &config.pushd.get_generic_routing_key(), None, - GenericConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: messages - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.message_queue, - config.pushd.get_message_routing_key().as_str(), + &config.pushd.get_message_routing_key(), None, - MessageConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: FR received - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.fr_received_queue, - config.pushd.get_fr_received_routing_key().as_str(), + &config.pushd.get_fr_received_routing_key(), None, - FRReceivedConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: FR accepted - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.fr_accepted_queue, - config.pushd.get_fr_accepted_routing_key().as_str(), + &config.pushd.get_fr_accepted_routing_key(), None, - FRAcceptedConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: Mass Mentions - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.mass_mention_queue, - config.pushd.get_mass_mention_routing_key().as_str(), + &config.pushd.get_mass_mention_routing_key(), None, - MassMessageConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: Dm Calls - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.dm_call_queue, - config.pushd.get_dm_call_routing_key().as_str(), + &config.pushd.get_dm_call_routing_key(), None, - DmCallConsumer::new(db.clone(), authifier.clone()), ) .await, ); if !config.pushd.apn.pkcs8.is_empty() { - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.apn.queue, &config.pushd.apn.queue, None, - ApnsOutboundConsumer::new(db.clone()).await.unwrap(), ) .await, ); - let mut table = FieldTable::new(); - table.insert("x-message-deduplication".try_into().unwrap(), "true".into()); + let mut table = FieldTable::default(); + table.insert("x-message-deduplication".into(), AMQPValue::Boolean(true)); - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.ack_queue, &config.pushd.ack_queue, Some(table), - AckConsumer::new(db.clone(), authifier.clone()), ) .await, ); } if !config.pushd.fcm.auth_uri.is_empty() { - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.fcm.queue, &config.pushd.fcm.queue, None, - FcmOutboundConsumer::new(db.clone()).await.unwrap(), ) .await, - ) + ); } if !config.pushd.vapid.public_key.is_empty() { - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.vapid.queue, &config.pushd.vapid.queue, None, - VapidOutboundConsumer::new(db.clone()).await.unwrap(), ) .await, - ) + ); } - let guard = Notify::new(); - guard.notified().await; + ctrl_c().await.unwrap(); - for (channel, conn) in connections { - channel.close().await.expect("Unable to close channel"); - conn.close().await.expect("Unable to close connection"); + for channel in channels { + let _ = channel.close(0, "close".into()).await; } } async fn make_queue_and_consume( + db: &Database, + authifier_db: &authifier::Database, + connection: &Arc, config: &Settings, queue_name: &str, routing_key: &str, queue_args: Option, - consumer: F, -) -> (Channel, Connection) +) -> Arc where - F: AsyncConsumer + Send + 'static, + F: Consumer, { - let connection = Connection::open(&OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - )) - .await - .unwrap(); - - let channel = connection.open_channel(None).await.unwrap(); + let channel = Arc::new(connection.create_channel().await.unwrap()); channel .exchange_declare( - ExchangeDeclareArguments::new(&config.pushd.exchange, "direct") - .durable(true) - .finish(), + config.pushd.exchange.clone().into(), + lapin::ExchangeKind::Direct, + ExchangeDeclareOptions { + durable: true, + ..Default::default() + }, + FieldTable::default(), ) .await - .expect("Failed to declare pushd exchange"); + .expect("Failed to declare exchange"); let mut queue_name = queue_name.to_string(); @@ -230,35 +255,59 @@ where let queue_name = queue_name.as_str(); - let mut args = QueueDeclareArguments::new(queue_name); - args.durable(true); - - if let Some(arg) = queue_args { - args.arguments(arg); - } - - let args = args.finish(); - _ = channel.queue_declare(args).await.unwrap().unwrap(); + let args = QueueDeclareOptions { + durable: true, + ..Default::default() + }; channel - .queue_bind(QueueBindArguments::new( - queue_name, - &config.pushd.exchange, - routing_key, - )) + .queue_declare(queue_name.into(), args, queue_args.unwrap_or_default()) + .await + .unwrap(); + + channel + .queue_bind( + queue_name.into(), + config.pushd.exchange.clone().into(), + routing_key.into(), + QueueBindOptions::default(), + FieldTable::default(), + ) .await .expect( "This probably means the revolt.notifications exchange does not exist in rabbitmq!", ); - let args = BasicConsumeArguments::new(queue_name, "") - .manual_ack(false) - .finish(); - - let routing_key = channel.basic_consume(consumer, args).await.unwrap(); + let consumer = channel + .basic_consume( + queue_name.into(), + "".into(), + BasicConsumeOptions { + no_ack: true, + ..Default::default() + }, + FieldTable::default(), + ) + .await + .unwrap(); info!( "Consuming routing key {} as queue {}, tag {}", - routing_key, queue_name, routing_key + routing_key, + queue_name, + consumer.tag() ); - (channel, connection) + + let delegate = Delegate( + F::create( + db.clone(), + authifier_db.clone(), + connection.clone(), + channel.clone(), + ) + .await, + ); + + consumer.set_delegate(delegate); + + channel } diff --git a/crates/daemons/pushd/src/utils/consumer.rs b/crates/daemons/pushd/src/utils/consumer.rs new file mode 100644 index 00000000..9006aa32 --- /dev/null +++ b/crates/daemons/pushd/src/utils/consumer.rs @@ -0,0 +1,91 @@ +use std::{ + future::{ready, Future}, + pin::Pin, + sync::Arc, +}; + +use anyhow::Result; +use async_trait::async_trait; +use lapin::{ + message::{Delivery, DeliveryResult}, + options::BasicPublishOptions, + BasicProperties, Channel, Connection, ConsumerDelegate, Error as AMQPError, +}; +use log::debug; +use revolt_database::Database; + +#[async_trait] +pub trait Consumer: Clone + Send + Sync + 'static { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self; + fn channel(&self) -> &Arc; + async fn consume(&self, delivery: Delivery) -> Result<()>; + + async fn publish_message_with_options( + &self, + payload: &[u8], + exchange: &str, + routing_key: &str, + options: BasicPublishOptions, + properties: BasicProperties, + ) -> Result<(), AMQPError> { + let channel = self.channel(); + + channel + .basic_publish( + exchange.into(), + routing_key.into(), + options, + payload, + properties, + ) + .await?; + debug!("Sent message to queue for target {}", routing_key); + + Ok(()) + } + + async fn publish_message( + &self, + payload: &[u8], + exchange: &str, + routing_key: &str, + ) -> Result<(), AMQPError> { + self.publish_message_with_options( + payload, + exchange, + routing_key, + BasicPublishOptions::default(), + BasicProperties::default(), + ) + .await + } +} + +pub struct Delegate(pub C); + +impl ConsumerDelegate for Delegate { + fn on_new_delivery( + &self, + delivery: DeliveryResult, + ) -> Pin + Send>> { + match delivery { + Ok(Some(delivery)) => { + let consumer = self.0.clone(); + + Box::pin(async move { + if let Err(e) = consumer.consume(delivery).await { + revolt_config::capture_anyhow(&e); + log::error!("{e:?}"); + }; + }) + } + Ok(None) => Box::pin(ready(())), + Err(e) => Box::pin(async move { log::error!("Received bad delivery: {e:?}") }), + } + } +} diff --git a/crates/daemons/pushd/src/utils/mod.rs b/crates/daemons/pushd/src/utils/mod.rs index 62032e80..2a6866e0 100644 --- a/crates/daemons/pushd/src/utils/mod.rs +++ b/crates/daemons/pushd/src/utils/mod.rs @@ -1,2 +1,5 @@ mod renderer; +mod consumer; + pub use renderer::render_notification_content; +pub use consumer::{Consumer, Delegate}; \ No newline at end of file diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index 05ea4a60..e6996742 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -44,6 +44,3 @@ revolt-permissions = { workspace = true } livekit-api = { workspace = true } livekit-protocol = { workspace = true } livekit-runtime = { workspace = true, features = ["tokio"] } - -# RabbitMQ -amqprs = { workspace = true } diff --git a/crates/daemons/voice-ingress/src/main.rs b/crates/daemons/voice-ingress/src/main.rs index 727e1a25..45191c2c 100644 --- a/crates/daemons/voice-ingress/src/main.rs +++ b/crates/daemons/voice-ingress/src/main.rs @@ -13,7 +13,7 @@ mod guard; async fn main() -> Result<(), rocket::Error> { revolt_config::configure!(voice_ingress); - let amqp = AMQP::new_auto().await.unwrap(); + let amqp = AMQP::new_auto().await; let database = DatabaseInfo::Auto.connect().await.unwrap(); let voice_client = VoiceClient::from_revolt_config().await; diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index a74c5547..535da2af 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -64,7 +64,7 @@ schemars = { workspace = true } revolt_rocket_okapi = { workspace = true, features = ["swagger"] } # rabbit -amqprs = { workspace = true } +lapin = { workspace = true, features = ["tokio"] } # core authifier = { workspace = true } diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 41f8fc02..c1dd1dac 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -9,8 +9,7 @@ pub mod routes; pub mod util; use revolt_config::config; -use revolt_database::events::client::EventV1; -use revolt_database::AMQP; +use revolt_database::{AMQP, events::client::EventV1}; use revolt_ratelimits::rocket as ratelimiter; use rocket::{Build, Rocket}; use rocket_cors::{AllowedOrigins, CorsOptions}; @@ -18,10 +17,6 @@ use rocket_prometheus::PrometheusMetrics; use std::net::Ipv4Addr; use std::str::FromStr; -use amqprs::{ - channel::ExchangeDeclareArguments, - connection::{Connection, OpenConnectionArguments}, -}; use async_std::channel::unbounded; use authifier::AuthifierEvent; use revolt_database::voice::VoiceClient; @@ -36,7 +31,6 @@ pub async fn web() -> Rocket { // Setup database let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap(); - log::info!("database_here {db:?}"); db.migrate_database().await.unwrap(); // Setup Authifier event channel @@ -96,33 +90,8 @@ pub async fn web() -> Rocket { // Voice handler let voice_client = VoiceClient::new(config.api.livekit.nodes.clone()); // Configure Rabbit - let connection = Connection::open(&OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - )) - .await - .expect("Failed to connect to RabbitMQ"); - let channel = connection - .open_channel(None) - .await - .expect("Failed to open RabbitMQ channel"); - - channel - .exchange_declare( - ExchangeDeclareArguments::new(&config.pushd.exchange, "direct") - .durable(true) - .finish(), - ) - .await - .expect("Failed to declare exchange"); - - let mut amqp = AMQP::new(connection, channel); - // amqp.configure_channels() - // .await - // .expect("Failed to configure channels"); + let amqp = AMQP::new_auto().await; // Launch background task workers revolt_database::tasks::start_workers(db.clone(), amqp.clone()); diff --git a/crates/delta/src/util/test.rs b/crates/delta/src/util/test.rs index a6251509..3c8d6835 100644 --- a/crates/delta/src/util/test.rs +++ b/crates/delta/src/util/test.rs @@ -4,7 +4,7 @@ use authifier::{ }; use futures::StreamExt; use rand::Rng; -use redis_kiss::redis::aio::PubSub; +use redis_kiss::{redis::aio::PubSub}; use revolt_database::{ events::client::EventV1, Channel, Database, Member, Message, PartialRole, Server, User, AMQP, }; @@ -25,8 +25,6 @@ pub struct TestHarness { impl TestHarness { pub async fn new() -> TestHarness { - let config = revolt_config::config().await; - let client = Client::tracked(crate::web().await) .await .expect("valid rocket instance"); @@ -49,19 +47,7 @@ impl TestHarness { .expect("`Authifier`") .clone(); - let connection = amqprs::connection::Connection::open( - &amqprs::connection::OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - ), - ) - .await - .unwrap(); - let channel = connection.open_channel(None).await.unwrap(); - - let amqp = AMQP::new(connection, channel); + let amqp = AMQP::new_auto().await; TestHarness { client, From 03b52655ff5006d5466133235633cc8bd7513109 Mon Sep 17 00:00:00 2001 From: "stoat-release[bot]" <245062572+stoat-release[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 15:53:42 -0700 Subject: [PATCH 34/39] chore(main): release 0.13.6 (#762) * chore(main): release 0.13.6 * chore: update Cargo.lock Signed-off-by: github-actions[bot] --------- Signed-off-by: github-actions[bot] Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] --- .release-please-manifest.json | 2 +- CHANGELOG.md | 21 +++++++++++++++ Cargo.lock | 36 ++++++++++++------------- Cargo.toml | 20 +++++++------- crates/bonfire/Cargo.toml | 2 +- crates/core/coalesced/Cargo.toml | 2 +- crates/core/config/Cargo.toml | 2 +- crates/core/database/Cargo.toml | 2 +- crates/core/files/Cargo.toml | 2 +- crates/core/models/Cargo.toml | 2 +- crates/core/parser/Cargo.toml | 2 +- crates/core/permissions/Cargo.toml | 2 +- crates/core/presence/Cargo.toml | 2 +- crates/core/ratelimits/Cargo.toml | 2 +- crates/core/result/Cargo.toml | 2 +- crates/daemons/crond/Cargo.toml | 2 +- crates/daemons/pushd/Cargo.toml | 2 +- crates/daemons/voice-ingress/Cargo.toml | 2 +- crates/delta/Cargo.toml | 2 +- crates/services/autumn/Cargo.toml | 2 +- crates/services/gifbox/Cargo.toml | 2 +- crates/services/january/Cargo.toml | 2 +- version.txt | 2 +- 23 files changed, 69 insertions(+), 48 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 429a83fc..59c119ef 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.5" + ".": "0.13.6" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1921756b..2a9c37a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [0.13.6](https://github.com/stoatchat/stoatchat/compare/v0.13.5...v0.13.6) (2026-05-18) + + +### Features + +* Update FCM payload for android notifications ([#766](https://github.com/stoatchat/stoatchat/issues/766)) ([acbc087](https://github.com/stoatchat/stoatchat/commit/acbc087982e9aeb05cabc5ab4c9b1291f67490ad)) +* user slowmode events ([#760](https://github.com/stoatchat/stoatchat/issues/760)) ([af0d8aa](https://github.com/stoatchat/stoatchat/commit/af0d8aad14dc68d88159d0e1c714077d362e21e4)) + + +### Bug Fixes + +* include `minio` region as tests need it ([#761](https://github.com/stoatchat/stoatchat/issues/761)) ([298742d](https://github.com/stoatchat/stoatchat/commit/298742dbad4eafae356f976c56b9db23904b0c3a)) +* set env var for publishing crates ([#768](https://github.com/stoatchat/stoatchat/issues/768)) ([018afaf](https://github.com/stoatchat/stoatchat/commit/018afaf38f6330d92dad2a68b640c0cb3f6b639a)) +* Use proper headers to determine IP when not behind cloudflare ([#764](https://github.com/stoatchat/stoatchat/issues/764)) ([494c8b7](https://github.com/stoatchat/stoatchat/commit/494c8b7cabaae2a51039a7a5b559d5e2e5279554)) +* voice ingress crashing due to new Result in AMQP::new_auto() ([#765](https://github.com/stoatchat/stoatchat/issues/765)) ([2871632](https://github.com/stoatchat/stoatchat/commit/2871632382395cb20cbe0047c542d3ac31ff3f03)) + + +### Miscellaneous Chores + +* switch to lapin ([#767](https://github.com/stoatchat/stoatchat/issues/767)) ([5b19853](https://github.com/stoatchat/stoatchat/commit/5b1985381ae829a92c80a19e91a414cd9dc4de93)) + ## [0.13.5](https://github.com/stoatchat/stoatchat/compare/v0.13.4...v0.13.5) (2026-05-17) diff --git a/Cargo.lock b/Cargo.lock index c65f954e..69e48fdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7488,7 +7488,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.13.5" +version = "0.13.6" dependencies = [ "axum", "axum-macros", @@ -7529,7 +7529,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7560,7 +7560,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.13.5" +version = "0.13.6" dependencies = [ "indexmap 2.13.1", "lru", @@ -7569,7 +7569,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-std", "cached", @@ -7586,7 +7586,7 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.13.5" +version = "0.13.6" dependencies = [ "futures-lite", "iso8601-timestamp", @@ -7606,7 +7606,7 @@ dependencies = [ [[package]] name = "revolt-database" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-lock 2.8.0", "async-recursion", @@ -7657,7 +7657,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7706,7 +7706,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.13.5" +version = "0.13.6" dependencies = [ "aes-gcm", "anyhow", @@ -7734,7 +7734,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.13.5" +version = "0.13.6" dependencies = [ "axum", "axum-extra", @@ -7757,7 +7757,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-recursion", "axum", @@ -7787,7 +7787,7 @@ dependencies = [ [[package]] name = "revolt-models" -version = "0.13.5" +version = "0.13.6" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7806,14 +7806,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.13.5" +version = "0.13.6" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-std", "async-trait", @@ -7828,7 +7828,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-std", "log", @@ -7840,7 +7840,7 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.13.5" +version = "0.13.6" dependencies = [ "anyhow", "async-trait", @@ -7871,7 +7871,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-trait", "authifier", @@ -7888,7 +7888,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.13.5" +version = "0.13.6" dependencies = [ "axum", "log", @@ -7904,7 +7904,7 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.13.5" +version = "0.13.6" dependencies = [ "async-std", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 3b2b92fa..d530ac30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -191,13 +191,13 @@ futures-lite = "2.6.1" vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.13.5", path = "crates/core/coalesced" } -revolt-config = { version = "0.13.5", path = "crates/core/config" } -revolt-database = { version = "0.13.5", path = "crates/core/database" } -revolt-files = { version = "0.13.5", path = "crates/core/files" } -revolt-models = { version = "0.13.5", path = "crates/core/models" } -revolt-parser = { version = "0.13.5", path = "crates/core/parser" } -revolt-permissions = { version = "0.13.5", path = "crates/core/permissions" } -revolt-presence = { version = "0.13.5", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.13.5", path = "crates/core/ratelimits" } -revolt-result = { version = "0.13.5", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.6", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.6", path = "crates/core/config" } +revolt-database = { version = "0.13.6", path = "crates/core/database" } +revolt-files = { version = "0.13.6", path = "crates/core/files" } +revolt-models = { version = "0.13.6", path = "crates/core/models" } +revolt-parser = { version = "0.13.6", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.6", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.6", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.6", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.6", path = "crates/core/result" } diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index c0fbe500..d570b52b 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.13.5" +version = "0.13.6" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index 7c8bdcb9..e501633d 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index c8be6a4f..d6a59245 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index dc213fd0..2294cc29 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index a17be124..b38620ff 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index 75976fc5..a94816d1 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index e73e0994..7c217006 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index 3ffbf1b1..bac29b7a 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index f48182f7..c5743e5c 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index 7d5be0c1..1f17c28a 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.13.5" +version = "0.13.6" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 25caa496..f5d23dd2 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index 8b1d0376..249c1129 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.13.5" +version = "0.13.6" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index 8fb22999..3eeb084d 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index e6996742..f43f025b 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.13.5" +version = "0.13.6" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 535da2af..4fda6be0 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.13.5" +version = "0.13.6" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index c45d8522..ef46936b 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 5fbcba08..8bec9132 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index 55ae526e..b1e36ccb 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.13.5" +version = "0.13.6" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/version.txt b/version.txt index c37136a8..ebf55b3d 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.13.5 +0.13.6 From 0d9ae508d9d2199f0e408b8ca634d20489be6f61 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Tue, 19 May 2026 07:38:07 +0100 Subject: [PATCH 35/39] fix: update mention count badge for channel acks (#769) Signed-off-by: Zomatree --- crates/daemons/crond/src/main.rs | 6 ++-- crates/daemons/crond/src/tasks/acks.rs | 47 +++++++++++++------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/crates/daemons/crond/src/main.rs b/crates/daemons/crond/src/main.rs index c45780d5..2b9a82f6 100644 --- a/crates/daemons/crond/src/main.rs +++ b/crates/daemons/crond/src/main.rs @@ -1,5 +1,5 @@ use revolt_config::configure; -use revolt_database::DatabaseInfo; +use revolt_database::{DatabaseInfo, AMQP}; use revolt_result::Result; use tasks::{acks, file_deletion, prune_dangling_files, prune_members}; use tokio::try_join; @@ -11,11 +11,13 @@ async fn main() -> Result<()> { configure!(crond); let db = DatabaseInfo::Auto.connect().await.expect("database"); + let amqp = AMQP::new_auto().await; + try_join!( file_deletion::task(db.clone()), prune_dangling_files::task(db.clone()), prune_members::task(db.clone()), - acks::task(db.clone()) + acks::task(db.clone(), amqp.clone()), ) .map(|_| ()) } diff --git a/crates/daemons/crond/src/tasks/acks.rs b/crates/daemons/crond/src/tasks/acks.rs index 2a3dc3a6..7576bac9 100644 --- a/crates/daemons/crond/src/tasks/acks.rs +++ b/crates/daemons/crond/src/tasks/acks.rs @@ -5,14 +5,14 @@ use lapin::{ uri::{AMQPAuthority, AMQPQueryString, AMQPUri, AMQPUserInfo}, ConnectionBuilder, ConnectionProperties, ExchangeKind, }; -use log::info; +use log::{debug, info}; use redis_kiss::{get_connection, AsyncCommands, Conn as RedisConnection}; use revolt_config::config; -use revolt_database::{events::rabbit::AckEventPayload, Database}; +use revolt_database::{events::rabbit::AckEventPayload, Database, AMQP}; use revolt_result::{Result, ToRevoltError}; use serde_json; -pub async fn task(db: Database) -> Result<()> { +pub async fn task(db: Database, amqp: AMQP) -> Result<()> { let config = config().await; let mut redis = get_connection() @@ -94,12 +94,14 @@ pub async fn task(db: Database) -> Result<()> { while let Some(delivery) = consumer.next().await { if let Ok(delivery) = delivery { - let payload: std::result::Result = - serde_json::from_slice(&delivery.data); + let payload = serde_json::from_slice::(&delivery.data); + if let Ok(payload) = payload { - info!("{:?}", payload); + debug!("Received ack event: {payload:?}"); + if let Err(e) = process_channel_ack( &db, + &amqp, payload.user_id, payload.channel_id.unwrap(), &mut redis, @@ -125,6 +127,7 @@ pub async fn task(db: Database) -> Result<()> { #[allow(clippy::disallowed_methods)] async fn process_channel_ack( db: &Database, + amqp: &AMQP, user: String, channel: String, redis: &mut RedisConnection, @@ -135,28 +138,24 @@ async fn process_channel_ack( .to_internal_error()?; if let Some(message_id) = message_id { - // This will be uncommented eventually, but we need to sort out the transition to lapin first. For now we'll simply disable the badge update logic. - // We also drop a db request as a bonus. + let unread = db.fetch_unread(&user, &channel).await?; + let updated = db.acknowledge_message(&channel, &user, &message_id).await?; - //let unread = db.fetch_unread(&user, &channel).await?; - let _updated = db.acknowledge_message(&channel, &user, &message_id).await?; info!("Set new state for ack: {}:{}:{}", channel, user, message_id); - // if let (Some(before), Some(after)) = (unread, updated) { - // let before_mentions = before.mentions.unwrap_or_default().len(); - // let after_mentions = after.mentions.unwrap_or_default().len(); + if let (Some(before), Some(after)) = (unread, updated) { + let before_mentions = before.mentions.unwrap_or_default().len(); + let after_mentions = after.mentions.unwrap_or_default().len(); - // let mentions_acked = before_mentions - after_mentions; - - // if mentions_acked > 0 { - // if let Err(err) = amqp - // .ack_message(user.to_string(), channel.to_string(), payload.message_id) - // .await - // { - // revolt_config::capture_error(&err); - // } - // }; - // } + if after_mentions < before_mentions { + if let Err(err) = amqp + .ack_notification_message(user.to_string(), channel.to_string(), message_id) + .await + { + revolt_config::capture_error(&err); + } + }; + } Ok(()) } else { From 4815429952e4736b83baa8c4a57be82fc96cc79a Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 20 May 2026 13:58:18 -0500 Subject: [PATCH 36/39] ci: create Docker images for PR preview (#772) --- .github/workflows/docker-cleanup.yaml | 56 +++++++++++++++++++++++++++ .github/workflows/docker.yaml | 37 +++++++++++------- 2 files changed, 79 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/docker-cleanup.yaml diff --git a/.github/workflows/docker-cleanup.yaml b/.github/workflows/docker-cleanup.yaml new file mode 100644 index 00000000..61121259 --- /dev/null +++ b/.github/workflows/docker-cleanup.yaml @@ -0,0 +1,56 @@ +name: Docker PR Image Cleanup + +on: + pull_request: + types: + - closed + +permissions: + contents: read + packages: write + +concurrency: + group: docker-cleanup-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + enumerate: + runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.head.repo.fork }} + outputs: + packages: ${{ steps.list.outputs.packages }} + steps: + - id: list + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG: stoatchat + REPO: ${{ github.repository }} + run: | + set -euo pipefail + packages=$(gh api --paginate \ + "/orgs/${ORG}/packages?package_type=container" \ + --jq "[.[] | select(.repository.full_name == \"${REPO}\") | .name]") + echo "packages=${packages}" >> "$GITHUB_OUTPUT" + + cleanup: + needs: enumerate + runs-on: ubuntu-latest + if: ${{ needs.enumerate.outputs.packages != '[]' }} + strategy: + fail-fast: false + matrix: + package: ${{ fromJSON(needs.enumerate.outputs.packages) }} + steps: + - env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG: stoatchat + PACKAGE: ${{ matrix.package }} + TAG: pr-${{ github.event.pull_request.number }} + run: | + set -euo pipefail + gh api --paginate \ + "/orgs/${ORG}/packages/container/${PACKAGE}/versions" \ + --jq ".[] | select(.metadata.container.tags | index(\"${TAG}\")) | .id" \ + | while read -r id; do + gh api -X DELETE "/orgs/${ORG}/packages/container/${PACKAGE}/versions/${id}" + done diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 2be1f438..75aefeb6 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -5,8 +5,6 @@ on: tags: - "*" pull_request: - paths: - - "Dockerfile" workflow_dispatch: permissions: @@ -19,9 +17,9 @@ concurrency: jobs: base: - name: Test base image build + name: Test base image build (fork) runs-on: arc-runner-set - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork steps: # Configure build environment - name: Checkout @@ -42,7 +40,7 @@ jobs: publish: runs-on: arc-runner-set - if: github.event_name != 'pull_request' + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} name: Publish Docker images steps: # Configure build environment @@ -59,6 +57,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Determine base image tag + id: base + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "tag=pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + # Build the image - name: Build base image uses: docker/build-push-action@v4 @@ -66,7 +73,9 @@ jobs: context: . push: true platforms: linux/amd64,linux/arm64 - tags: ghcr.io/${{ github.repository_owner }}/base:latest + tags: ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} + cache-from: type=gha,scope=buildx-base-multi-arch + cache-to: type=gha,scope=buildx-base-multi-arch,mode=max # stoatchat/api - name: Docker meta @@ -84,7 +93,7 @@ jobs: file: crates/delta/Dockerfile tags: ${{ steps.meta-delta.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-delta.outputs.labels }} # stoatchat/events @@ -103,7 +112,7 @@ jobs: file: crates/bonfire/Dockerfile tags: ${{ steps.meta-bonfire.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-bonfire.outputs.labels }} # stoatchat/file-server @@ -122,7 +131,7 @@ jobs: file: crates/services/autumn/Dockerfile tags: ${{ steps.meta-autumn.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-autumn.outputs.labels }} # stoatchat/proxy @@ -141,7 +150,7 @@ jobs: file: crates/services/january/Dockerfile tags: ${{ steps.meta-january.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-january.outputs.labels }} # stoatchat/gifbox @@ -160,7 +169,7 @@ jobs: file: crates/services/gifbox/Dockerfile tags: ${{ steps.meta-gifbox.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-gifbox.outputs.labels }} # stoatchat/crond @@ -179,7 +188,7 @@ jobs: file: crates/daemons/crond/Dockerfile tags: ${{ steps.meta-crond.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-crond.outputs.labels }} # stoatchat/pushd @@ -198,7 +207,7 @@ jobs: file: crates/daemons/pushd/Dockerfile tags: ${{ steps.meta-pushd.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-pushd.outputs.labels }} # stoatchat/voice-ingress @@ -217,5 +226,5 @@ jobs: file: crates/daemons/voice-ingress/Dockerfile tags: ${{ steps.meta-voice-ingress.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-voice-ingress.outputs.labels }} From b38499a05be489a597f681d1ff4fafae69d60603 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Wed, 20 May 2026 14:10:09 -0500 Subject: [PATCH 37/39] ci: hard code packages, gh token limitation [skip ci] (#773) --- .github/workflows/docker-cleanup.yaml | 32 +++++++++------------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/.github/workflows/docker-cleanup.yaml b/.github/workflows/docker-cleanup.yaml index 61121259..83061949 100644 --- a/.github/workflows/docker-cleanup.yaml +++ b/.github/workflows/docker-cleanup.yaml @@ -14,32 +14,22 @@ concurrency: cancel-in-progress: false jobs: - enumerate: + cleanup: runs-on: ubuntu-latest if: ${{ !github.event.pull_request.head.repo.fork }} - outputs: - packages: ${{ steps.list.outputs.packages }} - steps: - - id: list - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ORG: stoatchat - REPO: ${{ github.repository }} - run: | - set -euo pipefail - packages=$(gh api --paginate \ - "/orgs/${ORG}/packages?package_type=container" \ - --jq "[.[] | select(.repository.full_name == \"${REPO}\") | .name]") - echo "packages=${packages}" >> "$GITHUB_OUTPUT" - - cleanup: - needs: enumerate - runs-on: ubuntu-latest - if: ${{ needs.enumerate.outputs.packages != '[]' }} strategy: fail-fast: false matrix: - package: ${{ fromJSON(needs.enumerate.outputs.packages) }} + package: + - base + - api + - events + - file-server + - proxy + - gifbox + - crond + - pushd + - voice-ingress steps: - env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2d308e03d58c19f27b5b4d65dc2a15ef20b56190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Thu, 21 May 2026 18:21:43 +0300 Subject: [PATCH 38/39] fix: sanitize emoji input to handle variation selectors (#774) Signed-off-by: ispik --- crates/core/database/src/models/emojis/model.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/core/database/src/models/emojis/model.rs b/crates/core/database/src/models/emojis/model.rs index 3f380716..55d13e8c 100644 --- a/crates/core/database/src/models/emojis/model.rs +++ b/crates/core/database/src/models/emojis/model.rs @@ -12,7 +12,7 @@ use crate::Database; static PERMISSIBLE_EMOJIS: Lazy> = Lazy::new(|| { include_str!("unicode_emoji.txt") .split('\n') - .map(|x| x.into()) + .map(|x| x.replace('\u{FE0F}', "")) .collect() }); @@ -108,7 +108,8 @@ impl Emoji { db.fetch_emoji(emoji).await?; Ok(true) } else { - Ok(PERMISSIBLE_EMOJIS.contains(emoji)) + let sanitized_emoji = emoji.replace('\u{FE0F}', ""); + Ok(PERMISSIBLE_EMOJIS.contains(&sanitized_emoji)) } } } From 7937179db771b6cafb959da9e54dc9aff3bb56b3 Mon Sep 17 00:00:00 2001 From: "stoat-release[bot]" <245062572+stoat-release[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 19:33:49 +0100 Subject: [PATCH 39/39] chore(main): release 0.13.7 (#770) Co-authored-by: github-actions[bot] Signed-off-by: github-actions[bot] --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++ Cargo.lock | 36 ++++++++++++------------- Cargo.toml | 20 +++++++------- crates/bonfire/Cargo.toml | 2 +- crates/core/coalesced/Cargo.toml | 2 +- crates/core/config/Cargo.toml | 2 +- crates/core/database/Cargo.toml | 2 +- crates/core/files/Cargo.toml | 2 +- crates/core/models/Cargo.toml | 2 +- crates/core/parser/Cargo.toml | 2 +- crates/core/permissions/Cargo.toml | 2 +- crates/core/presence/Cargo.toml | 2 +- crates/core/ratelimits/Cargo.toml | 2 +- crates/core/result/Cargo.toml | 2 +- crates/daemons/crond/Cargo.toml | 2 +- crates/daemons/pushd/Cargo.toml | 2 +- crates/daemons/voice-ingress/Cargo.toml | 2 +- crates/delta/Cargo.toml | 2 +- crates/services/autumn/Cargo.toml | 2 +- crates/services/gifbox/Cargo.toml | 2 +- crates/services/january/Cargo.toml | 2 +- version.txt | 2 +- 23 files changed, 56 insertions(+), 48 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 59c119ef..29781928 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.13.6" + ".": "0.13.7" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a9c37a9..9912f5b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.13.7](https://github.com/stoatchat/stoatchat/compare/v0.13.6...v0.13.7) (2026-05-21) + + +### Bug Fixes + +* sanitize emoji input to handle variation selectors ([#774](https://github.com/stoatchat/stoatchat/issues/774)) ([2d308e0](https://github.com/stoatchat/stoatchat/commit/2d308e03d58c19f27b5b4d65dc2a15ef20b56190)) +* update mention count badge for channel acks ([#769](https://github.com/stoatchat/stoatchat/issues/769)) ([0d9ae50](https://github.com/stoatchat/stoatchat/commit/0d9ae508d9d2199f0e408b8ca634d20489be6f61)) + ## [0.13.6](https://github.com/stoatchat/stoatchat/compare/v0.13.5...v0.13.6) (2026-05-18) diff --git a/Cargo.lock b/Cargo.lock index 69e48fdc..bcd1c5ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7488,7 +7488,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.13.6" +version = "0.13.7" dependencies = [ "axum", "axum-macros", @@ -7529,7 +7529,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7560,7 +7560,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.13.6" +version = "0.13.7" dependencies = [ "indexmap 2.13.1", "lru", @@ -7569,7 +7569,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-std", "cached", @@ -7586,7 +7586,7 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.13.6" +version = "0.13.7" dependencies = [ "futures-lite", "iso8601-timestamp", @@ -7606,7 +7606,7 @@ dependencies = [ [[package]] name = "revolt-database" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-lock 2.8.0", "async-recursion", @@ -7657,7 +7657,7 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-channel 2.5.0", "async-std", @@ -7706,7 +7706,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.13.6" +version = "0.13.7" dependencies = [ "aes-gcm", "anyhow", @@ -7734,7 +7734,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.13.6" +version = "0.13.7" dependencies = [ "axum", "axum-extra", @@ -7757,7 +7757,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-recursion", "axum", @@ -7787,7 +7787,7 @@ dependencies = [ [[package]] name = "revolt-models" -version = "0.13.6" +version = "0.13.7" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7806,14 +7806,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.13.6" +version = "0.13.7" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-std", "async-trait", @@ -7828,7 +7828,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-std", "log", @@ -7840,7 +7840,7 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.13.6" +version = "0.13.7" dependencies = [ "anyhow", "async-trait", @@ -7871,7 +7871,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-trait", "authifier", @@ -7888,7 +7888,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.13.6" +version = "0.13.7" dependencies = [ "axum", "log", @@ -7904,7 +7904,7 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.13.6" +version = "0.13.7" dependencies = [ "async-std", "chrono", diff --git a/Cargo.toml b/Cargo.toml index d530ac30..6d1335cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -191,13 +191,13 @@ futures-lite = "2.6.1" vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.13.6", path = "crates/core/coalesced" } -revolt-config = { version = "0.13.6", path = "crates/core/config" } -revolt-database = { version = "0.13.6", path = "crates/core/database" } -revolt-files = { version = "0.13.6", path = "crates/core/files" } -revolt-models = { version = "0.13.6", path = "crates/core/models" } -revolt-parser = { version = "0.13.6", path = "crates/core/parser" } -revolt-permissions = { version = "0.13.6", path = "crates/core/permissions" } -revolt-presence = { version = "0.13.6", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.13.6", path = "crates/core/ratelimits" } -revolt-result = { version = "0.13.6", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.7", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.7", path = "crates/core/config" } +revolt-database = { version = "0.13.7", path = "crates/core/database" } +revolt-files = { version = "0.13.7", path = "crates/core/files" } +revolt-models = { version = "0.13.7", path = "crates/core/models" } +revolt-parser = { version = "0.13.7", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.7", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.7", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.7", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.7", path = "crates/core/result" } diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index d570b52b..b2687266 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.13.6" +version = "0.13.7" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index e501633d..1c8102ad 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index d6a59245..e9e329c7 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index 2294cc29..f820c899 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index b38620ff..c60bf678 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index a94816d1..c92f3f8a 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index 7c217006..5bb079ac 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index bac29b7a..03ed629d 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index c5743e5c..7868342d 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index 1f17c28a..e606d151 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.13.6" +version = "0.13.7" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index f5d23dd2..9cc5db3f 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index 249c1129..bb2f6ced 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.13.6" +version = "0.13.7" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index 3eeb084d..9f38020e 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index f43f025b..e28e7120 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.13.6" +version = "0.13.7" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 4fda6be0..87028aba 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.13.6" +version = "0.13.7" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index ef46936b..7c134170 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 8bec9132..a569b2cd 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index b1e36ccb..f72f6572 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.13.6" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/version.txt b/version.txt index ebf55b3d..5daaa7ba 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.13.6 +0.13.7