From 791c247fbdc457b73a06f052b0574bef7ad75c80 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 10 Oct 2025 15:11:31 +0200 Subject: [PATCH 001/100] feat(api): add stop and start API route --- Cargo.lock | 99 ++++++++++- Cargo.toml | 4 +- src/api/handler/bot/mod.rs | 2 + src/api/handler/bot/start.rs | 38 ++++ src/api/handler/bot/stop.rs | 26 +++ src/api/handler/mod.rs | 1 + src/api/mod.rs | 3 + src/api/router.rs | 14 ++ src/api/routes/bot.rs | 14 ++ src/api/routes/mod.rs | 1 + src/bot.rs | 165 ++++++++++++++++++ src/commands/add_reminder/common.rs | 14 +- .../slash_command/add_reminder.rs | 11 +- .../add_reminder/text_command/add_reminder.rs | 17 +- .../add_staff/slash_command/add_staff.rs | 5 +- .../add_staff/text_command/add_staff.rs | 11 +- src/commands/alert/slash_command/alert.rs | 7 +- src/commands/alert/text_command/alert.rs | 11 +- .../anonreply/text_command/anonreply.rs | 15 +- src/commands/close/slash_command/close.rs | 7 +- src/commands/close/text_command/close.rs | 11 +- src/commands/delete/slash_command/delete.rs | 7 +- src/commands/delete/text_command/delete.rs | 11 +- src/commands/edit/slash_command/edit.rs | 7 +- src/commands/edit/text_command/edit.rs | 15 +- .../force_close/slash_command/force_close.rs | 7 +- .../force_close/text_command/force_close.rs | 11 +- src/commands/help/slash_command/help.rs | 5 +- src/commands/help/text_command/help.rs | 9 +- src/commands/id/slash_command/id.rs | 5 +- src/commands/id/text_command/id.rs | 11 +- src/commands/mod.rs | 10 +- .../move_thread/slash_command/move_thread.rs | 5 +- .../move_thread/text_command/move_thread.rs | 13 +- .../new_thread/slash_command/new_thread.rs | 7 +- .../new_thread/text_command/new_thread.rs | 11 +- src/commands/recover/slash_command/recover.rs | 7 +- src/commands/recover/text_command/recover.rs | 11 +- .../slash_command/remove_reminder.rs | 7 +- .../text_command/remove_reminder.rs | 11 +- .../slash_command/remove_staff.rs | 7 +- .../remove_staff/text_command/remove_staff.rs | 11 +- src/commands/reply/slash_command/reply.rs | 9 +- src/commands/reply/text_command/reply.rs | 13 +- src/config.rs | 10 +- src/handlers/guild_interaction_handler.rs | 9 +- src/handlers/guild_messages_handler.rs | 23 ++- src/handlers/ready_handler.rs | 13 +- src/main.rs | 145 ++++----------- src/modules/reminders.rs | 11 +- src/utils/command/wrap_command.rs | 12 +- 51 files changed, 698 insertions(+), 211 deletions(-) create mode 100644 src/api/handler/bot/mod.rs create mode 100644 src/api/handler/bot/start.rs create mode 100644 src/api/handler/bot/stop.rs create mode 100644 src/api/handler/mod.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/router.rs create mode 100644 src/api/routes/bot.rs create mode 100644 src/api/routes/mod.rs create mode 100644 src/bot.rs diff --git a/Cargo.lock b/Cargo.lock index 336f73b..31bdf75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,58 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -880,19 +932,22 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "httparse", + "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -919,7 +974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "rustls 0.23.27", "rustls-pki-types", @@ -936,7 +991,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "native-tls", "tokio", @@ -957,7 +1012,7 @@ dependencies = [ "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", "ipnet", "libc", "percent-encoding", @@ -1220,6 +1275,12 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -1684,7 +1745,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.27.6", "hyper-tls", "hyper-util", @@ -1857,7 +1918,9 @@ name = "rustmail" version = "0.1.0" dependencies = [ "async-trait", + "axum", "chrono", + "hyper 1.7.0", "regex", "reqwest 0.12.23", "serde", @@ -2007,6 +2070,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "1.0.2" @@ -2093,6 +2167,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -2609,6 +2692,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "slab", "socket2 0.6.0", "tokio-macros", @@ -2759,6 +2843,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 50d0746..7996123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,14 @@ license = "MIT" serde = { version = "*", features = ["derive"] } serenity = "0.12.4" sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "macros", "migrate"] } -tokio = { version = "1.47.1", features = ["rt-multi-thread"] } +tokio = { version = "1.47.1", features = ["rt-multi-thread", "signal"] } toml = "0.9.7" reqwest = "0.12.23" async-trait = "0.1.89" chrono = { version = "0.4.42", features = ["serde"] } regex = "1.11.3" +axum = "0.8.6" +hyper = "1.7.0" [dependencies.uuid] version = "1.18.1" diff --git a/src/api/handler/bot/mod.rs b/src/api/handler/bot/mod.rs new file mode 100644 index 0000000..99457f5 --- /dev/null +++ b/src/api/handler/bot/mod.rs @@ -0,0 +1,2 @@ +pub mod start; +pub mod stop; diff --git a/src/api/handler/bot/start.rs b/src/api/handler/bot/start.rs new file mode 100644 index 0000000..fad307b --- /dev/null +++ b/src/api/handler/bot/start.rs @@ -0,0 +1,38 @@ +use crate::bot::run_bot; +use crate::{BotState, BotStatus}; +use axum::extract::State; +use axum::http::StatusCode; +use axum::Json; +use std::sync::{Arc, Mutex}; +use tokio::spawn; + +pub async fn handle_start_bot( + State(bot_state): State>>, +) -> (StatusCode, Json<&'static str>) { + let mut state_lock = bot_state.lock().unwrap(); + + match state_lock.status { + BotStatus::Stopped => { + if state_lock.config.is_none() { + return (StatusCode::BAD_REQUEST, Json("Missing configuration.")); + } + + let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false); + let bot_state_clone = bot_state.clone(); + + let handle = spawn(async move { + run_bot(bot_state_clone.clone(), &mut shutdown_rx).await; + let mut s = bot_state_clone.lock().unwrap(); + s.status = BotStatus::Stopped; + }); + state_lock.status = BotStatus::Running { + handle, + shutdown: shutdown_tx, + }; + + drop(state_lock); + (StatusCode::OK, Json("Bot is starting")) + } + BotStatus::Running { .. } => (StatusCode::CONFLICT, Json("Bot is already running")), + } +} diff --git a/src/api/handler/bot/stop.rs b/src/api/handler/bot/stop.rs new file mode 100644 index 0000000..58c88c4 --- /dev/null +++ b/src/api/handler/bot/stop.rs @@ -0,0 +1,26 @@ +use crate::{BotState, BotStatus}; +use axum::extract::State; +use axum::http::StatusCode; +use axum::Json; +use std::sync::{Arc, Mutex}; + +pub async fn handle_stop_bot( + State(bot_state): State>>, +) -> (StatusCode, Json<&'static str>) { + let handle_and_shutdown = { + let mut state_lock = bot_state.lock().unwrap(); + + match std::mem::replace(&mut state_lock.status, BotStatus::Stopped) { + BotStatus::Running { handle, shutdown } => Some((handle, shutdown)), + BotStatus::Stopped => None, + } + }; + + if let Some((handle, shutdown_tx)) = handle_and_shutdown { + let _ = shutdown_tx.send(true); + handle.await.unwrap(); + (StatusCode::OK, Json("Bot stopped")) + } else { + (StatusCode::CONFLICT, Json("Bot is not running")) + } +} diff --git a/src/api/handler/mod.rs b/src/api/handler/mod.rs new file mode 100644 index 0000000..1cbfd96 --- /dev/null +++ b/src/api/handler/mod.rs @@ -0,0 +1 @@ +pub mod bot; diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..317fc3e --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,3 @@ +pub mod handler; +pub mod router; +pub mod routes; diff --git a/src/api/router.rs b/src/api/router.rs new file mode 100644 index 0000000..4ded206 --- /dev/null +++ b/src/api/router.rs @@ -0,0 +1,14 @@ +use crate::api::routes::bot::create_bot_router; +use crate::BotState; +use axum::Router; +use std::sync::{Arc, Mutex}; + +pub fn create_api_router(bot_state: Arc>) -> Router { + let bot_router = create_bot_router(); + + let app = Router::new() + .nest("/bot", bot_router) + .with_state(bot_state.clone()); + + app +} diff --git a/src/api/routes/bot.rs b/src/api/routes/bot.rs new file mode 100644 index 0000000..0700815 --- /dev/null +++ b/src/api/routes/bot.rs @@ -0,0 +1,14 @@ +use crate::api::handler::bot::start::handle_start_bot; +use crate::api::handler::bot::stop::handle_stop_bot; +use crate::BotState; +use axum::routing::post; +use axum::Router; +use std::sync::{Arc, Mutex}; + +pub fn create_bot_router() -> Router>> { + let bot_router = Router::new() + .route("/start", post(handle_start_bot)) + .route("/stop", post(handle_stop_bot)); + + bot_router +} diff --git a/src/api/routes/mod.rs b/src/api/routes/mod.rs new file mode 100644 index 0000000..1cbfd96 --- /dev/null +++ b/src/api/routes/mod.rs @@ -0,0 +1 @@ +pub mod bot; diff --git a/src/bot.rs b/src/bot.rs new file mode 100644 index 0000000..8c5a86c --- /dev/null +++ b/src/bot.rs @@ -0,0 +1,165 @@ +use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; +use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; +use crate::commands::alert::slash_command::alert::AlertCommand; +use crate::commands::close::slash_command::close::CloseCommand; +use crate::commands::delete::slash_command::delete::DeleteCommand; +use crate::commands::edit::slash_command::edit::EditCommand; +use crate::commands::force_close::slash_command::force_close::ForceCloseCommand; +use crate::commands::help::slash_command::help::HelpCommand; +use crate::commands::id::slash_command::id::IdCommand; +use crate::commands::move_thread::slash_command::move_thread::MoveCommand; +use crate::commands::new_thread::slash_command::new_thread::NewThreadCommand; +use crate::commands::recover::slash_command::recover::RecoverCommand; +use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; +use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; +use crate::commands::reply::slash_command::reply::ReplyCommand; +use crate::commands::CommandRegistry; +use crate::config::load_config; +use crate::handlers::guild_handler::GuildHandler; +use crate::handlers::guild_interaction_handler::InteractionHandler; +use crate::handlers::guild_members_handler::GuildMembersHandler; +use crate::handlers::guild_message_reactions_handler::GuildMessageReactionsHandler; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; +use crate::handlers::guild_moderation_handler::GuildModerationHandler; +use crate::handlers::ready_handler::ReadyHandler; +use crate::handlers::typing_proxy_handler::TypingProxyHandler; +use crate::{db, BotState, BotStatus}; +use serenity::all::{ClientBuilder, GatewayIntents}; +use serenity::cache::Settings as CacheSettings; +use std::process; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tokio::sync::watch::Receiver; +use tokio::{select, spawn}; + +pub async fn init_bot_state() -> Arc> { + let pool = db::operations::init_database() + .await + .expect("An error occured!"); + println!("Database connected!"); + + let config = load_config("config.toml"); + + let bot_state = BotState { + config, + status: BotStatus::Stopped, + db_pool: Some(pool), + }; + + Arc::new(Mutex::new(bot_state)) +} + +pub async fn run_bot(bot_state: Arc>, shutdown: &mut Receiver) { + let shutdown_rx_command = shutdown.clone(); + let shutdown_rx = shutdown.clone(); + + let pool = { + let state_lock = bot_state.lock().unwrap(); + state_lock.db_pool.clone().expect("Database pool not set") + }; + + let mut config = { + let state_lock = bot_state.lock().unwrap(); + state_lock.config.clone().expect("Config not set") + }; + + config.db_pool = Some(pool.clone()); + + let intents = GatewayIntents::GUILDS + | GatewayIntents::GUILD_MESSAGES + | GatewayIntents::MESSAGE_CONTENT + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::GUILD_MESSAGE_TYPING + | GatewayIntents::DIRECT_MESSAGE_TYPING + | GatewayIntents::GUILD_MEMBERS + | GatewayIntents::GUILD_PRESENCES + | GatewayIntents::GUILD_MESSAGE_REACTIONS + | GatewayIntents::DIRECT_MESSAGE_REACTIONS + | GatewayIntents::GUILD_MODERATION; + + let mut cache_settings = CacheSettings::default(); + cache_settings.max_messages = 10_000; + cache_settings.time_to_live = Duration::from_secs(6 * 60 * 60); + + let mut registry = CommandRegistry::new(shutdown_rx_command); + registry.register_command(AddStaffCommand); + registry.register_command(AlertCommand); + registry.register_command(CloseCommand); + registry.register_command(DeleteCommand); + registry.register_command(EditCommand); + registry.register_command(ForceCloseCommand); + registry.register_command(HelpCommand); + registry.register_command(IdCommand); + registry.register_command(MoveCommand); + registry.register_command(NewThreadCommand); + registry.register_command(RecoverCommand); + registry.register_command(RemoveStaffCommand); + registry.register_command(ReplyCommand); + registry.register_command(AddReminderCommand); + registry.register_command(RemoveReminderCommand); + + let registry = Arc::new(registry); + + let mut client: serenity::Client = ClientBuilder::new(config.bot.token.clone(), intents) + .cache_settings(cache_settings) + .event_handler(ReadyHandler::new( + &config, + registry.clone(), + Arc::new(shutdown_rx.clone()), + )) + .event_handler(GuildMessagesHandler::new(&config, shutdown_rx.clone())) + .event_handler(TypingProxyHandler::new(&config)) + .event_handler(GuildMembersHandler::new(&config)) + .event_handler(GuildMessageReactionsHandler::new(&config)) + .event_handler(GuildModerationHandler::new(&config)) + .event_handler(InteractionHandler::new( + &config, + registry.clone(), + shutdown_rx, + )) + .event_handler(GuildHandler::new(&config)) + .await + .expect("Failed to create client."); + + if let Err(e) = config.validate_servers(&client.http).await { + eprintln!("Configuration validation error: {}", e); + eprintln!( + "Check that the server IDs are correct and that the bot has access to the servers." + ); + process::exit(1); + } + + println!("Configuration successfully validated!!"); + if config.bot.is_dual_mode() { + println!( + "Mode: Dual server (Community: {}, Staff: {})", + config.bot.get_community_guild_id(), + config.bot.get_staff_guild_id() + ); + } else { + println!( + "Mode: Mono server (ID: {})", + config.bot.get_community_guild_id() + ); + } + + let shard_manager = client.shard_manager.clone(); + + let discord_task = spawn(async move { + if let Err(e) = client.start().await { + println!("Failed to initialize client: {e}"); + } + }); + + select! { + _ = shutdown.changed() => { + println!("Shutdown signal received, shutting down..."); + shard_manager.shutdown_all().await; + } + _ = discord_task => { + println!("Discord task ended."); + } + } + + println!("Bot has been shut down."); +} diff --git a/src/commands/add_reminder/common.rs b/src/commands/add_reminder/common.rs index c336510..286001a 100644 --- a/src/commands/add_reminder/common.rs +++ b/src/commands/add_reminder/common.rs @@ -1,12 +1,15 @@ use crate::config::Config; -use crate::db::reminders::{Reminder, is_reminder_active, update_reminder_status}; +use crate::db::reminders::{is_reminder_active, update_reminder_status, Reminder}; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; use chrono::Local; use serenity::all::{ChannelId, CommandInteraction, Context, Message, UserId}; use sqlx::SqlitePool; use std::collections::HashMap; +use std::sync::Arc; use std::time::Duration; +use tokio::select; +use tokio::sync::watch::Receiver; use tokio::time::sleep; pub async fn send_register_confirmation_from_message( @@ -115,11 +118,13 @@ pub fn spawn_reminder( ctx: &Context, config: &Config, pool: &SqlitePool, + shutdown: Arc>, ) { let pool = pool.clone(); let config = config.clone(); let ctx = ctx.clone(); let reminder = reminder.clone(); + let mut shutdown_rx = shutdown.as_ref().clone(); tokio::spawn(async move { let now = Local::now().timestamp(); @@ -128,7 +133,12 @@ pub fn spawn_reminder( } else { 0 }; - sleep(Duration::from_secs(delay_duration as u64)).await; + select! { + _ = sleep(Duration::from_secs(delay_duration as u64)) => {} + _ = shutdown_rx.changed() => { + return; + } + } if let Some(reminder_id) = reminder_id { match is_reminder_active(reminder_id, &pool).await { diff --git a/src/commands/add_reminder/slash_command/add_reminder.rs b/src/commands/add_reminder/slash_command/add_reminder.rs index e3d0730..cb8077a 100644 --- a/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/src/commands/add_reminder/slash_command/add_reminder.rs @@ -3,10 +3,10 @@ use crate::commands::add_reminder::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::db::reminders::{Reminder, insert_reminder}; +use crate::db::reminders::{insert_reminder, Reminder}; use crate::db::threads::get_thread_by_user_id; use crate::errors::{ - CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, common, + common, CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, }; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; @@ -16,6 +16,8 @@ use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct AddReminderCommand; @@ -81,8 +83,9 @@ impl RegistrableCommand for AddReminderCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); @@ -199,7 +202,7 @@ impl RegistrableCommand for AddReminderCommand { ) .await; - spawn_reminder(&reminder, Some(reminder_id), &ctx, &config, &pool); + spawn_reminder(&reminder, Some(reminder_id), &ctx, &config, &pool, shutdown); Ok(()) }) diff --git a/src/commands/add_reminder/text_command/add_reminder.rs b/src/commands/add_reminder/text_command/add_reminder.rs index 78aa397..fb1a837 100644 --- a/src/commands/add_reminder/text_command/add_reminder.rs +++ b/src/commands/add_reminder/text_command/add_reminder.rs @@ -2,17 +2,24 @@ use crate::commands::add_reminder::common::{ send_register_confirmation_from_message, spawn_reminder, }; use crate::config::Config; -use crate::db::reminders::{Reminder, insert_reminder}; +use crate::db::reminders::{insert_reminder, Reminder}; use crate::db::threads::get_thread_by_user_id; use crate::errors::{ - CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, common, + common, CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, }; use crate::utils::command::extract_reply_content::extract_reply_content; use chrono::{Local, NaiveTime}; use regex::Regex; use serenity::all::{Context, Message}; - -pub async fn add_reminder(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +use std::sync::Arc; +use tokio::sync::watch::Receiver; + +pub async fn add_reminder( + ctx: &Context, + msg: &Message, + config: &Config, + shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() @@ -100,7 +107,7 @@ pub async fn add_reminder(ctx: &Context, msg: &Message, config: &Config) -> Modm let _ = msg.delete(&ctx.http).await; - spawn_reminder(&reminder, Some(reminder_id), &ctx, &config, &pool); + spawn_reminder(&reminder, Some(reminder_id), &ctx, &config, &pool, shutdown); Ok(()) } diff --git a/src/commands/add_staff/slash_command/add_staff.rs b/src/commands/add_staff/slash_command/add_staff.rs index 7bc5f0c..a0dd581 100644 --- a/src/commands/add_staff/slash_command/add_staff.rs +++ b/src/commands/add_staff/slash_command/add_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; @@ -13,6 +13,8 @@ use serenity::all::{ CreateCommand, CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct AddStaffCommand; @@ -63,6 +65,7 @@ impl RegistrableCommand for AddStaffCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/add_staff/text_command/add_staff.rs b/src/commands/add_staff/text_command/add_staff.rs index c7585a2..5450f37 100644 --- a/src/commands/add_staff/text_command/add_staff.rs +++ b/src/commands/add_staff/text_command/add_staff.rs @@ -4,12 +4,19 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn add_staff(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn add_staff( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() diff --git a/src/commands/alert/slash_command/alert.rs b/src/commands/alert/slash_command/alert.rs index da5f3c9..9a4db8a 100644 --- a/src/commands/alert/slash_command/alert.rs +++ b/src/commands/alert/slash_command/alert.rs @@ -4,13 +4,15 @@ use crate::commands::alert::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct AlertCommand; @@ -58,8 +60,9 @@ impl RegistrableCommand for AlertCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/alert/text_command/alert.rs b/src/commands/alert/text_command/alert.rs index 17ce4c1..9104b15 100644 --- a/src/commands/alert/text_command/alert.rs +++ b/src/commands/alert/text_command/alert.rs @@ -3,10 +3,17 @@ use crate::commands::alert::common::{ handle_set_alert_from_msg, }; use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use serenity::all::{Context, Message}; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn alert(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn alert( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() diff --git a/src/commands/anonreply/text_command/anonreply.rs b/src/commands/anonreply/text_command/anonreply.rs index fd9b644..72f1a41 100644 --- a/src/commands/anonreply/text_command/anonreply.rs +++ b/src/commands/anonreply/text_command/anonreply.rs @@ -1,14 +1,21 @@ use crate::config::Config; use crate::db::operations::allocate_next_message_number; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; - -pub async fn anonreply(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +use std::sync::Arc; +use tokio::sync::watch::Receiver; + +pub async fn anonreply( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let db_pool = config .db_pool .as_ref() diff --git a/src/commands/close/slash_command/close.rs b/src/commands/close/slash_command/close.rs index d93da4b..63d4855 100644 --- a/src/commands/close/slash_command/close.rs +++ b/src/commands/close/slash_command/close.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; @@ -15,7 +15,9 @@ use serenity::all::{ CreateCommandOption, GuildId, ResolvedOption, UserId, }; use std::collections::HashMap; +use std::sync::Arc; use std::time::Duration; +use tokio::sync::watch::Receiver; use tokio::time::sleep; pub struct CloseCommand; @@ -94,8 +96,9 @@ impl RegistrableCommand for CloseCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/close/text_command/close.rs b/src/commands/close/text_command/close.rs index dad8631..8af51ee 100644 --- a/src/commands/close/text_command/close.rs +++ b/src/commands/close/text_command/close.rs @@ -3,16 +3,23 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::fetch_thread::fetch_thread; use chrono::Utc; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; +use std::sync::Arc; use std::time::Duration; +use tokio::sync::watch::Receiver; use tokio::time::sleep; -pub async fn close(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn close( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let db_pool = config .db_pool .as_ref() diff --git a/src/commands/delete/slash_command/delete.rs b/src/commands/delete/slash_command/delete.rs index 7ba9216..abac640 100644 --- a/src/commands/delete/slash_command/delete.rs +++ b/src/commands/delete/slash_command/delete.rs @@ -5,7 +5,7 @@ use crate::commands::delete::common::{ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::messages::get_thread_message_by_message_id; -use crate::errors::{MessageError, ModmailError, ModmailResult, common}; +use crate::errors::{common, MessageError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response_ephemeral; use crate::utils::message::message_builder::MessageBuilder; @@ -14,6 +14,8 @@ use serenity::all::{ CreateCommand, CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct DeleteCommand; @@ -66,8 +68,9 @@ impl RegistrableCommand for DeleteCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/delete/text_command/delete.rs b/src/commands/delete/text_command/delete.rs index 8173d0e..cca2b42 100644 --- a/src/commands/delete/text_command/delete.rs +++ b/src/commands/delete/text_command/delete.rs @@ -3,10 +3,17 @@ use crate::commands::delete::common::{ get_thread_info, send_delete_message, update_message_numbers, }; use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use serenity::all::{Context, Message}; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn delete(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn delete( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() diff --git a/src/commands/edit/slash_command/edit.rs b/src/commands/edit/slash_command/edit.rs index fa51a00..93a5453 100644 --- a/src/commands/edit/slash_command/edit.rs +++ b/src/commands/edit/slash_command/edit.rs @@ -4,7 +4,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::{get_thread_message_by_inbox_message_id, update_message_content}; use crate::errors::common::message_not_found; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; @@ -14,6 +14,8 @@ use serenity::all::{ CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct EditCommand; @@ -82,8 +84,9 @@ impl RegistrableCommand for EditCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/edit/text_command/edit.rs b/src/commands/edit/text_command/edit.rs index 71eb917..94671c4 100644 --- a/src/commands/edit/text_command/edit.rs +++ b/src/commands/edit/text_command/edit.rs @@ -3,19 +3,26 @@ use crate::commands::edit::message_ops::{ cleanup_command_message, edit_messages, format_new_message, get_message_ids, }; use crate::commands::edit::validation::{ - EditCommandInput, parse_edit_command, validate_edit_permissions, + parse_edit_command, validate_edit_permissions, EditCommandInput, }; use crate::config::Config; use crate::db::get_thread_message_by_inbox_message_id; use crate::db::update_message_content; use crate::errors::common::message_not_found; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::collections::HashMap; - -pub async fn edit(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +use std::sync::Arc; +use tokio::sync::watch::Receiver; + +pub async fn edit( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() diff --git a/src/commands/force_close/slash_command/force_close.rs b/src/commands/force_close/slash_command/force_close.rs index 086d242..75bd5e9 100644 --- a/src/commands/force_close/slash_command/force_close.rs +++ b/src/commands/force_close/slash_command/force_close.rs @@ -4,9 +4,11 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct ForceCloseCommand; @@ -38,8 +40,9 @@ impl RegistrableCommand for ForceCloseCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/force_close/text_command/force_close.rs b/src/commands/force_close/text_command/force_close.rs index a48db1e..e92f7f7 100644 --- a/src/commands/force_close/text_command/force_close.rs +++ b/src/commands/force_close/text_command/force_close.rs @@ -3,10 +3,17 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; use serenity::all::{Context, Message}; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn force_close(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn force_close( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let db_pool = config .db_pool .as_ref() diff --git a/src/commands/help/slash_command/help.rs b/src/commands/help/slash_command/help.rs index 9fb1467..6a1f71c 100644 --- a/src/commands/help/slash_command/help.rs +++ b/src/commands/help/slash_command/help.rs @@ -5,6 +5,8 @@ use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct HelpCommand; @@ -36,8 +38,9 @@ impl RegistrableCommand for HelpCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/help/text_command/help.rs b/src/commands/help/text_command/help.rs index 086ca7a..82c4980 100644 --- a/src/commands/help/text_command/help.rs +++ b/src/commands/help/text_command/help.rs @@ -2,8 +2,15 @@ use crate::config::Config; use crate::errors::ModmailResult; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn help(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn help( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let help_message = "# Available commands:\n\n\ **!add_staff ** - Add a staff to an hidden ticket\n\ **!remove_staff ** - Remove a staff from a ticket\n\ diff --git a/src/commands/id/slash_command/id.rs b/src/commands/id/slash_command/id.rs index 906ea38..e3a3854 100644 --- a/src/commands/id/slash_command/id.rs +++ b/src/commands/id/slash_command/id.rs @@ -9,6 +9,8 @@ use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, ResolvedOption}; use serenity::builder::CreateCommand; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct IdCommand; @@ -40,8 +42,9 @@ impl RegistrableCommand for IdCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/id/text_command/id.rs b/src/commands/id/text_command/id.rs index 119f9c0..4eacf9c 100644 --- a/src/commands/id/text_command/id.rs +++ b/src/commands/id/text_command/id.rs @@ -1,13 +1,20 @@ use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::common::{database_connection_failed, thread_not_found}; +use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::{ModmailError, ModmailResult}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn id(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn id( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let db_pool = config .db_pool .as_ref() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 80b4563..a3cf5f4 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,6 +4,7 @@ use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; +use tokio::sync::watch::Receiver; pub mod add_reminder; pub mod add_staff; @@ -35,20 +36,27 @@ pub trait RegistrableCommand: Send + Sync { command: &CommandInteraction, options: &[ResolvedOption<'_>], config: &Config, + shutdown: Arc>, ) -> BoxFuture>; } pub struct CommandRegistry { commands: HashMap<&'static str, Arc>, + shutdown: Arc>, } impl CommandRegistry { - pub fn new() -> Self { + pub fn new(shutdown: Receiver) -> Self { Self { commands: HashMap::new(), + shutdown: Arc::new(shutdown), } } + pub fn shutdown(&self) -> Arc> { + self.shutdown.clone() + } + pub fn register_command(&mut self, command: C) { self.commands.insert(command.name(), Arc::new(command)); } diff --git a/src/commands/move_thread/slash_command/move_thread.rs b/src/commands/move_thread/slash_command/move_thread.rs index 9cf5a77..d42a9ca 100644 --- a/src/commands/move_thread/slash_command/move_thread.rs +++ b/src/commands/move_thread/slash_command/move_thread.rs @@ -15,6 +15,8 @@ use serenity::all::{ ResolvedOption, }; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct MoveCommand; @@ -64,8 +66,9 @@ impl RegistrableCommand for MoveCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/move_thread/text_command/move_thread.rs b/src/commands/move_thread/text_command/move_thread.rs index 0070786..9a39f8f 100644 --- a/src/commands/move_thread/text_command/move_thread.rs +++ b/src/commands/move_thread/text_command/move_thread.rs @@ -5,10 +5,17 @@ use crate::commands::move_thread::common::{ use crate::config::Config; use crate::errors::CommandError::NotInThread; use crate::errors::ThreadError::CategoryNotFound; -use crate::errors::{CommandError, DiscordError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DiscordError, ModmailError, ModmailResult}; use serenity::all::{Context, Message}; - -pub async fn move_thread(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +use std::sync::Arc; +use tokio::sync::watch::Receiver; + +pub async fn move_thread( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() diff --git a/src/commands/new_thread/slash_command/new_thread.rs b/src/commands/new_thread/slash_command/new_thread.rs index 9da4552..4c7c32d 100644 --- a/src/commands/new_thread/slash_command/new_thread.rs +++ b/src/commands/new_thread/slash_command/new_thread.rs @@ -3,7 +3,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; use crate::errors::{ - CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, common, + common, CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, }; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; @@ -13,6 +13,8 @@ use serenity::all::{ CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup, GuildId, ResolvedOption, }; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct NewThreadCommand; @@ -62,8 +64,9 @@ impl RegistrableCommand for NewThreadCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/new_thread/text_command/new_thread.rs b/src/commands/new_thread/text_command/new_thread.rs index 7849c14..d6eabd6 100644 --- a/src/commands/new_thread/text_command/new_thread.rs +++ b/src/commands/new_thread/text_command/new_thread.rs @@ -4,11 +4,18 @@ use crate::commands::new_thread::common::{ }; use crate::config::Config; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; -use crate::errors::{DiscordError, ModmailError, ModmailResult, common}; +use crate::errors::{common, DiscordError, ModmailError, ModmailResult}; use serenity::all::{ChannelId, Context, GuildId, Message}; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn new_thread(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn new_thread( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() diff --git a/src/commands/recover/slash_command/recover.rs b/src/commands/recover/slash_command/recover.rs index 95e662a..4709ddc 100644 --- a/src/commands/recover/slash_command/recover.rs +++ b/src/commands/recover/slash_command/recover.rs @@ -1,12 +1,14 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct RecoverCommand; @@ -38,8 +40,9 @@ impl RegistrableCommand for RecoverCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/recover/text_command/recover.rs b/src/commands/recover/text_command/recover.rs index 06626b8..b81beeb 100644 --- a/src/commands/recover/text_command/recover.rs +++ b/src/commands/recover/text_command/recover.rs @@ -1,12 +1,19 @@ use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn recover(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn recover( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let _ = config .db_pool .as_ref() diff --git a/src/commands/remove_reminder/slash_command/remove_reminder.rs b/src/commands/remove_reminder/slash_command/remove_reminder.rs index c63d43e..1d69c3c 100644 --- a/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -1,7 +1,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; @@ -10,6 +10,8 @@ use serenity::all::{ CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct RemoveReminderCommand; @@ -52,8 +54,9 @@ impl RegistrableCommand for RemoveReminderCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/remove_reminder/text_command/remove_reminder.rs b/src/commands/remove_reminder/text_command/remove_reminder.rs index 58b969e..0dea3b5 100644 --- a/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -1,12 +1,19 @@ use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn remove_reminder(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn remove_reminder( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() diff --git a/src/commands/remove_staff/slash_command/remove_staff.rs b/src/commands/remove_staff/slash_command/remove_staff.rs index f387a75..a098397 100644 --- a/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/src/commands/remove_staff/slash_command/remove_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; @@ -13,6 +13,8 @@ use serenity::all::{ CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup, ResolvedOption, }; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct RemoveStaffCommand; @@ -62,8 +64,9 @@ impl RegistrableCommand for RemoveStaffCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/remove_staff/text_command/remove_staff.rs b/src/commands/remove_staff/text_command/remove_staff.rs index c7e0329..0038ba7 100644 --- a/src/commands/remove_staff/text_command/remove_staff.rs +++ b/src/commands/remove_staff/text_command/remove_staff.rs @@ -3,12 +3,19 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn remove_staff(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn remove_staff( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, +) -> ModmailResult<()> { let pool = config .db_pool .as_ref() diff --git a/src/commands/reply/slash_command/reply.rs b/src/commands/reply/slash_command/reply.rs index af9659c..98d3035 100644 --- a/src/commands/reply/slash_command/reply.rs +++ b/src/commands/reply/slash_command/reply.rs @@ -2,17 +2,19 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{ Attachment, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, GuildId, ResolvedOption, UserId, }; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct ReplyCommand; @@ -95,8 +97,9 @@ impl RegistrableCommand for ReplyCommand { &self, ctx: &Context, command: &CommandInteraction, - options: &[ResolvedOption<'_>], + _options: &[ResolvedOption<'_>], config: &Config, + _shutdown: Arc>, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/src/commands/reply/text_command/reply.rs b/src/commands/reply/text_command/reply.rs index ab377b7..9e63b5e 100644 --- a/src/commands/reply/text_command/reply.rs +++ b/src/commands/reply/text_command/reply.rs @@ -1,15 +1,22 @@ use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn reply(ctx: &Context, msg: &Message, config: &Config) -> ModmailResult<()> { +pub async fn reply( + ctx: &Context, + msg: &Message, + config: &Config, + shutdown: Arc>, +) -> ModmailResult<()> { let db_pool = config .db_pool .as_ref() diff --git a/src/config.rs b/src/config.rs index e977ff2..4a532da 100644 --- a/src/config.rs +++ b/src/config.rs @@ -112,9 +112,11 @@ pub struct ErrorHandlingConfig { pub error_message_ttl: Option, } -pub fn load_config(path: &str) -> Config { - let content = fs::read_to_string(path) - .expect("No configuration file found ! Add 'config.toml' file at the root!"); +pub fn load_config(path: &str) -> Option { + let content = match fs::read_to_string(path) { + Ok(c) => c, + Err(_) => return None, + }; let mut config: Config = toml::from_str(&content).expect("The format of the config.toml is not correct!"); @@ -155,7 +157,7 @@ pub fn load_config(path: &str) -> Config { config.thread_locks = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())); - config + Some(config) } impl Default for LanguageConfig { diff --git a/src/handlers/guild_interaction_handler.rs b/src/handlers/guild_interaction_handler.rs index 8eae40c..1dada24 100644 --- a/src/handlers/guild_interaction_handler.rs +++ b/src/handlers/guild_interaction_handler.rs @@ -6,18 +6,21 @@ use crate::modules::threads::{ }; use serenity::all::{Context, EventHandler, Interaction}; use std::sync::Arc; +use tokio::sync::watch::Receiver; #[derive(Clone)] pub struct InteractionHandler { pub config: Config, pub registry: Arc, + pub shutdown: Arc>, } impl InteractionHandler { - pub fn new(config: &Config, register: Arc) -> Self { + pub fn new(config: &Config, register: Arc, shutdown: Receiver) -> Self { Self { config: config.clone(), registry: register, + shutdown: Arc::new(shutdown), } } } @@ -52,7 +55,9 @@ impl EventHandler for InteractionHandler { let config = self.config.clone(); if let Some(handler) = self.registry.get(command.data.name.as_str()) { - let result = handler.run(&ctx, &command, &options, &config).await; + let result = handler + .run(&ctx, &command, &options, &config, self.shutdown.clone()) + .await; if let Err(e) = result { if let Some(error_handler) = &self.config.error_handler { diff --git a/src/handlers/guild_messages_handler.rs b/src/handlers/guild_messages_handler.rs index 57b7a68..5da1d7b 100644 --- a/src/handlers/guild_messages_handler.rs +++ b/src/handlers/guild_messages_handler.rs @@ -23,7 +23,7 @@ use crate::db::operations::{ }; use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::get_thread_lock::get_thread_lock; @@ -37,12 +37,18 @@ use serenity::{ use std::collections::HashSet; use std::sync::{LazyLock, Mutex}; use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; +use tokio::sync::watch::Receiver; static SUPPRESSED_DELETES: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); type CommandFunc = Arc; -type StaticCommandFunc = dyn Fn(Context, Message, Config) -> Pin> + Send>> +type StaticCommandFunc = dyn Fn( + Context, + Message, + Config, + Receiver, + ) -> Pin> + Send>> + Send + Sync + 'static; @@ -50,13 +56,15 @@ type StaticCommandFunc = dyn Fn(Context, Message, Config) -> Pin, + pub shutdown: Receiver, } impl GuildMessagesHandler { - pub fn new(config: &Config) -> Self { + pub fn new(config: &Config, shutdown: Receiver) -> Self { let mut h = Self { config: config.clone(), commands: HashMap::new(), + shutdown, }; wrap_command!(h.commands, ["reply", "r"], reply); wrap_command!(h.commands, ["edit", "e"], edit); @@ -177,8 +185,13 @@ impl EventHandler for GuildMessagesHandler { } if let Some(command_func) = self.commands.get(command_name) - && let Err(error) = - command_func(ctx.clone(), msg.clone(), self.config.clone()).await + && let Err(error) = command_func( + ctx.clone(), + msg.clone(), + self.config.clone(), + self.shutdown.clone(), + ) + .await { if let Some(error_handler) = &self.config.error_handler { let _ = error_handler diff --git a/src/handlers/ready_handler.rs b/src/handlers/ready_handler.rs index 4fd70f5..4f989e0 100644 --- a/src/handlers/ready_handler.rs +++ b/src/handlers/ready_handler.rs @@ -1,4 +1,4 @@ -use crate::commands::{CommandRegistry, RegistrableCommand}; +use crate::commands::CommandRegistry; use crate::config::Config; use crate::features::sync_features; use crate::modules::message_recovery::{recover_missing_messages, send_recovery_summary}; @@ -10,18 +10,25 @@ use serenity::{ async_trait, }; use std::sync::Arc; +use tokio::sync::watch::Receiver; #[derive(Clone)] pub struct ReadyHandler { pub config: Config, pub registry: Arc, + pub shutdown: Arc>, } impl ReadyHandler { - pub fn new(config: &Config, registry: Arc) -> Self { + pub fn new( + config: &Config, + registry: Arc, + shutdown: Arc>, + ) -> Self { Self { config: config.clone(), registry, + shutdown, } } } @@ -51,7 +58,7 @@ impl EventHandler for ReadyHandler { } }); - load_reminders(&ctx, &self.config, pool).await; + load_reminders(&ctx, &self.config, pool, self.shutdown.clone()).await; let guild_id = GuildId::new(self.config.bot.get_staff_guild_id()); diff --git a/src/main.rs b/src/main.rs index 0b74f7e..7a2bd74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,35 +1,12 @@ -use crate::commands::CommandRegistry; -use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; -use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; -use crate::commands::alert::slash_command::alert::AlertCommand; -use crate::commands::close::slash_command::close::CloseCommand; -use crate::commands::delete::slash_command::delete::DeleteCommand; -use crate::commands::edit::slash_command::edit::EditCommand; -use crate::commands::force_close::slash_command::force_close::ForceCloseCommand; -use crate::commands::help::slash_command::help::HelpCommand; -use crate::commands::id::slash_command::id::IdCommand; -use crate::commands::move_thread::slash_command::move_thread::MoveCommand; -use crate::commands::new_thread::slash_command::new_thread::NewThreadCommand; -use crate::commands::recover::slash_command::recover::RecoverCommand; -use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; -use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; -use crate::commands::reply::slash_command::reply::ReplyCommand; -use crate::handlers::guild_handler::GuildHandler; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::handlers::guild_message_reactions_handler::GuildMessageReactionsHandler; -use crate::handlers::guild_moderation_handler::GuildModerationHandler; -use crate::handlers::typing_proxy_handler::TypingProxyHandler; -use config::load_config; -use handlers::{ - guild_members_handler::GuildMembersHandler, guild_messages_handler::GuildMessagesHandler, - ready_handler::ReadyHandler, -}; -use serenity::all::{ClientBuilder, GatewayIntents}; -use serenity::cache::Settings as CacheSettings; -use std::process; -use std::sync::Arc; -use std::time::Duration; - +use crate::api::router::create_api_router; +use crate::bot::init_bot_state; +use crate::config::Config; +use tokio::signal; +use tokio::sync::watch::Sender; +use tokio::task::JoinHandle; + +mod api; +mod bot; mod commands; mod config; mod db; @@ -42,87 +19,37 @@ mod utils; pub struct Database; -#[tokio::main] -async fn main() { - let pool = db::operations::init_database() - .await - .expect("An error occured!"); - println!("Database connected!"); - - let mut config = load_config("config.toml"); - config.db_pool = Some(pool.clone()); - - let intents = GatewayIntents::GUILDS - | GatewayIntents::GUILD_MESSAGES - | GatewayIntents::MESSAGE_CONTENT - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::GUILD_MESSAGE_TYPING - | GatewayIntents::DIRECT_MESSAGE_TYPING - | GatewayIntents::GUILD_MEMBERS - | GatewayIntents::GUILD_PRESENCES - | GatewayIntents::GUILD_MESSAGE_REACTIONS - | GatewayIntents::DIRECT_MESSAGE_REACTIONS - | GatewayIntents::GUILD_MODERATION; +enum BotStatus { + Stopped, + Running { + handle: JoinHandle<()>, + shutdown: Sender, + }, +} - let mut cache_settings = CacheSettings::default(); - cache_settings.max_messages = 10_000; - cache_settings.time_to_live = Duration::from_secs(6 * 60 * 60); +struct BotState { + config: Option, + status: BotStatus, + db_pool: Option, +} - let mut registry = CommandRegistry::new(); - registry.register_command(AddStaffCommand); - registry.register_command(AlertCommand); - registry.register_command(CloseCommand); - registry.register_command(DeleteCommand); - registry.register_command(EditCommand); - registry.register_command(ForceCloseCommand); - registry.register_command(HelpCommand); - registry.register_command(IdCommand); - registry.register_command(MoveCommand); - registry.register_command(NewThreadCommand); - registry.register_command(RecoverCommand); - registry.register_command(RemoveStaffCommand); - registry.register_command(ReplyCommand); - registry.register_command(AddReminderCommand); - registry.register_command(RemoveReminderCommand); +#[tokio::main] +async fn main() { + let bot_state = init_bot_state().await; - let registry = Arc::new(registry); + let app = create_api_router(bot_state); - let mut client: serenity::Client = ClientBuilder::new(config.bot.token.clone(), intents) - .cache_settings(cache_settings) - .event_handler(ReadyHandler::new(&config, registry.clone())) - .event_handler(GuildMessagesHandler::new(&config)) - .event_handler(TypingProxyHandler::new(&config)) - .event_handler(GuildMembersHandler::new(&config)) - .event_handler(GuildMessageReactionsHandler::new(&config)) - .event_handler(GuildModerationHandler::new(&config)) - .event_handler(InteractionHandler::new(&config, registry.clone())) - .event_handler(GuildHandler::new(&config)) + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + println!("listening on {}", listener.local_addr().unwrap()); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) .await - .expect("Failed to create client."); - - if let Err(e) = config.validate_servers(&client.http).await { - eprintln!("Configuration validation error: {}", e); - eprintln!( - "Check that the server IDs are correct and that the bot has access to the servers." - ); - process::exit(1); - } - - println!("Configuration successfully validated!!"); - if config.bot.is_dual_mode() { - println!( - "Mode: Dual server (Community: {}, Staff: {})", - config.bot.get_community_guild_id(), - config.bot.get_staff_guild_id() - ); - } else { - println!( - "Mode: Mono server (ID: {})", - config.bot.get_community_guild_id() - ); - } + .unwrap(); +} - if let Err(e) = client.start().await { - println!("Failed to initialize client: {e}"); - } +async fn shutdown_signal() { + signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + println!("Shutdown signal received"); } diff --git a/src/modules/reminders.rs b/src/modules/reminders.rs index a887dbb..ad9ed6d 100644 --- a/src/modules/reminders.rs +++ b/src/modules/reminders.rs @@ -2,15 +2,22 @@ use crate::commands::add_reminder::common::spawn_reminder; use crate::config::Config; use crate::db::reminders::get_all_pending_reminders; use serenity::all::Context; +use std::sync::Arc; +use tokio::sync::watch::Receiver; -pub async fn load_reminders(ctx: &Context, config: &Config, pool: &sqlx::SqlitePool) { +pub async fn load_reminders( + ctx: &Context, + config: &Config, + pool: &sqlx::SqlitePool, + shutdown: Arc>, +) { let reminders = get_all_pending_reminders(pool).await.unwrap_or_else(|e| { eprintln!("Failed to fetch pending reminders: {:?}", e); Vec::new() }); for reminder in reminders { - spawn_reminder(&reminder, None, &ctx, &config, &pool); + spawn_reminder(&reminder, None, &ctx, &config, &pool, shutdown.clone()); } println!("All pending reminders have been scheduled."); } diff --git a/src/utils/command/wrap_command.rs b/src/utils/command/wrap_command.rs index 852674c..c44bc18 100644 --- a/src/utils/command/wrap_command.rs +++ b/src/utils/command/wrap_command.rs @@ -1,10 +1,10 @@ #[macro_export] macro_rules! wrap_command { ($map:expr, [$($name:expr),+], $func:expr) => {{ - let command: std::sync::Arc std::pin::Pin> + Send>> + Send + Sync + 'static> = - std::sync::Arc::new(|ctx, msg, config| { + let command: std::sync::Arc) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = + std::sync::Arc::new(|ctx, msg, config, shutdown| { Box::pin(async move { - $func(&ctx, &msg, &config).await + $func(&ctx, &msg, &config, Arc::new(shutdown)).await }) }); $( @@ -12,10 +12,10 @@ macro_rules! wrap_command { )+ }}; ($map:expr, $name:expr, $func:expr) => {{ - let command: std::sync::Arc std::pin::Pin> + Send>> + Send + Sync + 'static> = - std::sync::Arc::new(|ctx, msg, config| { + let command: std::sync::Arc) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = + std::sync::Arc::new(|ctx, msg, config, shutdown| { Box::pin(async move { - $func(&ctx, &msg, &config).await + $func(&ctx, &msg, &config, Arc::new(shutdown)).await }) }); $map.insert($name.to_string(), command); From 964a1f476007cf47b8ab1ce6329792abd2199159 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 12 Oct 2025 20:16:30 +0200 Subject: [PATCH 002/100] refactor(api): update base api port --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 7a2bd74..f983676 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ async fn main() { let app = create_api_router(bot_state); - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3002").await.unwrap(); println!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) From 3e34deb9e5de0bc4dc116002f886b15cc981bdf5 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 13 Oct 2025 10:33:09 +0200 Subject: [PATCH 003/100] refactor(start): start the bot if the config file is correct and add base for panel --- .github/workflows/ci.yml | 20 +- .gitignore | 5 +- Cargo.lock | 456 +++++++++++++++++- Cargo.toml | 25 +- package-lock.json | 430 ----------------- package.json | 8 - prisma/schema.prisma | 53 -- rustmail/Cargo.toml | 24 + {src => rustmail/src}/api/handler/bot/mod.rs | 0 .../src}/api/handler/bot/start.rs | 9 +- {src => rustmail/src}/api/handler/bot/stop.rs | 7 +- {src => rustmail/src}/api/handler/mod.rs | 0 {src => rustmail/src}/api/mod.rs | 0 {src => rustmail/src}/api/router.rs | 7 +- {src => rustmail/src}/api/routes/bot.rs | 7 +- {src => rustmail/src}/api/routes/mod.rs | 0 {src => rustmail/src}/bot.rs | 52 +- .../src}/commands/add_reminder/common.rs | 2 +- .../src}/commands/add_reminder/mod.rs | 0 .../slash_command/add_reminder.rs | 4 +- .../add_reminder/slash_command/mod.rs | 0 .../add_reminder/text_command/add_reminder.rs | 4 +- .../commands/add_reminder/text_command/mod.rs | 0 .../src}/commands/add_staff/common.rs | 0 .../src}/commands/add_staff/mod.rs | 0 .../add_staff/slash_command/add_staff.rs | 2 +- .../commands/add_staff/slash_command/mod.rs | 0 .../add_staff/text_command/add_staff.rs | 2 +- .../commands/add_staff/text_command/mod.rs | 0 .../src}/commands/alert/common.rs | 0 {src => rustmail/src}/commands/alert/mod.rs | 0 .../commands/alert/slash_command/alert.rs | 2 +- .../src}/commands/alert/slash_command/mod.rs | 0 .../src}/commands/alert/text_command/alert.rs | 2 +- .../src}/commands/alert/text_command/mod.rs | 0 .../src}/commands/anonreply/mod.rs | 0 .../anonreply/text_command/anonreply.rs | 4 +- .../commands/anonreply/text_command/mod.rs | 0 .../src}/commands/close/common.rs | 0 {src => rustmail/src}/commands/close/mod.rs | 0 .../commands/close/slash_command/close.rs | 2 +- .../src}/commands/close/slash_command/mod.rs | 0 .../src}/commands/close/text_command/close.rs | 2 +- .../src}/commands/close/text_command/mod.rs | 0 .../src}/commands/delete/common.rs | 0 {src => rustmail/src}/commands/delete/mod.rs | 0 .../commands/delete/slash_command/delete.rs | 2 +- .../src}/commands/delete/slash_command/mod.rs | 0 .../commands/delete/text_command/delete.rs | 2 +- .../src}/commands/delete/text_command/mod.rs | 0 {src => rustmail/src}/commands/edit/common.rs | 1 + .../src}/commands/edit/message_ops.rs | 0 {src => rustmail/src}/commands/edit/mod.rs | 0 .../src}/commands/edit/slash_command/edit.rs | 2 +- .../src}/commands/edit/slash_command/mod.rs | 0 .../src}/commands/edit/text_command/edit.rs | 4 +- .../src}/commands/edit/text_command/mod.rs | 0 .../src}/commands/edit/validation.rs | 0 .../src}/commands/force_close/common.rs | 0 .../src}/commands/force_close/mod.rs | 0 .../force_close/slash_command/force_close.rs | 2 +- .../commands/force_close/slash_command/mod.rs | 0 .../force_close/text_command/force_close.rs | 2 +- .../commands/force_close/text_command/mod.rs | 0 {src => rustmail/src}/commands/help/mod.rs | 0 .../src}/commands/help/slash_command/help.rs | 0 .../src}/commands/help/slash_command/mod.rs | 0 .../src}/commands/help/text_command/help.rs | 0 .../src}/commands/help/text_command/mod.rs | 0 {src => rustmail/src}/commands/id/mod.rs | 0 .../src}/commands/id/slash_command/id.rs | 0 .../src}/commands/id/slash_command/mod.rs | 0 .../src}/commands/id/text_command/id.rs | 2 +- .../src}/commands/id/text_command/mod.rs | 0 {src => rustmail/src}/commands/mod.rs | 0 .../src}/commands/move_thread/common.rs | 0 .../src}/commands/move_thread/mod.rs | 0 .../commands/move_thread/slash_command/mod.rs | 0 .../move_thread/slash_command/move_thread.rs | 0 .../commands/move_thread/text_command/mod.rs | 0 .../move_thread/text_command/move_thread.rs | 2 +- .../src}/commands/new_thread/common.rs | 0 .../src}/commands/new_thread/mod.rs | 0 .../commands/new_thread/slash_command/mod.rs | 0 .../new_thread/slash_command/new_thread.rs | 2 +- .../commands/new_thread/text_command/mod.rs | 0 .../new_thread/text_command/new_thread.rs | 2 +- {src => rustmail/src}/commands/recover/mod.rs | 0 .../commands/recover/slash_command/mod.rs | 0 .../commands/recover/slash_command/recover.rs | 2 +- .../src}/commands/recover/text_command/mod.rs | 0 .../commands/recover/text_command/recover.rs | 2 +- .../src}/commands/remove_reminder/mod.rs | 0 .../remove_reminder/slash_command/mod.rs | 0 .../slash_command/remove_reminder.rs | 2 +- .../remove_reminder/text_command/mod.rs | 0 .../text_command/remove_reminder.rs | 2 +- .../src}/commands/remove_staff/common.rs | 0 .../src}/commands/remove_staff/mod.rs | 0 .../remove_staff/slash_command/mod.rs | 0 .../slash_command/remove_staff.rs | 2 +- .../commands/remove_staff/text_command/mod.rs | 0 .../remove_staff/text_command/remove_staff.rs | 2 +- {src => rustmail/src}/commands/reply/mod.rs | 0 .../src}/commands/reply/slash_command/mod.rs | 0 .../commands/reply/slash_command/reply.rs | 4 +- .../src}/commands/reply/text_command/mod.rs | 0 .../src}/commands/reply/text_command/reply.rs | 4 +- {src => rustmail/src}/config.rs | 74 ++- {src => rustmail/src}/db/mod.rs | 0 .../src}/db/operations/features.rs | 0 {src => rustmail/src}/db/operations/init.rs | 8 +- .../src}/db/operations/messages.rs | 0 {src => rustmail/src}/db/operations/mod.rs | 0 .../src}/db/operations/reminders.rs | 0 .../src}/db/operations/scheduled.rs | 0 .../src}/db/operations/threads.rs | 0 {src => rustmail/src}/db/repr.rs | 0 {src => rustmail/src}/errors/dictionary.rs | 0 {src => rustmail/src}/errors/handler.rs | 0 {src => rustmail/src}/errors/mod.rs | 0 {src => rustmail/src}/errors/types.rs | 4 +- {src => rustmail/src}/features/mod.rs | 0 {src => rustmail/src}/features/poll.rs | 0 .../src}/handlers/guild_handler.rs | 0 .../handlers/guild_interaction_handler.rs | 0 .../src}/handlers/guild_members_handler.rs | 0 .../guild_message_reactions_handler.rs | 0 .../src}/handlers/guild_messages_handler.rs | 2 +- .../src}/handlers/guild_moderation_handler.rs | 0 {src => rustmail/src}/handlers/mod.rs | 0 .../src}/handlers/ready_handler.rs | 0 .../src}/handlers/typing_proxy_handler.rs | 0 {src => rustmail/src}/i18n/language/cn.rs | 0 {src => rustmail/src}/i18n/language/dt.rs | 0 {src => rustmail/src}/i18n/language/en.rs | 27 +- {src => rustmail/src}/i18n/language/fr.rs | 36 +- {src => rustmail/src}/i18n/language/gr.rs | 0 {src => rustmail/src}/i18n/language/it.rs | 0 {src => rustmail/src}/i18n/language/jp.rs | 0 {src => rustmail/src}/i18n/language/kr.rs | 0 {src => rustmail/src}/i18n/language/mod.rs | 0 {src => rustmail/src}/i18n/language/pr.rs | 0 {src => rustmail/src}/i18n/language/ru.rs | 0 {src => rustmail/src}/i18n/language/sp.rs | 0 {src => rustmail/src}/i18n/languages.rs | 0 {src => rustmail/src}/i18n/mod.rs | 0 {src => rustmail/src}/i18n/utils.rs | 0 rustmail/src/main.rs | 136 ++++++ .../src}/modules/message_recovery.rs | 0 {src => rustmail/src}/modules/mod.rs | 0 {src => rustmail/src}/modules/reminders.rs | 0 .../src}/modules/scheduled_closures.rs | 0 {src => rustmail/src}/modules/threads.rs | 5 +- .../src}/utils/command/defer_response.rs | 0 .../utils/command/extract_reply_content.rs | 0 {src => rustmail/src}/utils/command/mod.rs | 0 .../src}/utils/command/wrap_command.rs | 0 .../utils/conversion/hex_string_to_int.rs | 0 {src => rustmail/src}/utils/conversion/mod.rs | 0 .../src}/utils/message/message_builder.rs | 0 {src => rustmail/src}/utils/message/mod.rs | 0 .../src}/utils/message/reply_intent.rs | 0 .../src}/utils/message/ui_components.rs | 0 {src => rustmail/src}/utils/mod.rs | 0 .../src}/utils/thread/fetch_thread.rs | 0 .../src}/utils/thread/get_thread_lock.rs | 0 {src => rustmail/src}/utils/thread/mod.rs | 0 .../src}/utils/thread/send_to_thread.rs | 0 .../src}/utils/time/format_duration_since.rs | 0 .../src}/utils/time/get_member_join_date.rs | 0 {src => rustmail/src}/utils/time/mod.rs | 0 rustmail_panel/Cargo.toml | 8 + rustmail_panel/Trunk.toml | 3 + rustmail_panel/index.html | 5 + rustmail_panel/src/main.rs | 24 + src/main.rs | 55 --- 177 files changed, 873 insertions(+), 686 deletions(-) delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 prisma/schema.prisma create mode 100644 rustmail/Cargo.toml rename {src => rustmail/src}/api/handler/bot/mod.rs (100%) rename {src => rustmail/src}/api/handler/bot/start.rs (88%) rename {src => rustmail/src}/api/handler/bot/stop.rs (88%) rename {src => rustmail/src}/api/handler/mod.rs (100%) rename {src => rustmail/src}/api/mod.rs (100%) rename {src => rustmail/src}/api/router.rs (77%) rename {src => rustmail/src}/api/routes/bot.rs (89%) rename {src => rustmail/src}/api/routes/mod.rs (100%) rename {src => rustmail/src}/bot.rs (80%) rename {src => rustmail/src}/commands/add_reminder/common.rs (98%) rename {src => rustmail/src}/commands/add_reminder/mod.rs (100%) rename {src => rustmail/src}/commands/add_reminder/slash_command/add_reminder.rs (98%) rename {src => rustmail/src}/commands/add_reminder/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/add_reminder/text_command/add_reminder.rs (95%) rename {src => rustmail/src}/commands/add_reminder/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/add_staff/common.rs (100%) rename {src => rustmail/src}/commands/add_staff/mod.rs (100%) rename {src => rustmail/src}/commands/add_staff/slash_command/add_staff.rs (98%) rename {src => rustmail/src}/commands/add_staff/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/add_staff/text_command/add_staff.rs (96%) rename {src => rustmail/src}/commands/add_staff/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/alert/common.rs (100%) rename {src => rustmail/src}/commands/alert/mod.rs (100%) rename {src => rustmail/src}/commands/alert/slash_command/alert.rs (97%) rename {src => rustmail/src}/commands/alert/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/alert/text_command/alert.rs (95%) rename {src => rustmail/src}/commands/alert/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/anonreply/mod.rs (100%) rename {src => rustmail/src}/commands/anonreply/text_command/anonreply.rs (97%) rename {src => rustmail/src}/commands/anonreply/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/close/common.rs (100%) rename {src => rustmail/src}/commands/close/mod.rs (100%) rename {src => rustmail/src}/commands/close/slash_command/close.rs (99%) rename {src => rustmail/src}/commands/close/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/close/text_command/close.rs (99%) rename {src => rustmail/src}/commands/close/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/delete/common.rs (100%) rename {src => rustmail/src}/commands/delete/mod.rs (100%) rename {src => rustmail/src}/commands/delete/slash_command/delete.rs (98%) rename {src => rustmail/src}/commands/delete/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/delete/text_command/delete.rs (96%) rename {src => rustmail/src}/commands/delete/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/edit/common.rs (98%) rename {src => rustmail/src}/commands/edit/message_ops.rs (100%) rename {src => rustmail/src}/commands/edit/mod.rs (100%) rename {src => rustmail/src}/commands/edit/slash_command/edit.rs (99%) rename {src => rustmail/src}/commands/edit/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/edit/text_command/edit.rs (97%) rename {src => rustmail/src}/commands/edit/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/edit/validation.rs (100%) rename {src => rustmail/src}/commands/force_close/common.rs (100%) rename {src => rustmail/src}/commands/force_close/mod.rs (100%) rename {src => rustmail/src}/commands/force_close/slash_command/force_close.rs (98%) rename {src => rustmail/src}/commands/force_close/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/force_close/text_command/force_close.rs (96%) rename {src => rustmail/src}/commands/force_close/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/help/mod.rs (100%) rename {src => rustmail/src}/commands/help/slash_command/help.rs (100%) rename {src => rustmail/src}/commands/help/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/help/text_command/help.rs (100%) rename {src => rustmail/src}/commands/help/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/id/mod.rs (100%) rename {src => rustmail/src}/commands/id/slash_command/id.rs (100%) rename {src => rustmail/src}/commands/id/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/id/text_command/id.rs (100%) rename {src => rustmail/src}/commands/id/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/mod.rs (100%) rename {src => rustmail/src}/commands/move_thread/common.rs (100%) rename {src => rustmail/src}/commands/move_thread/mod.rs (100%) rename {src => rustmail/src}/commands/move_thread/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/move_thread/slash_command/move_thread.rs (100%) rename {src => rustmail/src}/commands/move_thread/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/move_thread/text_command/move_thread.rs (95%) rename {src => rustmail/src}/commands/new_thread/common.rs (100%) rename {src => rustmail/src}/commands/new_thread/mod.rs (100%) rename {src => rustmail/src}/commands/new_thread/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/new_thread/slash_command/new_thread.rs (98%) rename {src => rustmail/src}/commands/new_thread/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/new_thread/text_command/new_thread.rs (98%) rename {src => rustmail/src}/commands/recover/mod.rs (100%) rename {src => rustmail/src}/commands/recover/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/recover/slash_command/recover.rs (98%) rename {src => rustmail/src}/commands/recover/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/recover/text_command/recover.rs (98%) rename {src => rustmail/src}/commands/remove_reminder/mod.rs (100%) rename {src => rustmail/src}/commands/remove_reminder/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/remove_reminder/slash_command/remove_reminder.rs (98%) rename {src => rustmail/src}/commands/remove_reminder/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/remove_reminder/text_command/remove_reminder.rs (96%) rename {src => rustmail/src}/commands/remove_staff/common.rs (100%) rename {src => rustmail/src}/commands/remove_staff/mod.rs (100%) rename {src => rustmail/src}/commands/remove_staff/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/remove_staff/slash_command/remove_staff.rs (98%) rename {src => rustmail/src}/commands/remove_staff/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/remove_staff/text_command/remove_staff.rs (96%) rename {src => rustmail/src}/commands/reply/mod.rs (100%) rename {src => rustmail/src}/commands/reply/slash_command/mod.rs (100%) rename {src => rustmail/src}/commands/reply/slash_command/reply.rs (98%) rename {src => rustmail/src}/commands/reply/text_command/mod.rs (100%) rename {src => rustmail/src}/commands/reply/text_command/reply.rs (95%) rename {src => rustmail/src}/config.rs (84%) rename {src => rustmail/src}/db/mod.rs (100%) rename {src => rustmail/src}/db/operations/features.rs (100%) rename {src => rustmail/src}/db/operations/init.rs (89%) rename {src => rustmail/src}/db/operations/messages.rs (100%) rename {src => rustmail/src}/db/operations/mod.rs (100%) rename {src => rustmail/src}/db/operations/reminders.rs (100%) rename {src => rustmail/src}/db/operations/scheduled.rs (100%) rename {src => rustmail/src}/db/operations/threads.rs (100%) rename {src => rustmail/src}/db/repr.rs (100%) rename {src => rustmail/src}/errors/dictionary.rs (100%) rename {src => rustmail/src}/errors/handler.rs (100%) rename {src => rustmail/src}/errors/mod.rs (100%) rename {src => rustmail/src}/errors/types.rs (99%) rename {src => rustmail/src}/features/mod.rs (100%) rename {src => rustmail/src}/features/poll.rs (100%) rename {src => rustmail/src}/handlers/guild_handler.rs (100%) rename {src => rustmail/src}/handlers/guild_interaction_handler.rs (100%) rename {src => rustmail/src}/handlers/guild_members_handler.rs (100%) rename {src => rustmail/src}/handlers/guild_message_reactions_handler.rs (100%) rename {src => rustmail/src}/handlers/guild_messages_handler.rs (99%) rename {src => rustmail/src}/handlers/guild_moderation_handler.rs (100%) rename {src => rustmail/src}/handlers/mod.rs (100%) rename {src => rustmail/src}/handlers/ready_handler.rs (100%) rename {src => rustmail/src}/handlers/typing_proxy_handler.rs (100%) rename {src => rustmail/src}/i18n/language/cn.rs (100%) rename {src => rustmail/src}/i18n/language/dt.rs (100%) rename {src => rustmail/src}/i18n/language/en.rs (97%) rename {src => rustmail/src}/i18n/language/fr.rs (96%) rename {src => rustmail/src}/i18n/language/gr.rs (100%) rename {src => rustmail/src}/i18n/language/it.rs (100%) rename {src => rustmail/src}/i18n/language/jp.rs (100%) rename {src => rustmail/src}/i18n/language/kr.rs (100%) rename {src => rustmail/src}/i18n/language/mod.rs (100%) rename {src => rustmail/src}/i18n/language/pr.rs (100%) rename {src => rustmail/src}/i18n/language/ru.rs (100%) rename {src => rustmail/src}/i18n/language/sp.rs (100%) rename {src => rustmail/src}/i18n/languages.rs (100%) rename {src => rustmail/src}/i18n/mod.rs (100%) rename {src => rustmail/src}/i18n/utils.rs (100%) create mode 100644 rustmail/src/main.rs rename {src => rustmail/src}/modules/message_recovery.rs (100%) rename {src => rustmail/src}/modules/mod.rs (100%) rename {src => rustmail/src}/modules/reminders.rs (100%) rename {src => rustmail/src}/modules/scheduled_closures.rs (100%) rename {src => rustmail/src}/modules/threads.rs (99%) rename {src => rustmail/src}/utils/command/defer_response.rs (100%) rename {src => rustmail/src}/utils/command/extract_reply_content.rs (100%) rename {src => rustmail/src}/utils/command/mod.rs (100%) rename {src => rustmail/src}/utils/command/wrap_command.rs (100%) rename {src => rustmail/src}/utils/conversion/hex_string_to_int.rs (100%) rename {src => rustmail/src}/utils/conversion/mod.rs (100%) rename {src => rustmail/src}/utils/message/message_builder.rs (100%) rename {src => rustmail/src}/utils/message/mod.rs (100%) rename {src => rustmail/src}/utils/message/reply_intent.rs (100%) rename {src => rustmail/src}/utils/message/ui_components.rs (100%) rename {src => rustmail/src}/utils/mod.rs (100%) rename {src => rustmail/src}/utils/thread/fetch_thread.rs (100%) rename {src => rustmail/src}/utils/thread/get_thread_lock.rs (100%) rename {src => rustmail/src}/utils/thread/mod.rs (100%) rename {src => rustmail/src}/utils/thread/send_to_thread.rs (100%) rename {src => rustmail/src}/utils/time/format_duration_since.rs (100%) rename {src => rustmail/src}/utils/time/get_member_join_date.rs (100%) rename {src => rustmail/src}/utils/time/mod.rs (100%) create mode 100644 rustmail_panel/Cargo.toml create mode 100644 rustmail_panel/Trunk.toml create mode 100644 rustmail_panel/index.html create mode 100644 rustmail_panel/src/main.rs delete mode 100644 src/main.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7a16ca..1664f37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,21 @@ jobs: - nightly steps: - uses: actions/checkout@v4 - - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} - - run: cargo build --verbose - - run: cargo test --verbose + - name: Set up Rust + run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} + + - name: Install trunk and wasm target + run: | + rustup target add wasm32-unknown-unknown + cargo install trunk --locked + + - name: Build frontend + working-directory: rustmail_panel + run: trunk build --release --dist ../rustmail/static --config Trunk.toml + + - name: Build Rust backend + run: cargo build --verbose -p rustmail + + - name: Run tests + run: cargo test --verbose -p rustmail \ No newline at end of file diff --git a/.gitignore b/.gitignore index bd6c574..fe9956e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ /target config.toml -/src/generated/prisma node_modules .env db/db.sqlite .idea .vscode/ package-lock.json -bin \ No newline at end of file +bin +rustmail/static +rustmail_panel/dist \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 31bdf75..7072b2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,6 +167,15 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -295,6 +304,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -707,8 +726,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -735,6 +756,187 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4748e10122b01435750ff530095b1217cf6546173459448b83913ebe7815df" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c26fb45f7c385ba980f5fa87ac677e363949e065a083722697ef1b2cc91e41" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" +dependencies = [ + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" +dependencies = [ + "getrandom 0.2.16", + "gloo-events", + "gloo-utils", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56008b6744713a8e8d98ac3dcb7d06543d5662358c9c805b4ce2167ad4649833" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" +dependencies = [ + "gloo-utils", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode", + "futures", + "gloo-utils", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "h2" version = "0.3.26" @@ -805,6 +1007,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1156,6 +1364,26 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "implicit-clone" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e4c5423e5b38b6f175b42561a08f6ddbc8ed3a3275f48eb350ba2dfe9a6b60" +dependencies = [ + "implicit-clone-derive", + "indexmap", +] + +[[package]] +name = "implicit-clone-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" +dependencies = [ + "quote", + "syn 2.0.101", +] + [[package]] name = "indexmap" version = "2.11.4" @@ -1418,6 +1646,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.36.7" @@ -1521,6 +1759,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1533,6 +1791,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinned" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b" +dependencies = [ + "futures", + "rustversion", + "thiserror 1.0.69", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -1584,6 +1853,50 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +dependencies = [ + "proc-macro2", + "syn 2.0.101", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1805,6 +2118,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-embed" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.101", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1915,14 +2262,16 @@ dependencies = [ [[package]] name = "rustmail" -version = "0.1.0" +version = "0.1.1" dependencies = [ "async-trait", "axum", "chrono", "hyper 1.7.0", + "mime_guess", "regex", "reqwest 0.12.23", + "rust-embed", "serde", "serenity", "sqlx", @@ -1931,6 +2280,13 @@ dependencies = [ "uuid", ] +[[package]] +name = "rustmail_panel" +version = "0.1.0" +dependencies = [ + "yew", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -2029,6 +2385,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2791,6 +3158,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokise" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf97738ce15b9e9cc1671ea29b0f6c56538719e1a092d19cc2134bf144e40e" +dependencies = [ + "futures", + "gloo", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + [[package]] name = "toml" version = "0.9.7" @@ -2800,12 +3184,18 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.2", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.13", ] +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + [[package]] name = "toml_datetime" version = "0.7.2" @@ -2815,13 +3205,24 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + [[package]] name = "toml_parser" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ - "winnow", + "winnow 0.7.13", ] [[package]] @@ -3475,6 +3876,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.7.13" @@ -3506,6 +3916,44 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +[[package]] +name = "yew" +version = "0.21.0" +source = "git+https://github.com/yewstack/yew/#85050f50fb4d211f5a96a814e175467ca87681b3" +dependencies = [ + "console_error_panic_hook", + "futures", + "gloo", + "implicit-clone", + "indexmap", + "js-sys", + "rustversion", + "serde", + "slab", + "thiserror 2.0.12", + "tokio", + "tokise", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew-macro", +] + +[[package]] +name = "yew-macro" +version = "0.21.0" +source = "git+https://github.com/yewstack/yew/#85050f50fb4d211f5a96a814e175467ca87681b3" +dependencies = [ + "once_cell", + "prettyplease", + "proc-macro-error", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.101", +] + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 7996123..1549bee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,22 +1,3 @@ -[package] -name = "rustmail" -version = "0.1.0" -edition = "2024" -license = "MIT" - -[dependencies] -serde = { version = "*", features = ["derive"] } -serenity = "0.12.4" -sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "macros", "migrate"] } -tokio = { version = "1.47.1", features = ["rt-multi-thread", "signal"] } -toml = "0.9.7" -reqwest = "0.12.23" -async-trait = "0.1.89" -chrono = { version = "0.4.42", features = ["serde"] } -regex = "1.11.3" -axum = "0.8.6" -hyper = "1.7.0" - -[dependencies.uuid] -version = "1.18.1" -features = ["v4"] +[workspace] +members = ["rustmail", "rustmail_panel"] +resolver = "3" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 26b6af0..0000000 --- a/package-lock.json +++ /dev/null @@ -1,430 +0,0 @@ -{ - "name": "rustmail", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@prisma/client": "^6.16.3" - }, - "devDependencies": { - "prisma": "^6.16.3" - } - }, - "node_modules/@prisma/client": { - "version": "6.16.3", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.3.tgz", - "integrity": "sha512-JfNfAtXG+/lIopsvoZlZiH2k5yNx87mcTS4t9/S5oufM1nKdXYxOvpDC1XoTCFBa5cQh7uXnbMPsmZrwZY80xw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@prisma/config": { - "version": "6.16.3", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz", - "integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "c12": "3.1.0", - "deepmerge-ts": "7.1.5", - "effect": "3.16.12", - "empathic": "2.0.0" - } - }, - "node_modules/@prisma/debug": { - "version": "6.16.3", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz", - "integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "6.16.3", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz", - "integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.16.3", - "@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", - "@prisma/fetch-engine": "6.16.3", - "@prisma/get-platform": "6.16.3" - } - }, - "node_modules/@prisma/engines-version": { - "version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz", - "integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "6.16.3", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz", - "integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.16.3", - "@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a", - "@prisma/get-platform": "6.16.3" - } - }, - "node_modules/@prisma/get-platform": { - "version": "6.16.3", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz", - "integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.16.3" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/c12": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.3", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^16.6.1", - "exsolve": "^1.0.7", - "giget": "^2.0.0", - "jiti": "^2.4.2", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.2.0", - "rc9": "^2.1.2" - }, - "peerDependencies": { - "magicast": "^0.3.5" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/effect": { - "version": "3.16.12", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", - "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "fast-check": "^3.23.1" - } - }, - "node_modules/empathic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/giget": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", - "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, - "node_modules/prisma": { - "version": "6.16.3", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz", - "integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/config": "6.16.3", - "@prisma/engines": "6.16.3" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "devOptional": true, - "license": "MIT" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 8256c49..0000000 --- a/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "devDependencies": { - "prisma": "^6.16.3" - }, - "dependencies": { - "@prisma/client": "^6.16.3" - } -} diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 33c45ec..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,53 +0,0 @@ -datasource db { - provider = "sqlite" - url = "file:../db/db.sqlite" -} - -model blocked_users { - user_id String @id - user_name String - blocked_by String - blocked_at DateTime @default(now()) - expires_at DateTime @default(now()) -} - -model threads { - id String @id @unique - user_id Int - user_name String - channel_id String - created_at DateTime @default(now()) - next_message_number Int? @default(1) - status Int @default(1) - user_left Boolean @default(false) - thread_messages thread_messages[] -} - -model thread_messages { - id Int @id @unique() @default(autoincrement()) - thread_id String - thread threads @relation(fields: [thread_id], references: [id]) - user_id Int - user_name String - is_anonymous Boolean - dm_message_id String? - inbox_message_id String? - message_number Int? - created_at DateTime @default(now()) - content String - thread_status Int @default(1) -} - -model staff_alerts { - id Int @id @default(autoincrement()) - staff_user_id Int - thread_user_id Int - created_at DateTime @default(now()) - used Boolean @default(false) -} - -model system_metadata { - key String @id - value String - updated_at DateTime @default(now()) -} diff --git a/rustmail/Cargo.toml b/rustmail/Cargo.toml new file mode 100644 index 0000000..4de916d --- /dev/null +++ b/rustmail/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rustmail" +version = "0.1.1" +edition = "2024" +license = "MIT" + +[dependencies] +serde = { version = "*", features = ["derive"] } +serenity = "0.12.4" +sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "macros", "migrate"] } +tokio = { version = "1.47.1", features = ["rt-multi-thread", "signal"] } +toml = "0.9.7" +reqwest = "0.12.23" +async-trait = "0.1.89" +chrono = { version = "0.4.42", features = ["serde"] } +regex = "1.11.3" +axum = "0.8.6" +hyper = "1.7.0" +rust-embed = "8.7.2" +mime_guess = "2.0.5" + +[dependencies.uuid] +version = "1.18.1" +features = ["v4"] \ No newline at end of file diff --git a/src/api/handler/bot/mod.rs b/rustmail/src/api/handler/bot/mod.rs similarity index 100% rename from src/api/handler/bot/mod.rs rename to rustmail/src/api/handler/bot/mod.rs diff --git a/src/api/handler/bot/start.rs b/rustmail/src/api/handler/bot/start.rs similarity index 88% rename from src/api/handler/bot/start.rs rename to rustmail/src/api/handler/bot/start.rs index fad307b..ca55fad 100644 --- a/src/api/handler/bot/start.rs +++ b/rustmail/src/api/handler/bot/start.rs @@ -1,15 +1,16 @@ use crate::bot::run_bot; use crate::{BotState, BotStatus}; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use axum::Json; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use tokio::spawn; +use tokio::sync::Mutex; pub async fn handle_start_bot( State(bot_state): State>>, ) -> (StatusCode, Json<&'static str>) { - let mut state_lock = bot_state.lock().unwrap(); + let mut state_lock = bot_state.lock().await; match state_lock.status { BotStatus::Stopped => { @@ -22,7 +23,7 @@ pub async fn handle_start_bot( let handle = spawn(async move { run_bot(bot_state_clone.clone(), &mut shutdown_rx).await; - let mut s = bot_state_clone.lock().unwrap(); + let mut s = bot_state_clone.lock().await; s.status = BotStatus::Stopped; }); state_lock.status = BotStatus::Running { diff --git a/src/api/handler/bot/stop.rs b/rustmail/src/api/handler/bot/stop.rs similarity index 88% rename from src/api/handler/bot/stop.rs rename to rustmail/src/api/handler/bot/stop.rs index 58c88c4..dc156e5 100644 --- a/src/api/handler/bot/stop.rs +++ b/rustmail/src/api/handler/bot/stop.rs @@ -1,14 +1,15 @@ use crate::{BotState, BotStatus}; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use axum::Json; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use tokio::sync::Mutex; pub async fn handle_stop_bot( State(bot_state): State>>, ) -> (StatusCode, Json<&'static str>) { let handle_and_shutdown = { - let mut state_lock = bot_state.lock().unwrap(); + let mut state_lock = bot_state.lock().await; match std::mem::replace(&mut state_lock.status, BotStatus::Stopped) { BotStatus::Running { handle, shutdown } => Some((handle, shutdown)), diff --git a/src/api/handler/mod.rs b/rustmail/src/api/handler/mod.rs similarity index 100% rename from src/api/handler/mod.rs rename to rustmail/src/api/handler/mod.rs diff --git a/src/api/mod.rs b/rustmail/src/api/mod.rs similarity index 100% rename from src/api/mod.rs rename to rustmail/src/api/mod.rs diff --git a/src/api/router.rs b/rustmail/src/api/router.rs similarity index 77% rename from src/api/router.rs rename to rustmail/src/api/router.rs index 4ded206..480e826 100644 --- a/src/api/router.rs +++ b/rustmail/src/api/router.rs @@ -1,13 +1,14 @@ -use crate::api::routes::bot::create_bot_router; use crate::BotState; +use crate::api::routes::bot::create_bot_router; use axum::Router; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; +use tokio::sync::Mutex; pub fn create_api_router(bot_state: Arc>) -> Router { let bot_router = create_bot_router(); let app = Router::new() - .nest("/bot", bot_router) + .nest("/rustmail", bot_router) .with_state(bot_state.clone()); app diff --git a/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs similarity index 89% rename from src/api/routes/bot.rs rename to rustmail/src/api/routes/bot.rs index 0700815..817acd4 100644 --- a/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -1,9 +1,10 @@ +use crate::BotState; use crate::api::handler::bot::start::handle_start_bot; use crate::api::handler::bot::stop::handle_stop_bot; -use crate::BotState; -use axum::routing::post; use axum::Router; -use std::sync::{Arc, Mutex}; +use axum::routing::post; +use std::sync::Arc; +use tokio::sync::Mutex; pub fn create_bot_router() -> Router>> { let bot_router = Router::new() diff --git a/src/api/routes/mod.rs b/rustmail/src/api/routes/mod.rs similarity index 100% rename from src/api/routes/mod.rs rename to rustmail/src/api/routes/mod.rs diff --git a/src/bot.rs b/rustmail/src/bot.rs similarity index 80% rename from src/bot.rs rename to rustmail/src/bot.rs index 8c5a86c..a96ad77 100644 --- a/src/bot.rs +++ b/rustmail/src/bot.rs @@ -1,3 +1,4 @@ +use crate::commands::CommandRegistry; use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; use crate::commands::alert::slash_command::alert::AlertCommand; @@ -13,8 +14,9 @@ use crate::commands::recover::slash_command::recover::RecoverCommand; use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; use crate::commands::reply::slash_command::reply::ReplyCommand; -use crate::commands::CommandRegistry; use crate::config::load_config; +use crate::errors::ModmailError; +use crate::errors::types::ConfigError; use crate::handlers::guild_handler::GuildHandler; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::handlers::guild_members_handler::GuildMembersHandler; @@ -23,12 +25,13 @@ use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::handlers::guild_moderation_handler::GuildModerationHandler; use crate::handlers::ready_handler::ReadyHandler; use crate::handlers::typing_proxy_handler::TypingProxyHandler; -use crate::{db, BotState, BotStatus}; +use crate::{BotState, BotStatus, db}; use serenity::all::{ClientBuilder, GatewayIntents}; use serenity::cache::Settings as CacheSettings; use std::process; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; +use tokio::sync::Mutex; use tokio::sync::watch::Receiver; use tokio::{select, spawn}; @@ -49,17 +52,54 @@ pub async fn init_bot_state() -> Arc> { Arc::new(Mutex::new(bot_state)) } +pub async fn start_bot_if_config_valid( + bot_state: Arc>, +) -> Result<(), ModmailError> { + let mut state_lock = bot_state.lock().await; + + match state_lock.status { + BotStatus::Stopped => { + if state_lock.config.is_none() { + return Err(ModmailError::Config(ConfigError::FileNotFound)); + } + + let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); + let bot_state_clone = bot_state.clone(); + + let handle = spawn(async move { + let mut shutdown_rx_owned = shutdown_rx.clone(); + run_bot(bot_state_clone.clone(), &mut shutdown_rx_owned).await; + let mut s = bot_state_clone.lock().await; + s.status = BotStatus::Stopped; + }); + state_lock.status = BotStatus::Running { + handle, + shutdown: shutdown_tx, + }; + + drop(state_lock); + Ok(()) + } + BotStatus::Running { .. } => Ok(()), + } +} + pub async fn run_bot(bot_state: Arc>, shutdown: &mut Receiver) { let shutdown_rx_command = shutdown.clone(); let shutdown_rx = shutdown.clone(); + println!("Starting rustmail..."); + let pool = { - let state_lock = bot_state.lock().unwrap(); + let state_lock = bot_state.lock().await; state_lock.db_pool.clone().expect("Database pool not set") }; let mut config = { - let state_lock = bot_state.lock().unwrap(); + let state_lock = bot_state.lock().await; + if state_lock.config.is_none() { + panic!("Config not set before starting rustmail!"); + } state_lock.config.clone().expect("Config not set") }; @@ -124,7 +164,7 @@ pub async fn run_bot(bot_state: Arc>, shutdown: &mut Receiver, @@ -118,18 +119,43 @@ pub fn load_config(path: &str) -> Option { Err(_) => return None, }; - let mut config: Config = - toml::from_str(&content).expect("The format of the config.toml is not correct!"); + let mut config: Config = match toml::from_str(&content) { + Ok(c) => c, + Err(e) => { + eprintln!("Failed to parse config.toml: {}", e); + return None; + } + }; + + match u64::from_str_radix(&config.thread.user_message_color, 16) { + Ok(_) => {} + Err(_) => { + eprintln!( + "Incorect user message color in the config.toml! Please put a color in hex format!" + ); + return None; + } + } + + match u64::from_str_radix(&config.thread.staff_message_color, 16) { + Ok(_) => {} + Err(_) => { + eprintln!( + "Incorect staff message color in the config.toml! Please put a color in hex format!" + ); + return None; + } + } - let _ = u64::from_str_radix(&config.thread.user_message_color, 16).expect( - "Incorect user message color in the config.toml! Please put a color in hex format!", - ); - let _ = u64::from_str_radix(&config.thread.staff_message_color, 16).expect( - "Incorect staff message color in the config.toml! Please put a color in hex format!", - ); - let _ = u64::from_str_radix(&config.reminders.embed_color, 16).expect( - "Incorect staff message color in the config.toml! Please put a color in hex format!", - ); + match u64::from_str_radix(&config.reminders.embed_color, 16) { + Ok(_) => {} + Err(_) => { + eprintln!( + "Incorect reminder embed color in the config.toml! Please put a color in hex format!" + ); + return None; + } + } if !config .language @@ -141,14 +167,21 @@ pub fn load_config(path: &str) -> Option { ); } - config - .bot - .validate_logs_config() - .expect("Invalid logs configuration !"); - config - .bot - .validate_features_config() - .expect("Invalid features configuration !"); + match config.bot.validate_logs_config() { + Ok(_) => {} + Err(e) => { + eprintln!("Invalid logs configuration: {}", e); + return None; + } + } + + match config.bot.validate_features_config() { + Ok(_) => {} + Err(e) => { + eprintln!("Invalid features configuration: {}", e); + return None; + } + } let default_lang = config.language.get_default_language(); let fallback_lang = config.language.get_fallback_language(); @@ -307,7 +340,8 @@ impl Config { let community_guild_id = GuildId::new(*community_guild_id); let staff_guild_id = GuildId::new(*staff_guild_id); - if let Err(_) = community_guild_id.to_partial_guild(http).await { + if let Err(e) = community_guild_id.to_partial_guild(http).await { + eprintln!("Error fetching community guild: {}", e); return Err(format!( "Serveur communautaire introuvable: {}", community_guild_id diff --git a/src/db/mod.rs b/rustmail/src/db/mod.rs similarity index 100% rename from src/db/mod.rs rename to rustmail/src/db/mod.rs diff --git a/src/db/operations/features.rs b/rustmail/src/db/operations/features.rs similarity index 100% rename from src/db/operations/features.rs rename to rustmail/src/db/operations/features.rs diff --git a/src/db/operations/init.rs b/rustmail/src/db/operations/init.rs similarity index 89% rename from src/db/operations/init.rs rename to rustmail/src/db/operations/init.rs index 7dafed5..7f951ff 100644 --- a/src/db/operations/init.rs +++ b/rustmail/src/db/operations/init.rs @@ -1,11 +1,11 @@ -use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; +use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; use std::fs; use std::path::Path; pub async fn init_database() -> Result { - let db_path = "./db/db.sqlite"; + let db_path = "db/db.sqlite"; - fs::create_dir_all("./db")?; + fs::create_dir_all("db")?; if !Path::new(db_path).exists() { fs::File::create(db_path)?; @@ -18,7 +18,7 @@ pub async fn init_database() -> Result { .connect(&db_url) .await?; - sqlx::migrate!("./migrations").run(&pool).await?; + sqlx::migrate!("../migrations").run(&pool).await?; println!("Database connection pool established"); Ok(pool) diff --git a/src/db/operations/messages.rs b/rustmail/src/db/operations/messages.rs similarity index 100% rename from src/db/operations/messages.rs rename to rustmail/src/db/operations/messages.rs diff --git a/src/db/operations/mod.rs b/rustmail/src/db/operations/mod.rs similarity index 100% rename from src/db/operations/mod.rs rename to rustmail/src/db/operations/mod.rs diff --git a/src/db/operations/reminders.rs b/rustmail/src/db/operations/reminders.rs similarity index 100% rename from src/db/operations/reminders.rs rename to rustmail/src/db/operations/reminders.rs diff --git a/src/db/operations/scheduled.rs b/rustmail/src/db/operations/scheduled.rs similarity index 100% rename from src/db/operations/scheduled.rs rename to rustmail/src/db/operations/scheduled.rs diff --git a/src/db/operations/threads.rs b/rustmail/src/db/operations/threads.rs similarity index 100% rename from src/db/operations/threads.rs rename to rustmail/src/db/operations/threads.rs diff --git a/src/db/repr.rs b/rustmail/src/db/repr.rs similarity index 100% rename from src/db/repr.rs rename to rustmail/src/db/repr.rs diff --git a/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs similarity index 100% rename from src/errors/dictionary.rs rename to rustmail/src/errors/dictionary.rs diff --git a/src/errors/handler.rs b/rustmail/src/errors/handler.rs similarity index 100% rename from src/errors/handler.rs rename to rustmail/src/errors/handler.rs diff --git a/src/errors/mod.rs b/rustmail/src/errors/mod.rs similarity index 100% rename from src/errors/mod.rs rename to rustmail/src/errors/mod.rs diff --git a/src/errors/types.rs b/rustmail/src/errors/types.rs similarity index 99% rename from src/errors/types.rs rename to rustmail/src/errors/types.rs index 802d321..1843ba0 100644 --- a/src/errors/types.rs +++ b/rustmail/src/errors/types.rs @@ -180,12 +180,12 @@ impl fmt::Display for DiscordError { DiscordError::ApiError(msg) => write!(f, "Discord API error: {}", msg), DiscordError::ChannelNotFound => write!(f, "Channel not found"), DiscordError::UserNotFound => write!(f, "User not found"), - DiscordError::UserIsABot => write!(f, "User is a bot"), + DiscordError::UserIsABot => write!(f, "User is a rustmail"), DiscordError::GuildNotFound => write!(f, "Guild not found"), DiscordError::MessageNotFound => write!(f, "Message not found"), DiscordError::PermissionDenied => write!(f, "Permission denied"), DiscordError::RateLimited => write!(f, "Rate limited"), - DiscordError::InvalidToken => write!(f, "Invalid bot token"), + DiscordError::InvalidToken => write!(f, "Invalid rustmail token"), DiscordError::WebhookError(msg) => write!(f, "Webhook error: {}", msg), DiscordError::EmbedTooLarge => write!(f, "Embed too large"), DiscordError::MessageTooLong => write!(f, "Message too long"), diff --git a/src/features/mod.rs b/rustmail/src/features/mod.rs similarity index 100% rename from src/features/mod.rs rename to rustmail/src/features/mod.rs diff --git a/src/features/poll.rs b/rustmail/src/features/poll.rs similarity index 100% rename from src/features/poll.rs rename to rustmail/src/features/poll.rs diff --git a/src/handlers/guild_handler.rs b/rustmail/src/handlers/guild_handler.rs similarity index 100% rename from src/handlers/guild_handler.rs rename to rustmail/src/handlers/guild_handler.rs diff --git a/src/handlers/guild_interaction_handler.rs b/rustmail/src/handlers/guild_interaction_handler.rs similarity index 100% rename from src/handlers/guild_interaction_handler.rs rename to rustmail/src/handlers/guild_interaction_handler.rs diff --git a/src/handlers/guild_members_handler.rs b/rustmail/src/handlers/guild_members_handler.rs similarity index 100% rename from src/handlers/guild_members_handler.rs rename to rustmail/src/handlers/guild_members_handler.rs diff --git a/src/handlers/guild_message_reactions_handler.rs b/rustmail/src/handlers/guild_message_reactions_handler.rs similarity index 100% rename from src/handlers/guild_message_reactions_handler.rs rename to rustmail/src/handlers/guild_message_reactions_handler.rs diff --git a/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs similarity index 99% rename from src/handlers/guild_messages_handler.rs rename to rustmail/src/handlers/guild_messages_handler.rs index 5da1d7b..3cc0f9c 100644 --- a/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -23,7 +23,7 @@ use crate::db::operations::{ }; use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::get_thread_lock::get_thread_lock; diff --git a/src/handlers/guild_moderation_handler.rs b/rustmail/src/handlers/guild_moderation_handler.rs similarity index 100% rename from src/handlers/guild_moderation_handler.rs rename to rustmail/src/handlers/guild_moderation_handler.rs diff --git a/src/handlers/mod.rs b/rustmail/src/handlers/mod.rs similarity index 100% rename from src/handlers/mod.rs rename to rustmail/src/handlers/mod.rs diff --git a/src/handlers/ready_handler.rs b/rustmail/src/handlers/ready_handler.rs similarity index 100% rename from src/handlers/ready_handler.rs rename to rustmail/src/handlers/ready_handler.rs diff --git a/src/handlers/typing_proxy_handler.rs b/rustmail/src/handlers/typing_proxy_handler.rs similarity index 100% rename from src/handlers/typing_proxy_handler.rs rename to rustmail/src/handlers/typing_proxy_handler.rs diff --git a/src/i18n/language/cn.rs b/rustmail/src/i18n/language/cn.rs similarity index 100% rename from src/i18n/language/cn.rs rename to rustmail/src/i18n/language/cn.rs diff --git a/src/i18n/language/dt.rs b/rustmail/src/i18n/language/dt.rs similarity index 100% rename from src/i18n/language/dt.rs rename to rustmail/src/i18n/language/dt.rs diff --git a/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs similarity index 97% rename from src/i18n/language/en.rs rename to rustmail/src/i18n/language/en.rs index a5f033b..6d59c7f 100644 --- a/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -5,7 +5,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "database.connection_failed".to_string(), DictionaryMessage::new("Failed to connect to the database") - .with_description("The bot couldn't establish a connection to the database"), + .with_description("The rustmail couldn't establish a connection to the database"), ); dict.messages.insert( "database.query_failed".to_string(), @@ -20,7 +20,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "discord.channel_not_found".to_string(), DictionaryMessage::new("Channel not found").with_description( - "The specified channel doesn't exist or the bot doesn't have access to it", + "The specified channel doesn't exist or the rustmail doesn't have access to it", ), ); dict.messages.insert( @@ -31,7 +31,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "discord.permission_denied".to_string(), DictionaryMessage::new("Permission denied").with_description( - "The bot doesn't have the required permissions to perform this action", + "The rustmail doesn't have the required permissions to perform this action", ), ); dict.messages.insert( @@ -46,7 +46,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "discord.user_is_a_bot".to_string(), - DictionaryMessage::new("The specified user is a bot."), + DictionaryMessage::new("The specified user is a rustmail."), ); dict.messages.insert( "command.invalid_format".to_string(), @@ -211,7 +211,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "thread.modal_bot_user".to_string(), - DictionaryMessage::new("The specified user is a bot, please choose another one."), + DictionaryMessage::new("The specified user is a rustmail, please choose another one."), ); dict.messages.insert( "thread.thread_closing".to_string(), @@ -257,12 +257,12 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "reply.send_failed_thread".to_string(), DictionaryMessage::new("Failed to send the message to the channel.") - .with_description("The bot could not send the message to the thread channel."), + .with_description("The rustmail could not send the message to the thread channel."), ); dict.messages.insert( "reply.send_failed_dm".to_string(), DictionaryMessage::new("Failed to send the message to the user in DM.") - .with_description("The bot could not send the message to the user's DM."), + .with_description("The rustmail could not send the message to the user's DM."), ); dict.messages.insert( "edit.validation.invalid_format".to_string(), @@ -372,7 +372,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "config.invalid_configuration".to_string(), DictionaryMessage::new("Invalid configuration") - .with_description("The bot configuration is incorrect"), + .with_description("The rustmail configuration is incorrect"), ); dict.messages.insert( "general.unknown_error".to_string(), @@ -382,7 +382,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "recovery.messages_recovered".to_string(), - DictionaryMessage::new("📥 **{count} message(s) recovered** during bot downtime") + DictionaryMessage::new("📥 **{count} message(s) recovered** during rustmail downtime") .with_description("Notification of recovered missing messages"), ); dict.messages.insert( @@ -448,8 +448,9 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "move_thread.failed_to_fetch_categories".to_string(), - DictionaryMessage::new("❌ Failed to fetch server categories") - .with_description("The bot couldn't retrieve the list of categories from the server"), + DictionaryMessage::new("❌ Failed to fetch server categories").with_description( + "The rustmail couldn't retrieve the list of categories from the server", + ), ); dict.messages.insert( "move_thread.category_not_found".to_string(), @@ -497,7 +498,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "new_thread.user_is_a_bot".to_string(), - DictionaryMessage::new("❌ You cannot create a thread for a bot user."), + DictionaryMessage::new("❌ You cannot create a thread for a rustmail user."), ); dict.messages.insert( "new_thread.channel_creation_failed".to_string(), @@ -720,7 +721,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "slash_command.recover_command_description".to_string(), DictionaryMessage::new( - "Retrieve messages missed during the bot's downtime (This process is automatic).", + "Retrieve messages missed during the rustmail's downtime (This process is automatic).", ), ); dict.messages.insert( diff --git a/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs similarity index 96% rename from src/i18n/language/fr.rs rename to rustmail/src/i18n/language/fr.rs index add8156..c6f90ec 100644 --- a/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -4,7 +4,7 @@ use crate::errors::dictionary::ErrorDictionary; pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert("database.connection_failed".to_string(), DictionaryMessage::new("Échec de connexion à la base de données") - .with_description("Le bot n'a pas pu établir une connexion à la base de données") + .with_description("Le rustmail n'a pas pu établir une connexion à la base de données") .with_help("Vérifiez la configuration de la base de données et assurez-vous que le serveur est en marche")); dict.messages.insert( "database.query_failed".to_string(), @@ -19,7 +19,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "discord.channel_not_found".to_string(), DictionaryMessage::new("Canal non trouvé") - .with_description("Le canal spécifié n'existe pas ou le bot n'y a pas accès"), + .with_description("Le canal spécifié n'existe pas ou le rustmail n'y a pas accès"), ); dict.messages.insert( "discord.user_not_found".to_string(), @@ -29,7 +29,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "discord.permission_denied".to_string(), DictionaryMessage::new("Permission refusée").with_description( - "Le bot n'a pas les permissions requises pour effectuer cette action", + "Le rustmail n'a pas les permissions requises pour effectuer cette action", ), ); dict.messages.insert( @@ -44,7 +44,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "discord.user_is_a_bot".to_string(), - DictionaryMessage::new("L'utilisateur spécifié est un bot"), + DictionaryMessage::new("L'utilisateur spécifié est un rustmail"), ); dict.messages.insert( "command.invalid_format".to_string(), @@ -206,7 +206,9 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "thread.modal_bot_user".to_string(), - DictionaryMessage::new("L'utilisateur spécifié est un bot, veuillez en choisir un autre."), + DictionaryMessage::new( + "L'utilisateur spécifié est un rustmail, veuillez en choisir un autre.", + ), ); dict.messages.insert( "thread.created".to_string(), @@ -238,13 +240,13 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "reply.send_failed_thread".to_string(), DictionaryMessage::new("Échec de l'envoi du message dans le salon.") - .with_description("Le bot n'a pas pu envoyer le message dans le salon du thread."), + .with_description("Le rustmail n'a pas pu envoyer le message dans le salon du thread."), ); dict.messages.insert( "reply.send_failed_dm".to_string(), DictionaryMessage::new("Échec de l'envoi du message en DM à l'utilisateur.") .with_description( - "Le bot n'a pas pu envoyer le message en message privé à l'utilisateur.", + "Le rustmail n'a pas pu envoyer le message en message privé à l'utilisateur.", ), ); dict.messages.insert( @@ -325,7 +327,9 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "edit.edit_failed_thread".to_string(), DictionaryMessage::new("❌ Échec de la modification du message dans le thread.") - .with_description("Le bot n'a pas pu modifier le message dans le salon du thread."), + .with_description( + "Le rustmail n'a pas pu modifier le message dans le salon du thread.", + ), ); dict.messages.insert( "edit.invalid_id_dm".to_string(), @@ -335,12 +339,12 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "edit.dm_access_failed".to_string(), DictionaryMessage::new("❌ Impossible d'accéder aux DMs de l'utilisateur.") - .with_description("Le bot n'a pas pu envoyer de message privé à l'utilisateur. Il a peut-être bloqué le bot ou désactivé ses DMs."), + .with_description("Le rustmail n'a pas pu envoyer de message privé à l'utilisateur. Il a peut-être bloqué le rustmail ou désactivé ses DMs."), ); dict.messages.insert( "edit.edit_failed_dm".to_string(), DictionaryMessage::new("❌ Échec de la modification du message en DM.") - .with_description("Le bot n'a pas pu modifier le message en message privé."), + .with_description("Le rustmail n'a pas pu modifier le message en message privé."), ); dict.messages.insert( "permission.insufficient_permissions".to_string(), @@ -387,7 +391,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "config.invalid_configuration".to_string(), DictionaryMessage::new("Configuration invalide") - .with_description("La configuration du bot est incorrecte"), + .with_description("La configuration du rustmail est incorrecte"), ); dict.messages.insert( "general.unknown_error".to_string(), @@ -401,7 +405,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "recovery.messages_recovered".to_string(), DictionaryMessage::new( - "📥 **{count} message(s) récupéré(s)** pendant la période d'indisponibilité du bot", + "📥 **{count} message(s) récupéré(s)** pendant la période d'indisponibilité du rustmail", ) .with_description("Notification de récupération de messages manqués"), ); @@ -470,7 +474,9 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert( "move_thread.failed_to_fetch_categories".to_string(), DictionaryMessage::new("❌ Échec de récupération des catégories du serveur") - .with_description("Le bot n'a pas pu récupérer la liste des catégories du serveur"), + .with_description( + "Le rustmail n'a pas pu récupérer la liste des catégories du serveur", + ), ); dict.messages.insert( "move_thread.category_not_found".to_string(), @@ -514,7 +520,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "new_thread.user_is_a_bot".to_string(), - DictionaryMessage::new("❌ Vous ne pouvez pas créer un ticket pour un bot."), + DictionaryMessage::new("❌ Vous ne pouvez pas créer un ticket pour un rustmail."), ); dict.messages.insert( "new_thread.channel_creation_failed".to_string(), @@ -738,7 +744,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "slash_command.recover_command_description".to_string(), - DictionaryMessage::new("Récupérer les messages manqués pendant la période d'indisponibilité du bot (automatique)"), + DictionaryMessage::new("Récupérer les messages manqués pendant la période d'indisponibilité du rustmail (automatique)"), ); dict.messages.insert( "slash_command.help_command_description".to_string(), diff --git a/src/i18n/language/gr.rs b/rustmail/src/i18n/language/gr.rs similarity index 100% rename from src/i18n/language/gr.rs rename to rustmail/src/i18n/language/gr.rs diff --git a/src/i18n/language/it.rs b/rustmail/src/i18n/language/it.rs similarity index 100% rename from src/i18n/language/it.rs rename to rustmail/src/i18n/language/it.rs diff --git a/src/i18n/language/jp.rs b/rustmail/src/i18n/language/jp.rs similarity index 100% rename from src/i18n/language/jp.rs rename to rustmail/src/i18n/language/jp.rs diff --git a/src/i18n/language/kr.rs b/rustmail/src/i18n/language/kr.rs similarity index 100% rename from src/i18n/language/kr.rs rename to rustmail/src/i18n/language/kr.rs diff --git a/src/i18n/language/mod.rs b/rustmail/src/i18n/language/mod.rs similarity index 100% rename from src/i18n/language/mod.rs rename to rustmail/src/i18n/language/mod.rs diff --git a/src/i18n/language/pr.rs b/rustmail/src/i18n/language/pr.rs similarity index 100% rename from src/i18n/language/pr.rs rename to rustmail/src/i18n/language/pr.rs diff --git a/src/i18n/language/ru.rs b/rustmail/src/i18n/language/ru.rs similarity index 100% rename from src/i18n/language/ru.rs rename to rustmail/src/i18n/language/ru.rs diff --git a/src/i18n/language/sp.rs b/rustmail/src/i18n/language/sp.rs similarity index 100% rename from src/i18n/language/sp.rs rename to rustmail/src/i18n/language/sp.rs diff --git a/src/i18n/languages.rs b/rustmail/src/i18n/languages.rs similarity index 100% rename from src/i18n/languages.rs rename to rustmail/src/i18n/languages.rs diff --git a/src/i18n/mod.rs b/rustmail/src/i18n/mod.rs similarity index 100% rename from src/i18n/mod.rs rename to rustmail/src/i18n/mod.rs diff --git a/src/i18n/utils.rs b/rustmail/src/i18n/utils.rs similarity index 100% rename from src/i18n/utils.rs rename to rustmail/src/i18n/utils.rs diff --git a/rustmail/src/main.rs b/rustmail/src/main.rs new file mode 100644 index 0000000..dc2a270 --- /dev/null +++ b/rustmail/src/main.rs @@ -0,0 +1,136 @@ +use crate::api::router::create_api_router; +use crate::bot::{init_bot_state, start_bot_if_config_valid}; +use crate::config::Config; +use axum::extract::Path; +use axum::response::Response; +use rust_embed::RustEmbed; +use std::borrow::Cow; +use tokio::signal; +use tokio::sync::watch::Sender; +use tokio::task::JoinHandle; + +mod api; +mod bot; +mod commands; +mod config; +mod db; +mod errors; +mod features; +mod handlers; +mod i18n; +mod modules; +mod utils; + +pub struct Database; + +enum BotStatus { + Stopped, + Running { + handle: JoinHandle<()>, + shutdown: Sender, + }, +} + +struct BotState { + config: Option, + status: BotStatus, + db_pool: Option, +} + +#[derive(RustEmbed)] +#[folder = "static/"] +struct Assets; + +async fn static_handler(path: Option>) -> Response { + let path = path.map(|p| p.0).unwrap_or_else(|| "".to_string()); + + let path = if path.is_empty() || path == "/" { + "index.html".to_string() + } else { + path.trim_start_matches('/').to_string() + }; + + match Assets::get(&path) { + Some(content) => { + let mime = mime_guess::from_path(&path).first_or_octet_stream(); + let body = match content.data { + Cow::Borrowed(bytes) => axum::body::Body::from(bytes.to_vec()), + Cow::Owned(bytes) => axum::body::Body::from(bytes), + }; + axum::response::Response::builder() + .header("Content-Type", mime.as_ref()) + .body(body) + .unwrap() + } + None => { + if let Some(index) = Assets::get("index.html") { + let body = match index.data { + Cow::Borrowed(bytes) => axum::body::Body::from(bytes.to_vec()), + Cow::Owned(bytes) => axum::body::Body::from(bytes), + }; + axum::response::Response::builder() + .header("Content-Type", "text/html") + .body(body) + .unwrap() + } else { + axum::response::Response::builder() + .status(404) + .body(axum::body::Body::from("404 Not Found")) + .unwrap() + } + } + } +} + +#[tokio::main] +async fn main() { + let bot_state = init_bot_state().await; + + let _ = start_bot_if_config_valid(bot_state.clone()).await; + + let config = { + let state = bot_state.lock().await; + state.config.clone() + }; + + if let Some(config) = config { + if config.bot.enable_panel { + let bot_state_clone = bot_state.clone(); + + let server_task = tokio::spawn(async move { + let app = create_api_router(bot_state_clone) + .route("/", axum::routing::get(static_handler)) + .route("/{*path}", axum::routing::get(static_handler)); + + let listener = tokio::net::TcpListener::bind("0.0.0.0:3002").await.unwrap(); + println!("listening on {}", listener.local_addr().unwrap()); + + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); + }); + + tokio::select! { + _ = server_task => {}, + _ = tokio::signal::ctrl_c() => { println!("Shutting down"); } + } + } else { + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!("Shutting down"); + break; + } + } + } + } + } +} + +async fn shutdown_signal() { + signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + println!("Shutdown signal received"); +} diff --git a/src/modules/message_recovery.rs b/rustmail/src/modules/message_recovery.rs similarity index 100% rename from src/modules/message_recovery.rs rename to rustmail/src/modules/message_recovery.rs diff --git a/src/modules/mod.rs b/rustmail/src/modules/mod.rs similarity index 100% rename from src/modules/mod.rs rename to rustmail/src/modules/mod.rs diff --git a/src/modules/reminders.rs b/rustmail/src/modules/reminders.rs similarity index 100% rename from src/modules/reminders.rs rename to rustmail/src/modules/reminders.rs diff --git a/src/modules/scheduled_closures.rs b/rustmail/src/modules/scheduled_closures.rs similarity index 100% rename from src/modules/scheduled_closures.rs rename to rustmail/src/modules/scheduled_closures.rs diff --git a/src/modules/threads.rs b/rustmail/src/modules/threads.rs similarity index 99% rename from src/modules/threads.rs rename to rustmail/src/modules/threads.rs index 8c795a7..8cb1fbc 100644 --- a/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -272,7 +272,10 @@ pub async fn handle_thread_modal_interaction( }; if user.bot { - eprintln!("Attempted to create thread for a bot user: {}", user_id); + eprintln!( + "Attempted to create thread for a rustmail user: {}", + user_id + ); let _ = interaction .create_response( &ctx.http, diff --git a/src/utils/command/defer_response.rs b/rustmail/src/utils/command/defer_response.rs similarity index 100% rename from src/utils/command/defer_response.rs rename to rustmail/src/utils/command/defer_response.rs diff --git a/src/utils/command/extract_reply_content.rs b/rustmail/src/utils/command/extract_reply_content.rs similarity index 100% rename from src/utils/command/extract_reply_content.rs rename to rustmail/src/utils/command/extract_reply_content.rs diff --git a/src/utils/command/mod.rs b/rustmail/src/utils/command/mod.rs similarity index 100% rename from src/utils/command/mod.rs rename to rustmail/src/utils/command/mod.rs diff --git a/src/utils/command/wrap_command.rs b/rustmail/src/utils/command/wrap_command.rs similarity index 100% rename from src/utils/command/wrap_command.rs rename to rustmail/src/utils/command/wrap_command.rs diff --git a/src/utils/conversion/hex_string_to_int.rs b/rustmail/src/utils/conversion/hex_string_to_int.rs similarity index 100% rename from src/utils/conversion/hex_string_to_int.rs rename to rustmail/src/utils/conversion/hex_string_to_int.rs diff --git a/src/utils/conversion/mod.rs b/rustmail/src/utils/conversion/mod.rs similarity index 100% rename from src/utils/conversion/mod.rs rename to rustmail/src/utils/conversion/mod.rs diff --git a/src/utils/message/message_builder.rs b/rustmail/src/utils/message/message_builder.rs similarity index 100% rename from src/utils/message/message_builder.rs rename to rustmail/src/utils/message/message_builder.rs diff --git a/src/utils/message/mod.rs b/rustmail/src/utils/message/mod.rs similarity index 100% rename from src/utils/message/mod.rs rename to rustmail/src/utils/message/mod.rs diff --git a/src/utils/message/reply_intent.rs b/rustmail/src/utils/message/reply_intent.rs similarity index 100% rename from src/utils/message/reply_intent.rs rename to rustmail/src/utils/message/reply_intent.rs diff --git a/src/utils/message/ui_components.rs b/rustmail/src/utils/message/ui_components.rs similarity index 100% rename from src/utils/message/ui_components.rs rename to rustmail/src/utils/message/ui_components.rs diff --git a/src/utils/mod.rs b/rustmail/src/utils/mod.rs similarity index 100% rename from src/utils/mod.rs rename to rustmail/src/utils/mod.rs diff --git a/src/utils/thread/fetch_thread.rs b/rustmail/src/utils/thread/fetch_thread.rs similarity index 100% rename from src/utils/thread/fetch_thread.rs rename to rustmail/src/utils/thread/fetch_thread.rs diff --git a/src/utils/thread/get_thread_lock.rs b/rustmail/src/utils/thread/get_thread_lock.rs similarity index 100% rename from src/utils/thread/get_thread_lock.rs rename to rustmail/src/utils/thread/get_thread_lock.rs diff --git a/src/utils/thread/mod.rs b/rustmail/src/utils/thread/mod.rs similarity index 100% rename from src/utils/thread/mod.rs rename to rustmail/src/utils/thread/mod.rs diff --git a/src/utils/thread/send_to_thread.rs b/rustmail/src/utils/thread/send_to_thread.rs similarity index 100% rename from src/utils/thread/send_to_thread.rs rename to rustmail/src/utils/thread/send_to_thread.rs diff --git a/src/utils/time/format_duration_since.rs b/rustmail/src/utils/time/format_duration_since.rs similarity index 100% rename from src/utils/time/format_duration_since.rs rename to rustmail/src/utils/time/format_duration_since.rs diff --git a/src/utils/time/get_member_join_date.rs b/rustmail/src/utils/time/get_member_join_date.rs similarity index 100% rename from src/utils/time/get_member_join_date.rs rename to rustmail/src/utils/time/get_member_join_date.rs diff --git a/src/utils/time/mod.rs b/rustmail/src/utils/time/mod.rs similarity index 100% rename from src/utils/time/mod.rs rename to rustmail/src/utils/time/mod.rs diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml new file mode 100644 index 0000000..e6d6158 --- /dev/null +++ b/rustmail_panel/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rustmail_panel" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] } diff --git a/rustmail_panel/Trunk.toml b/rustmail_panel/Trunk.toml new file mode 100644 index 0000000..d041aad --- /dev/null +++ b/rustmail_panel/Trunk.toml @@ -0,0 +1,3 @@ +[build] +release = true +generate-integrity = false diff --git a/rustmail_panel/index.html b/rustmail_panel/index.html new file mode 100644 index 0000000..e2e5069 --- /dev/null +++ b/rustmail_panel/index.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/rustmail_panel/src/main.rs b/rustmail_panel/src/main.rs new file mode 100644 index 0000000..c19d61d --- /dev/null +++ b/rustmail_panel/src/main.rs @@ -0,0 +1,24 @@ +use yew::prelude::*; + +#[component] +fn App() -> Html { + let counter = use_state(|| 0); + let onclick = { + let counter = counter.clone(); + move |_| { + let value = *counter + 1; + counter.set(value); + } + }; + + html! { +
+ +

{ *counter }

+
+ } +} + +fn main() { + yew::Renderer::::new().render(); +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index f983676..0000000 --- a/src/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::api::router::create_api_router; -use crate::bot::init_bot_state; -use crate::config::Config; -use tokio::signal; -use tokio::sync::watch::Sender; -use tokio::task::JoinHandle; - -mod api; -mod bot; -mod commands; -mod config; -mod db; -mod errors; -mod features; -mod handlers; -mod i18n; -mod modules; -mod utils; - -pub struct Database; - -enum BotStatus { - Stopped, - Running { - handle: JoinHandle<()>, - shutdown: Sender, - }, -} - -struct BotState { - config: Option, - status: BotStatus, - db_pool: Option, -} - -#[tokio::main] -async fn main() { - let bot_state = init_bot_state().await; - - let app = create_api_router(bot_state); - - let listener = tokio::net::TcpListener::bind("0.0.0.0:3002").await.unwrap(); - println!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app) - .with_graceful_shutdown(shutdown_signal()) - .await - .unwrap(); -} - -async fn shutdown_signal() { - signal::ctrl_c() - .await - .expect("Failed to install Ctrl+C handler"); - println!("Shutdown signal received"); -} From 3b9773fd5a60d21e59bc18cfbb2c68cdd0badd0a Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 14 Oct 2025 14:58:50 +0200 Subject: [PATCH 004/100] feat(panel): add base for docs page --- rustmail_panel/Cargo.toml | 1 + rustmail_panel/Trunk.toml | 2 +- rustmail_panel/index.html | 18 +++++++++++---- rustmail_panel/src/components/home.rs | 18 +++++++++++++++ rustmail_panel/src/components/mod.rs | 1 + rustmail_panel/src/main.rs | 19 +++++---------- rustmail_panel/src/pages/docs.rs | 14 +++++++++++ rustmail_panel/src/pages/home.rs | 18 +++++++++++++++ rustmail_panel/src/pages/mod.rs | 2 ++ rustmail_panel/src/router.rs | 32 ++++++++++++++++++++++++++ rustmail_panel/src/tailwind.css | 3 +++ rustmail_panel/static/logo.png | Bin 0 -> 430591 bytes rustmail_panel/tailwind.config.js | 14 +++++++++++ 13 files changed, 124 insertions(+), 18 deletions(-) create mode 100644 rustmail_panel/src/components/home.rs create mode 100644 rustmail_panel/src/components/mod.rs create mode 100644 rustmail_panel/src/pages/docs.rs create mode 100644 rustmail_panel/src/pages/home.rs create mode 100644 rustmail_panel/src/pages/mod.rs create mode 100644 rustmail_panel/src/router.rs create mode 100644 rustmail_panel/src/tailwind.css create mode 100644 rustmail_panel/static/logo.png create mode 100644 rustmail_panel/tailwind.config.js diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index e6d6158..1330322 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -6,3 +6,4 @@ license = "MIT" [dependencies] yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] } +yew-router = "0.18.0" diff --git a/rustmail_panel/Trunk.toml b/rustmail_panel/Trunk.toml index d041aad..88d51a0 100644 --- a/rustmail_panel/Trunk.toml +++ b/rustmail_panel/Trunk.toml @@ -1,3 +1,3 @@ [build] release = true -generate-integrity = false +generate-integrity = false \ No newline at end of file diff --git a/rustmail_panel/index.html b/rustmail_panel/index.html index e2e5069..e03beba 100644 --- a/rustmail_panel/index.html +++ b/rustmail_panel/index.html @@ -1,5 +1,15 @@ - - - - + + + + + + Rustmail Panel + + + + + + + + \ No newline at end of file diff --git a/rustmail_panel/src/components/home.rs b/rustmail_panel/src/components/home.rs new file mode 100644 index 0000000..b382bc2 --- /dev/null +++ b/rustmail_panel/src/components/home.rs @@ -0,0 +1,18 @@ +use yew::prelude::*; + +#[function_component(Home)] +pub fn home() -> Html { + html! { +
+ Rustmail logo +

{"Rustmail Panel"}

+

+ {"Bienvenue sur le pannel de Rustmail"} +

+ +
+ } +} diff --git a/rustmail_panel/src/components/mod.rs b/rustmail_panel/src/components/mod.rs new file mode 100644 index 0000000..9b86bcf --- /dev/null +++ b/rustmail_panel/src/components/mod.rs @@ -0,0 +1 @@ +pub mod home; diff --git a/rustmail_panel/src/main.rs b/rustmail_panel/src/main.rs index c19d61d..f95038c 100644 --- a/rustmail_panel/src/main.rs +++ b/rustmail_panel/src/main.rs @@ -1,21 +1,14 @@ +mod components; +mod pages; +mod router; + +use crate::components::home::Home; use yew::prelude::*; #[component] fn App() -> Html { - let counter = use_state(|| 0); - let onclick = { - let counter = counter.clone(); - move |_| { - let value = *counter + 1; - counter.set(value); - } - }; - html! { -
- -

{ *counter }

-
+ } } diff --git a/rustmail_panel/src/pages/docs.rs b/rustmail_panel/src/pages/docs.rs new file mode 100644 index 0000000..d8094c7 --- /dev/null +++ b/rustmail_panel/src/pages/docs.rs @@ -0,0 +1,14 @@ +use yew::{function_component, html, Html}; + +#[function_component(Docs)] +pub fn docs() -> Html { + html! { +
+ Rustmail logo +

{"Rustmail Panel"}

+

+ {"Page de documentation en construction..."} +

+
+ } +} diff --git a/rustmail_panel/src/pages/home.rs b/rustmail_panel/src/pages/home.rs new file mode 100644 index 0000000..b382bc2 --- /dev/null +++ b/rustmail_panel/src/pages/home.rs @@ -0,0 +1,18 @@ +use yew::prelude::*; + +#[function_component(Home)] +pub fn home() -> Html { + html! { +
+ Rustmail logo +

{"Rustmail Panel"}

+

+ {"Bienvenue sur le pannel de Rustmail"} +

+ +
+ } +} diff --git a/rustmail_panel/src/pages/mod.rs b/rustmail_panel/src/pages/mod.rs new file mode 100644 index 0000000..076abb9 --- /dev/null +++ b/rustmail_panel/src/pages/mod.rs @@ -0,0 +1,2 @@ +pub mod docs; +pub mod home; diff --git a/rustmail_panel/src/router.rs b/rustmail_panel/src/router.rs new file mode 100644 index 0000000..9d96c0b --- /dev/null +++ b/rustmail_panel/src/router.rs @@ -0,0 +1,32 @@ +use crate::pages::docs::Docs; +use crate::pages::home::Home; +use yew::prelude::*; +use yew_router::prelude::*; + +#[derive(Routable, PartialEq, Eq, Clone, Debug)] +pub enum Route { + #[at("/")] + Home, + #[at("/docs")] + Docs, + #[not_found] + #[at("/404")] + NotFound, +} + +#[function_component(App)] +pub fn app() -> Html { + html! { + + render={Switch::render(switch)} /> + + } +} + +fn switch(route: Route) -> Html { + match route { + Route::Home => html! { }, + Route::Docs => html! { }, + Route::NotFound => html! {

{"Page not found"}

}, + } +} diff --git a/rustmail_panel/src/tailwind.css b/rustmail_panel/src/tailwind.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/rustmail_panel/src/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/rustmail_panel/static/logo.png b/rustmail_panel/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4f6c5ec539eae21e7b65be6b19331b797110abf9 GIT binary patch literal 430591 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

a}wb|4qq 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 z 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

#ke@$1Q`)dvCuHnR- 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⋙tJ~ zN=^NT`uWI)m)cNS#pq5y8@Sjj;|R^Y-@JTdz64nbBr;EGzR4_hocte6kvFHTi5~1;4)3 zF9(}1e40P>da*j@*H&A}o5I48L!0&RFLQCGhj>-_qvie{Dukjx2n>NjAa#MD*gFs? z1u%Ua-+IO5U;@M9TgIVxrKNOtwwSsj*wLptaqD?-+pBet;n!tH?|j8-%_@wijk7*z z<4Z4R^%hs$6y!vlUC34{@?eSeu@!?>Cq4Ny6}Q*DVrD0(H8@N=EtQ;Utl&7JNjJhQ z$he-g{QOIrdCmN>1H44TIB3nQ0-G?pQmM_TAlsY zMd<23wJ(ydTgPX}5WW`o(e0Vv%~H-?l-zUmsTcp56`2-4FZa_dxmAB(1?2M4Ddi&C z3dh#JSp0EJ&NOndj7YuzAXDO5owL-q+;87X*Q2KqTM_+qzoLH3^*=GJj_jGd@YQ@9 zc7DHAmeAg5X{`VGPN`R8gHfC`eDC#H;w9{O4&@>)D;W6QPOlzYyFi(q>Io$(`nleA zv4w$@uSMT7tLIdWl@hX|BC~@<%#$88cV#%wh}JqXh2$?}x>mY=Rg<;wLVsEpr?YKo zibWQQm?idKitcvlTRt?LUd-Ms+V}ieQFCzM=C?P0!-^}D?^Fxq&A?`L{0D#Bt&fE# zu(C#1V^Fe99OXKiJ4%=6Fxe(%`^lTyDfo16TOO4y>~D^9^{lq7ZP##_Y!c_jZnPfO zx^P@6Y271xW9#FyIkR|FwQ5r`=vTE6Tu8n-w&GSnd>`{Pf4IkNfYZ+?XtTPcr{5eVq-S01lKQA;tNe*KR2e_!o?%RQFT|D^jr-TjYU{7)JFrwsq! blwpVVLqx5N0F=551pH~J-ctFjgbw(Bc`g}9 literal 0 HcmV?d00001 diff --git a/rustmail_panel/tailwind.config.js b/rustmail_panel/tailwind.config.js new file mode 100644 index 0000000..b86ae22 --- /dev/null +++ b/rustmail_panel/tailwind.config.js @@ -0,0 +1,14 @@ +module.exports = { + mode: "jit", + content: { + files: ["src/**/*.rs", "index.html", "assets/**/*.png"], + }, + darkMode: "media", + theme: { + extend: {}, + }, + variants: { + extend: {}, + }, + plugins: [], +}; \ No newline at end of file From e6b1875ea7a330ff62566c1dc7a28dec206686d0 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 14 Oct 2025 22:06:25 +0200 Subject: [PATCH 005/100] fix(panel): fixed yew page router --- Cargo.lock | 1124 +++++++++++++++++++++------------- rustmail_panel/Cargo.toml | 2 +- rustmail_panel/src/main.rs | 9 +- rustmail_panel/src/router.rs | 27 +- 4 files changed, 731 insertions(+), 431 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7072b2d..4fc526d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,20 +2,11 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -41,6 +32,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + [[package]] name = "arrayvec" version = "0.7.6" @@ -58,7 +55,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -78,9 +75,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" @@ -134,21 +131,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base64" version = "0.21.7" @@ -163,9 +145,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bincode" @@ -184,9 +166,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "serde", ] @@ -200,11 +182,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "boolinator" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" + [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytecount" @@ -226,11 +214,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" -version = "1.1.9" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -257,18 +245,19 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.24" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" @@ -281,7 +270,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -362,9 +351,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -436,12 +425,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -464,7 +453,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -499,12 +488,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.12" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -529,9 +518,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -544,11 +533,17 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide", @@ -594,9 +589,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -667,7 +662,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -711,9 +706,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -728,51 +723,77 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "glob" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] -name = "glob" -version = "0.3.2" +name = "gloo" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console 0.2.3", + "gloo-dialogs 0.1.1", + "gloo-events 0.1.2", + "gloo-file 0.2.3", + "gloo-history 0.1.5", + "gloo-net 0.3.1", + "gloo-render 0.1.1", + "gloo-storage 0.2.2", + "gloo-timers 0.2.6", + "gloo-utils 0.1.7", + "gloo-worker 0.2.1", +] [[package]] name = "gloo" -version = "0.11.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +checksum = "cd35526c28cc55c1db77aed6296de58677dbab863b118483a27845631d870249" dependencies = [ - "gloo-console", - "gloo-dialogs", - "gloo-events", - "gloo-file", - "gloo-history", - "gloo-net", - "gloo-render", - "gloo-storage", - "gloo-timers", - "gloo-utils", - "gloo-worker", + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.4.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.4.0", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -781,13 +802,23 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a17868f56b4a24f677b17c8cb69958385102fa879418052d60b50bc1727e261" dependencies = [ - "gloo-utils", + "gloo-utils 0.2.0", "js-sys", "serde", "wasm-bindgen", "web-sys", ] +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-dialogs" version = "0.2.0" @@ -798,6 +829,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-events" version = "0.2.0" @@ -808,18 +849,47 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events 0.1.2", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-file" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97563d71863fb2824b2e974e754a81d19c4a7ec47b09ced8a0e6656b6d54bd1f" dependencies = [ - "gloo-events", + "futures-channel", + "gloo-events 0.2.0", "js-sys", "wasm-bindgen", "web-sys", ] +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events 0.1.2", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_urlencoded", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-history" version = "0.2.2" @@ -827,10 +897,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "903f432be5ba34427eac5e16048ef65604a82061fe93789f2212afc73d8617d6" dependencies = [ "getrandom 0.2.16", - "gloo-events", - "gloo-utils", + "gloo-events 0.2.0", + "gloo-utils 0.2.0", "serde", - "serde-wasm-bindgen", + "serde-wasm-bindgen 0.6.5", "serde_urlencoded", "thiserror 1.0.69", "wasm-bindgen", @@ -839,14 +909,14 @@ dependencies = [ [[package]] name = "gloo-net" -version = "0.5.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" dependencies = [ "futures-channel", "futures-core", "futures-sink", - "gloo-utils", + "gloo-utils 0.1.7", "http 0.2.12", "js-sys", "pin-project", @@ -858,6 +928,37 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-net" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac9e8288ae2c632fa9f8657ac70bfe38a1530f345282d7ba66a1f70b72b7dc4" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-render" version = "0.2.0" @@ -868,13 +969,28 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-storage" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc8031e8c92758af912f9bc08fbbadd3c6f3cfcbf6b64cdf3d6a81f0139277a" dependencies = [ - "gloo-utils", + "gloo-utils 0.2.0", "js-sys", "serde", "serde_json", @@ -883,16 +999,41 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gloo-timers" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" dependencies = [ + "futures-channel", + "futures-core", "js-sys", "wasm-bindgen", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gloo-utils" version = "0.2.0" @@ -908,13 +1049,30 @@ dependencies = [ [[package]] name = "gloo-worker" -version = "0.5.0" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console 0.2.3", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +checksum = "76495d3dd87de51da268fa3a593da118ab43eb7f8809e17eb38d3319b424e400" dependencies = [ "bincode", "futures", - "gloo-utils", + "gloo-utils 0.2.0", "gloo-worker-macros", "js-sys", "pinned", @@ -934,14 +1092,14 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes", "fnv", @@ -958,9 +1116,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -983,22 +1141,28 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.3", + "hashbrown 0.15.5", ] [[package]] @@ -1124,14 +1288,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.9", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1148,7 +1312,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.10", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -1177,17 +1341,17 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.3.1", "hyper 1.7.0", "hyper-util", - "rustls 0.23.27", + "rustls 0.23.32", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.4", "tower-service", ] @@ -1209,9 +1373,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", @@ -1225,7 +1389,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.9", + "socket2 0.6.1", "system-configuration 0.6.1", "tokio", "tower-service", @@ -1235,9 +1399,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1345,9 +1509,9 @@ dependencies = [ [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1366,9 +1530,9 @@ dependencies = [ [[package]] name = "implicit-clone" -version = "0.5.1" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88e4c5423e5b38b6f175b42561a08f6ddbc8ed3a3275f48eb350ba2dfe9a6b60" +checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" dependencies = [ "implicit-clone-derive", "indexmap", @@ -1381,7 +1545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -1391,18 +1555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.3", -] - -[[package]] -name = "io-uring" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", + "hashbrown 0.16.0", ] [[package]] @@ -1429,9 +1582,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -1454,9 +1607,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libm" @@ -1464,6 +1617,17 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.9.4", + "libc", + "redox_syscall", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -1477,9 +1641,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -1489,19 +1653,18 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "matchit" @@ -1521,9 +1684,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -1558,11 +1721,12 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1572,7 +1736,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.59.0", ] @@ -1656,15 +1820,6 @@ dependencies = [ "libc", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -1677,7 +1832,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -1694,7 +1849,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -1723,9 +1878,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -1733,15 +1888,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -1755,9 +1910,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project" @@ -1776,7 +1931,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -1831,9 +1986,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -1855,12 +2010,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.34" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -1899,38 +2054,55 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] +[[package]] +name = "prokio" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488" +dependencies = [ + "futures", + "gloo 0.8.1", + "num_cpus", + "once_cell", + "pin-project", + "pinned", + "tokio", + "tokio-stream", + "wasm-bindgen-futures", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "memchr", "unicase", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" @@ -1964,18 +2136,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -1985,9 +2157,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -1996,9 +2168,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" @@ -2011,7 +2183,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -2046,20 +2218,20 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", - "h2 0.4.10", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.7.0", - "hyper-rustls 0.27.6", + "hyper-rustls 0.27.7", "hyper-tls", "hyper-util", "js-sys", @@ -2098,6 +2270,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "route-recognizer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" + [[package]] name = "rsa" version = "0.9.8" @@ -2138,7 +2316,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.101", + "syn 2.0.106", "walkdir", ] @@ -2152,23 +2330,17 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2199,13 +2371,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.27" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.3", + "rustls-webpki 0.103.7", "subtle", "zeroize", ] @@ -2251,9 +2423,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "ring", "rustls-pki-types", @@ -2270,7 +2442,7 @@ dependencies = [ "hyper 1.7.0", "mime_guess", "regex", - "reqwest 0.12.23", + "reqwest 0.12.24", "rust-embed", "serde", "serenity", @@ -2285,13 +2457,14 @@ name = "rustmail_panel" version = "0.1.0" dependencies = [ "yew", + "yew-router", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2310,11 +2483,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2349,7 +2522,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation", "core-foundation-sys", "libc", @@ -2358,9 +2531,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -2368,11 +2541,12 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -2385,6 +2559,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -2422,19 +2607,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -2450,9 +2636,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -2478,7 +2664,7 @@ dependencies = [ "arrayvec", "async-trait", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes", "chrono", "command_attr", @@ -2553,6 +2739,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "skeptic" version = "0.13.7" @@ -2570,27 +2762,24 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2598,12 +2787,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2654,7 +2843,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "hashlink", "indexmap", "log", @@ -2665,7 +2854,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -2682,7 +2871,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -2705,7 +2894,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.101", + "syn 2.0.106", "tokio", "url", ] @@ -2718,7 +2907,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.9.4", "byteorder", "bytes", "crc", @@ -2747,7 +2936,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "whoami", ] @@ -2760,7 +2949,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.1", + "bitflags 2.9.4", "byteorder", "crc", "dotenvy", @@ -2784,7 +2973,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "whoami", ] @@ -2808,16 +2997,16 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "url", ] [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -2855,9 +3044,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -2887,7 +3076,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -2907,7 +3096,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "core-foundation", "system-configuration-sys 0.6.0", ] @@ -2940,15 +3129,15 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2962,11 +3151,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -2977,25 +3166,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3008,15 +3197,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3034,9 +3223,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -3049,32 +3238,29 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.6.0", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -3110,11 +3296,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.27", + "rustls 0.23.32", "tokio", ] @@ -3147,9 +3333,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -3158,33 +3344,16 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokise" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "decf97738ce15b9e9cc1671ea29b0f6c56538719e1a092d19cc2134bf144e40e" -dependencies = [ - "futures", - "gloo", - "num_cpus", - "once_cell", - "pin-project", - "pinned", - "tokio", - "tokio-stream", - "wasm-bindgen-futures", -] - [[package]] name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 0.7.2", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow 0.7.13", @@ -3198,9 +3367,9 @@ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -3218,18 +3387,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow 0.7.13", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tower" @@ -3253,7 +3422,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.4", "bytes", "futures-util", "http 1.3.1", @@ -3291,29 +3460,29 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] [[package]] name = "triomphe" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" [[package]] name = "try-lock" @@ -3350,9 +3519,9 @@ checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "typesize" @@ -3380,7 +3549,7 @@ checksum = "536b6812192bda8551cfa0e52524e328c6a951b48e66529ee4522d6c721243d6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -3397,9 +3566,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" @@ -3424,9 +3593,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -3434,6 +3603,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -3452,7 +3627,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -3496,17 +3671,17 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -3517,35 +3692,36 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -3556,9 +3732,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3566,22 +3742,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -3601,9 +3777,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -3621,93 +3797,93 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.0", + "webpki-roots 1.0.3", ] [[package]] name = "webpki-roots" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "redox_syscall", + "libredox", "wasite", ] [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.1", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link 0.1.1", - "windows-result", - "windows-strings", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -3716,7 +3892,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -3725,7 +3910,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -3755,6 +3949,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3779,13 +3991,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3798,6 +4027,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -3810,6 +4045,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -3822,12 +4063,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -3840,6 +4093,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -3852,6 +4111,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -3864,6 +4129,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3876,6 +4147,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.5.40" @@ -3902,13 +4179,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -3919,20 +4193,21 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yew" version = "0.21.0" -source = "git+https://github.com/yewstack/yew/#85050f50fb4d211f5a96a814e175467ca87681b3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac" dependencies = [ "console_error_panic_hook", "futures", - "gloo", + "gloo 0.10.0", "implicit-clone", "indexmap", "js-sys", + "prokio", "rustversion", "serde", "slab", - "thiserror 2.0.12", + "thiserror 1.0.69", "tokio", - "tokise", "tracing", "wasm-bindgen", "wasm-bindgen-futures", @@ -3943,15 +4218,46 @@ dependencies = [ [[package]] name = "yew-macro" version = "0.21.0" -source = "git+https://github.com/yewstack/yew/#85050f50fb4d211f5a96a814e175467ca87681b3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fd8ca5166d69e59f796500a2ce432ff751edecbbb308ca59fd3fe4d0343de2" dependencies = [ + "boolinator", "once_cell", "prettyplease", "proc-macro-error", "proc-macro2", "quote", - "rustversion", - "syn 2.0.101", + "syn 2.0.106", +] + +[[package]] +name = "yew-router" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca1d5052c96e6762b4d6209a8aded597758d442e6c479995faf0c7b5538e0c6" +dependencies = [ + "gloo 0.10.0", + "js-sys", + "route-recognizer", + "serde", + "serde_urlencoded", + "tracing", + "urlencoding", + "wasm-bindgen", + "web-sys", + "yew", + "yew-router-macro", +] + +[[package]] +name = "yew-router-macro" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bfd190a07ca8cfde7cd4c52b3ac463803dc07323db8c34daa697e86365978c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -3974,28 +4280,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.25" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -4015,15 +4321,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" @@ -4038,9 +4344,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -4055,5 +4361,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index 1330322..ca140ac 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -5,5 +5,5 @@ edition = "2024" license = "MIT" [dependencies] -yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] } +yew = { version = "0.21", features = ["csr"] } yew-router = "0.18.0" diff --git a/rustmail_panel/src/main.rs b/rustmail_panel/src/main.rs index f95038c..9ddb54b 100644 --- a/rustmail_panel/src/main.rs +++ b/rustmail_panel/src/main.rs @@ -2,13 +2,16 @@ mod components; mod pages; mod router; -use crate::components::home::Home; use yew::prelude::*; +use yew_router::prelude::*; +use crate::router::{switch, Route}; -#[component] +#[function_component] fn App() -> Html { html! { - + + render={switch} /> + } } diff --git a/rustmail_panel/src/router.rs b/rustmail_panel/src/router.rs index 9d96c0b..8f1eae1 100644 --- a/rustmail_panel/src/router.rs +++ b/rustmail_panel/src/router.rs @@ -1,32 +1,23 @@ +use yew_router::prelude::*; +use yew::prelude::*; use crate::pages::docs::Docs; use crate::pages::home::Home; -use yew::prelude::*; -use yew_router::prelude::*; -#[derive(Routable, PartialEq, Eq, Clone, Debug)] +#[derive(Clone, Routable, PartialEq)] pub enum Route { #[at("/")] Home, #[at("/docs")] - Docs, + Documentation, #[not_found] #[at("/404")] NotFound, } -#[function_component(App)] -pub fn app() -> Html { - html! { - - render={Switch::render(switch)} /> - - } -} - -fn switch(route: Route) -> Html { - match route { +pub fn switch(routes: Route) -> Html { + match routes { Route::Home => html! { }, - Route::Docs => html! { }, - Route::NotFound => html! {

{"Page not found"}

}, + Route::Documentation => html! { }, + Route::NotFound => html! {

{ "404" }

}, } -} +} \ No newline at end of file From ee51645b7b65fcbfc7e1231711095a7a8f06e477 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Wed, 15 Oct 2025 15:11:41 +0200 Subject: [PATCH 006/100] feat(auth): add discord auth for /panel route --- Cargo.lock | 825 +++++++++--------- Cargo.toml | 2 +- rustmail/Cargo.toml | 7 +- rustmail/src/api/handler/auth/callback.rs | 80 ++ rustmail/src/api/handler/auth/login.rs | 26 + rustmail/src/api/handler/auth/mod.rs | 2 + rustmail/src/api/handler/bot/start.rs | 6 +- rustmail/src/api/handler/mod.rs | 2 + rustmail/src/api/handler/panel/mod.rs | 1 + rustmail/src/api/handler/panel/panel.rs | 9 + rustmail/src/api/middleware/auth.rs | 80 ++ rustmail/src/api/middleware/mod.rs | 1 + rustmail/src/api/mod.rs | 1 + rustmail/src/api/router.rs | 10 +- rustmail/src/api/routes/auth.rs | 15 + rustmail/src/api/routes/bot.rs | 4 +- rustmail/src/api/routes/mod.rs | 2 + rustmail/src/api/routes/panel.rs | 19 + rustmail/src/bot.rs | 64 +- rustmail/src/commands/edit/common.rs | 4 +- rustmail/src/config.rs | 2 + .../src/handlers/guild_messages_handler.rs | 2 +- rustmail/src/main.rs | 20 +- rustmail/src/panel_commands/mod.rs | 1 + rustmail/src/panel_commands/user/is_member.rs | 10 + rustmail/src/panel_commands/user/mod.rs | 1 + rustmail/src/utils/command/wrap_command.rs | 4 +- rustmail_panel/src/pages/home.rs | 18 +- rustmail_panel/src/pages/mod.rs | 1 + rustmail_panel/src/pages/panel.rs | 14 + rustmail_panel/src/router.rs | 10 +- 31 files changed, 780 insertions(+), 463 deletions(-) create mode 100644 rustmail/src/api/handler/auth/callback.rs create mode 100644 rustmail/src/api/handler/auth/login.rs create mode 100644 rustmail/src/api/handler/auth/mod.rs create mode 100644 rustmail/src/api/handler/panel/mod.rs create mode 100644 rustmail/src/api/handler/panel/panel.rs create mode 100644 rustmail/src/api/middleware/auth.rs create mode 100644 rustmail/src/api/middleware/mod.rs create mode 100644 rustmail/src/api/routes/auth.rs create mode 100644 rustmail/src/api/routes/panel.rs create mode 100644 rustmail/src/panel_commands/mod.rs create mode 100644 rustmail/src/panel_commands/user/is_member.rs create mode 100644 rustmail/src/panel_commands/user/mod.rs create mode 100644 rustmail_panel/src/pages/panel.rs diff --git a/Cargo.lock b/Cargo.lock index 4fc526d..4c1a65f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,20 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" @@ -55,7 +64,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -75,9 +84,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" @@ -131,6 +140,44 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base64" version = "0.21.7" @@ -145,9 +192,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "bincode" @@ -166,9 +213,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde", ] @@ -190,9 +237,9 @@ checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytecount" @@ -214,11 +261,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" -version = "1.2.1" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ - "serde_core", + "serde", ] [[package]] @@ -245,19 +292,18 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.41" +version = "1.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" dependencies = [ - "find-msvc-tools", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" @@ -270,7 +316,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link 0.2.0", ] [[package]] @@ -309,6 +355,17 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -351,9 +408,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -425,12 +482,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.4" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", - "serde_core", + "serde", ] [[package]] @@ -453,7 +510,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -488,12 +545,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.14" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -518,9 +575,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -533,17 +590,11 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", "miniz_oxide", @@ -589,9 +640,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -662,7 +713,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -706,9 +757,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -723,27 +774,33 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", - "wasip2", + "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "glob" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "gloo" @@ -1092,14 +1149,14 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] name = "h2" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1116,9 +1173,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", @@ -1141,28 +1198,22 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - [[package]] name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.15.3", ] [[package]] @@ -1288,14 +1339,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.27", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.5.9", "tokio", "tower-service", "tracing", @@ -1312,7 +1363,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.12", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -1341,17 +1392,17 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ "http 1.3.1", "hyper 1.7.0", "hyper-util", - "rustls 0.23.32", + "rustls 0.23.27", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.4", + "tokio-rustls 0.26.2", "tower-service", ] @@ -1373,9 +1424,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ "base64 0.22.1", "bytes", @@ -1389,7 +1440,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.9", "system-configuration 0.6.1", "tokio", "tower-service", @@ -1399,9 +1450,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1509,9 +1560,9 @@ dependencies = [ [[package]] name = "idna" -version = "1.1.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", @@ -1545,7 +1596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "699c1b6d335e63d0ba5c1e1c7f647371ce989c3bcbe1f7ed2b85fa56e3bd1a21" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -1555,7 +1606,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.15.3", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", ] [[package]] @@ -1582,9 +1644,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -1607,9 +1669,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" @@ -1617,17 +1679,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "libredox" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" -dependencies = [ - "bitflags 2.9.4", - "libc", - "redox_syscall", -] - [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -1641,9 +1692,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -1653,18 +1704,19 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matchit" @@ -1684,9 +1736,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -1721,12 +1773,11 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", - "simd-adler32", ] [[package]] @@ -1736,7 +1787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1820,6 +1871,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1832,7 +1892,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -1849,7 +1909,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -1878,9 +1938,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.5" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1888,15 +1948,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.12" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-targets 0.52.6", ] [[package]] @@ -1910,9 +1970,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" @@ -1931,7 +1991,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -1986,9 +2046,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "zerovec", ] @@ -2010,12 +2070,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.37" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -2054,9 +2114,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -2084,25 +2144,25 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "memchr", "unicase", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" @@ -2136,18 +2196,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.18" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -2157,9 +2217,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", @@ -2168,9 +2228,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" @@ -2183,7 +2243,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.27", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -2218,20 +2278,20 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", "futures-core", - "h2 0.4.12", + "h2 0.4.10", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.7.0", - "hyper-rustls 0.27.7", + "hyper-rustls 0.27.6", "hyper-tls", "hyper-util", "js-sys", @@ -2316,7 +2376,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.106", + "syn 2.0.101", "walkdir", ] @@ -2330,17 +2390,23 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustix" -version = "1.1.2" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2371,13 +2437,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki 0.103.3", "subtle", "zeroize", ] @@ -2423,9 +2489,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -2438,17 +2504,20 @@ version = "0.1.1" dependencies = [ "async-trait", "axum", + "axum-extra", "chrono", "hyper 1.7.0", "mime_guess", "regex", - "reqwest 0.12.24", + "reqwest 0.12.23", "rust-embed", "serde", + "serde_json", "serenity", "sqlx", "tokio", "toml", + "urlencoding", "uuid", ] @@ -2462,9 +2531,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -2483,11 +2552,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2522,7 +2591,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -2531,9 +2600,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2541,12 +2610,11 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", - "serde_core", ] [[package]] @@ -2607,7 +2675,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -2636,9 +2704,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" dependencies = [ "serde_core", ] @@ -2664,7 +2732,7 @@ dependencies = [ "arrayvec", "async-trait", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.9.1", "bytes", "chrono", "command_attr", @@ -2739,12 +2807,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - [[package]] name = "skeptic" version = "0.13.7" @@ -2762,24 +2824,27 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" dependencies = [ "serde", ] [[package]] name = "socket2" -version = "0.5.10" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2787,12 +2852,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2843,7 +2908,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.5", + "hashbrown 0.15.3", "hashlink", "indexmap", "log", @@ -2854,7 +2919,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.12", "tokio", "tokio-stream", "tracing", @@ -2871,7 +2936,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -2894,7 +2959,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.106", + "syn 2.0.101", "tokio", "url", ] @@ -2907,7 +2972,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.9.1", "byteorder", "bytes", "crc", @@ -2936,7 +3001,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.12", "tracing", "whoami", ] @@ -2949,7 +3014,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.9.1", "byteorder", "crc", "dotenvy", @@ -2973,7 +3038,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.17", + "thiserror 2.0.12", "tracing", "whoami", ] @@ -2997,16 +3062,16 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.17", + "thiserror 2.0.12", "tracing", "url", ] [[package]] name = "stable_deref_trait" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "static_assertions" @@ -3044,9 +3109,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -3076,7 +3141,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -3096,7 +3161,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "core-foundation", "system-configuration-sys 0.6.0", ] @@ -3129,15 +3194,15 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.23.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3151,11 +3216,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.12", ] [[package]] @@ -3166,25 +3231,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -3197,15 +3262,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -3223,9 +3288,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -3238,29 +3303,32 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ + "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -3296,11 +3364,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.32", + "rustls 0.23.27", "tokio", ] @@ -3333,9 +3401,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -3346,14 +3414,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 0.7.3", + "toml_datetime 0.7.2", "toml_parser", "toml_writer", "winnow 0.7.13", @@ -3367,9 +3435,9 @@ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" dependencies = [ "serde_core", ] @@ -3387,18 +3455,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" dependencies = [ "winnow 0.7.13", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" [[package]] name = "tower" @@ -3422,7 +3490,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "bytes", "futures-util", "http 1.3.1", @@ -3460,29 +3528,29 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "triomphe" -version = "0.1.15" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" [[package]] name = "try-lock" @@ -3519,9 +3587,9 @@ checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" [[package]] name = "typenum" -version = "1.19.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "typesize" @@ -3549,7 +3617,7 @@ checksum = "536b6812192bda8551cfa0e52524e328c6a951b48e66529ee4522d6c721243d6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -3566,9 +3634,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" @@ -3593,9 +3661,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -3627,7 +3695,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.3.3", "js-sys", "wasm-bindgen", ] @@ -3671,17 +3739,17 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasi" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ - "wit-bindgen", + "wit-bindgen-rt", ] [[package]] @@ -3692,36 +3760,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", - "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -3732,9 +3799,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3742,22 +3809,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] @@ -3777,9 +3844,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -3797,93 +3864,93 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.3", + "webpki-roots 1.0.0", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" -version = "1.6.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ - "libredox", + "redox_syscall", "wasite", ] [[package]] name = "winapi-util" -version = "0.1.11" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] name = "windows-core" -version = "0.62.2" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-link 0.1.1", + "windows-result", + "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.2" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] name = "windows-interface" -version = "0.59.3" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-link" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.1.1", + "windows-result", + "windows-strings", ] [[package]] @@ -3892,16 +3959,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", + "windows-link 0.1.1", ] [[package]] @@ -3910,16 +3968,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", + "windows-link 0.1.1", ] [[package]] @@ -3949,24 +3998,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -3991,30 +4022,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4027,12 +4041,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4045,12 +4053,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4063,24 +4065,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4093,12 +4083,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4111,12 +4095,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4129,12 +4107,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4147,12 +4119,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" version = "0.5.40" @@ -4179,10 +4145,13 @@ dependencies = [ ] [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] [[package]] name = "writeable" @@ -4227,7 +4196,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -4257,7 +4226,7 @@ checksum = "42bfd190a07ca8cfde7cd4c52b3ac463803dc07323db8c34daa697e86365978c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -4280,28 +4249,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] [[package]] @@ -4321,15 +4290,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerotrie" @@ -4344,9 +4313,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -4361,5 +4330,5 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.101", ] diff --git a/Cargo.toml b/Cargo.toml index 1549bee..d03bbdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] members = ["rustmail", "rustmail_panel"] -resolver = "3" \ No newline at end of file +resolver = "3" diff --git a/rustmail/Cargo.toml b/rustmail/Cargo.toml index 4de916d..2c4ae97 100644 --- a/rustmail/Cargo.toml +++ b/rustmail/Cargo.toml @@ -10,15 +10,18 @@ serenity = "0.12.4" sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "macros", "migrate"] } tokio = { version = "1.47.1", features = ["rt-multi-thread", "signal"] } toml = "0.9.7" -reqwest = "0.12.23" +reqwest = { version = "0.12.23", features = ["json"] } async-trait = "0.1.89" chrono = { version = "0.4.42", features = ["serde"] } regex = "1.11.3" axum = "0.8.6" +axum-extra = { version = "0.10.3", features = ["cookie"] } hyper = "1.7.0" rust-embed = "8.7.2" mime_guess = "2.0.5" +urlencoding = "2.1.3" +serde_json = "1.0.145" [dependencies.uuid] version = "1.18.1" -features = ["v4"] \ No newline at end of file +features = ["v4"] diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs new file mode 100644 index 0000000..249c774 --- /dev/null +++ b/rustmail/src/api/handler/auth/callback.rs @@ -0,0 +1,80 @@ +use crate::BotState; +use axum::extract::{Query, State}; +use axum::response::Redirect; +use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; +use reqwest::Client; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(serde::Deserialize)] +pub struct AuthRequest { + pub code: String, +} + +#[derive(serde::Deserialize, Debug)] +struct DiscordUser { + id: String, + username: String, + discriminator: String, + avatar: Option, +} + +pub async fn handle_callback( + State(bot_state): State>>, + jar: CookieJar, + Query(params): Query, +) -> (CookieJar, Redirect) { + let state_lock = bot_state.lock().await; + let Some(config) = &state_lock.config else { + return (jar, Redirect::to("/error?message=Missing+configuration")); + }; + + let client = Client::new(); + + let client_id = config.bot.client_id.to_string(); + let client_secret = config.bot.client_secret.clone(); + let code = params.code.clone(); + + let token_response = client + .post("https://discord.com/api/oauth2/token") + .form(&[ + ("client_id", client_id.as_str()), + ("client_secret", client_secret.as_str()), + ("grant_type", "authorization_code"), + ("code", code.as_str()), + ("redirect_uri", "http://localhost:3002/api/auth/callback"), + ]) + .send() + .await + .unwrap(); + + let token_data: serde_json::Value = token_response.json().await.unwrap(); + let access_token = token_data["access_token"].as_str().unwrap(); + + let client_response = client + .get("https://discord.com/api/users/@me") + .bearer_auth(access_token) + .send() + .await + .unwrap(); + + let user: DiscordUser = client_response.json().await.unwrap(); + let user_id = user.id.clone(); + println!("Authenticated user: {:?}", user); + + let cookie_token = Cookie::build(("session_token", access_token.to_string())) + .path("/") + .http_only(true) + .same_site(SameSite::Lax) + .build(); + + let cookie_user = Cookie::build(("user_id", user_id.clone())) + .path("/") + .http_only(false) + .same_site(SameSite::Lax) + .build(); + + let jar = jar.add(cookie_token).add(cookie_user); + + (jar, Redirect::to("/panel")) +} diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs new file mode 100644 index 0000000..deac770 --- /dev/null +++ b/rustmail/src/api/handler/auth/login.rs @@ -0,0 +1,26 @@ +use crate::BotState; +use axum::extract::State; +use axum::response::Redirect; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub async fn handle_login(State(bot_state): State>>) -> Redirect { + println!("Yo"); + let state_lock = bot_state.lock().await; + + println!("Handling login request"); + + let Some(config) = &state_lock.config else { + return Redirect::to("/error?message=Missing+configuration"); + }; + + let client_id = config.bot.client_id; + + let url = format!( + "https://discord.com/oauth2/authorize?client_id={}&redirect_uri={}&response_type=code&scope=identify%20guilds", + client_id, + urlencoding::encode("http://localhost:3002/api/auth/callback") + ); + + Redirect::to(&url) +} diff --git a/rustmail/src/api/handler/auth/mod.rs b/rustmail/src/api/handler/auth/mod.rs new file mode 100644 index 0000000..d1702fb --- /dev/null +++ b/rustmail/src/api/handler/auth/mod.rs @@ -0,0 +1,2 @@ +pub mod callback; +pub mod login; diff --git a/rustmail/src/api/handler/bot/start.rs b/rustmail/src/api/handler/bot/start.rs index ca55fad..3e29854 100644 --- a/rustmail/src/api/handler/bot/start.rs +++ b/rustmail/src/api/handler/bot/start.rs @@ -1,8 +1,8 @@ use crate::bot::run_bot; use crate::{BotState, BotStatus}; -use axum::Json; use axum::extract::State; use axum::http::StatusCode; +use axum::Json; use std::sync::Arc; use tokio::spawn; use tokio::sync::Mutex; @@ -19,10 +19,11 @@ pub async fn handle_start_bot( } let (shutdown_tx, mut shutdown_rx) = tokio::sync::watch::channel(false); + let (command_tx, command_rx) = tokio::sync::mpsc::channel(32); let bot_state_clone = bot_state.clone(); let handle = spawn(async move { - run_bot(bot_state_clone.clone(), &mut shutdown_rx).await; + run_bot(bot_state_clone.clone(), &mut shutdown_rx, command_rx).await; let mut s = bot_state_clone.lock().await; s.status = BotStatus::Stopped; }); @@ -30,6 +31,7 @@ pub async fn handle_start_bot( handle, shutdown: shutdown_tx, }; + state_lock.command_tx = command_tx; drop(state_lock); (StatusCode::OK, Json("Bot is starting")) diff --git a/rustmail/src/api/handler/mod.rs b/rustmail/src/api/handler/mod.rs index 1cbfd96..9edd1e1 100644 --- a/rustmail/src/api/handler/mod.rs +++ b/rustmail/src/api/handler/mod.rs @@ -1 +1,3 @@ +pub mod auth; pub mod bot; +pub mod panel; diff --git a/rustmail/src/api/handler/panel/mod.rs b/rustmail/src/api/handler/panel/mod.rs new file mode 100644 index 0000000..7a6b958 --- /dev/null +++ b/rustmail/src/api/handler/panel/mod.rs @@ -0,0 +1 @@ +pub mod panel; diff --git a/rustmail/src/api/handler/panel/panel.rs b/rustmail/src/api/handler/panel/panel.rs new file mode 100644 index 0000000..ee45954 --- /dev/null +++ b/rustmail/src/api/handler/panel/panel.rs @@ -0,0 +1,9 @@ +use crate::BotState; +use axum::extract::State; +use axum::response::Redirect; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub async fn handle_panel(State(bot_state): State>>) -> Redirect { + Redirect::to("/panel") +} diff --git a/rustmail/src/api/middleware/auth.rs b/rustmail/src/api/middleware/auth.rs new file mode 100644 index 0000000..60bd5e2 --- /dev/null +++ b/rustmail/src/api/middleware/auth.rs @@ -0,0 +1,80 @@ +use crate::{BotCommand, BotState}; +use axum::extract::Request; +use axum::extract::State; +use axum::middleware::Next; +use axum::response::{IntoResponse, Redirect, Response}; +use axum_extra::extract::CookieJar; +use std::sync::Arc; +use tokio::sync::Mutex; + +async fn check_user_with_bot(bot_state: Arc>, user_id: &str) -> bool { + let user_id_num = match user_id.parse::() { + Ok(id) => id, + Err(_) => return false, + }; + + let cmd_tx = { + let state_lock = bot_state.lock().await; + state_lock.command_tx.clone() + }; + + let (resp_tx, resp_rx) = tokio::sync::oneshot::channel(); + + if cmd_tx + .send(BotCommand::CheckUserIsMember { + user_id: user_id_num, + resp: resp_tx, + }) + .await + .is_err() + { + return false; + } + + let test = resp_rx.await.unwrap(); + println!("User is member: {}", test); + + test +} + +async fn check_user_with_api(token: &str) -> bool { + false +} + +async fn verify_token(token: &str, user_id: &str, bot_state: Arc>) -> bool { + let state_lock = bot_state.lock().await; + + let bot_http = state_lock.bot_http.clone(); + drop(state_lock); + + if let Some(http) = &bot_http { + return check_user_with_bot(bot_state, user_id).await; + } + + check_user_with_api(token).await +} + +pub async fn auth_middleware( + State(bot_state): State>>, + jar: CookieJar, + req: Request, + next: Next, +) -> Response { + let token_cookie = jar.get("session_token"); + let user_cookie = jar.get("user_id"); + + if token_cookie.is_none() || user_cookie.is_none() { + return Redirect::to("/login").into_response(); + } + + let token = token_cookie.unwrap().value().to_string(); + let user_id = user_cookie.unwrap().value().to_string(); + + let is_valid = verify_token(token.as_str(), user_id.as_str(), bot_state).await; + + if !is_valid { + return Redirect::to("/login").into_response(); + } + + next.run(req).await +} diff --git a/rustmail/src/api/middleware/mod.rs b/rustmail/src/api/middleware/mod.rs new file mode 100644 index 0000000..0e4a05d --- /dev/null +++ b/rustmail/src/api/middleware/mod.rs @@ -0,0 +1 @@ +pub mod auth; diff --git a/rustmail/src/api/mod.rs b/rustmail/src/api/mod.rs index 317fc3e..1f91172 100644 --- a/rustmail/src/api/mod.rs +++ b/rustmail/src/api/mod.rs @@ -1,3 +1,4 @@ pub mod handler; +pub mod middleware; pub mod router; pub mod routes; diff --git a/rustmail/src/api/router.rs b/rustmail/src/api/router.rs index 480e826..7eb1e2e 100644 --- a/rustmail/src/api/router.rs +++ b/rustmail/src/api/router.rs @@ -1,14 +1,20 @@ -use crate::BotState; +use crate::api::routes::auth::create_auth_router; use crate::api::routes::bot::create_bot_router; +use crate::api::routes::panel::create_panel_router; +use crate::BotState; use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; pub fn create_api_router(bot_state: Arc>) -> Router { let bot_router = create_bot_router(); + let auth_router = create_auth_router(); + let panel_router = create_panel_router(bot_state.clone()); let app = Router::new() - .nest("/rustmail", bot_router) + .nest("/api/bot", bot_router) + .nest("/api/auth", auth_router) + .nest("/api/panel", panel_router) .with_state(bot_state.clone()); app diff --git a/rustmail/src/api/routes/auth.rs b/rustmail/src/api/routes/auth.rs new file mode 100644 index 0000000..29b8720 --- /dev/null +++ b/rustmail/src/api/routes/auth.rs @@ -0,0 +1,15 @@ +use crate::api::handler::auth::callback::handle_callback; +use crate::api::handler::auth::login::handle_login; +use crate::BotState; +use axum::routing::get; +use axum::Router; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub fn create_auth_router() -> Router>> { + let auth_router = Router::new() + .route("/login", get(handle_login)) + .route("/callback", get(handle_callback)); + + auth_router +} diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index 817acd4..e1a68dc 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -1,8 +1,8 @@ -use crate::BotState; use crate::api::handler::bot::start::handle_start_bot; use crate::api::handler::bot::stop::handle_stop_bot; -use axum::Router; +use crate::BotState; use axum::routing::post; +use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/mod.rs b/rustmail/src/api/routes/mod.rs index 1cbfd96..9edd1e1 100644 --- a/rustmail/src/api/routes/mod.rs +++ b/rustmail/src/api/routes/mod.rs @@ -1 +1,3 @@ +pub mod auth; pub mod bot; +pub mod panel; diff --git a/rustmail/src/api/routes/panel.rs b/rustmail/src/api/routes/panel.rs new file mode 100644 index 0000000..90db6e5 --- /dev/null +++ b/rustmail/src/api/routes/panel.rs @@ -0,0 +1,19 @@ +use crate::api::handler::panel::panel::handle_panel; +use crate::api::middleware::auth::auth_middleware; +use crate::BotState; +use axum::routing::get; +use axum::Router; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub fn create_panel_router(bot_state: Arc>) -> Router>> { + let panel_router = + Router::new() + .route("/", get(handle_panel)) + .layer(axum::middleware::from_fn_with_state( + bot_state, + auth_middleware, + )); + + panel_router +} diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index a96ad77..ec05efb 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -1,4 +1,3 @@ -use crate::commands::CommandRegistry; use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; use crate::commands::alert::slash_command::alert::AlertCommand; @@ -14,9 +13,10 @@ use crate::commands::recover::slash_command::recover::RecoverCommand; use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; use crate::commands::reply::slash_command::reply::ReplyCommand; +use crate::commands::CommandRegistry; use crate::config::load_config; -use crate::errors::ModmailError; use crate::errors::types::ConfigError; +use crate::errors::ModmailError; use crate::handlers::guild_handler::GuildHandler; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::handlers::guild_members_handler::GuildMembersHandler; @@ -25,14 +25,14 @@ use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::handlers::guild_moderation_handler::GuildModerationHandler; use crate::handlers::ready_handler::ReadyHandler; use crate::handlers::typing_proxy_handler::TypingProxyHandler; -use crate::{BotState, BotStatus, db}; +use crate::panel_commands::user::is_member::is_member; +use crate::{db, BotCommand, BotState, BotStatus}; use serenity::all::{ClientBuilder, GatewayIntents}; use serenity::cache::Settings as CacheSettings; use std::process; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; -use tokio::sync::watch::Receiver; use tokio::{select, spawn}; pub async fn init_bot_state() -> Arc> { @@ -43,10 +43,14 @@ pub async fn init_bot_state() -> Arc> { let config = load_config("config.toml"); + let (command_tx, _command_rx) = tokio::sync::mpsc::channel(32); + let bot_state = BotState { config, status: BotStatus::Stopped, db_pool: Some(pool), + command_tx: command_tx.clone(), + bot_http: None, }; Arc::new(Mutex::new(bot_state)) @@ -64,11 +68,12 @@ pub async fn start_bot_if_config_valid( } let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); + let (command_tx, command_rx) = tokio::sync::mpsc::channel(32); let bot_state_clone = bot_state.clone(); let handle = spawn(async move { let mut shutdown_rx_owned = shutdown_rx.clone(); - run_bot(bot_state_clone.clone(), &mut shutdown_rx_owned).await; + run_bot(bot_state_clone.clone(), &mut shutdown_rx_owned, command_rx).await; let mut s = bot_state_clone.lock().await; s.status = BotStatus::Stopped; }); @@ -76,6 +81,7 @@ pub async fn start_bot_if_config_valid( handle, shutdown: shutdown_tx, }; + state_lock.command_tx = command_tx; drop(state_lock); Ok(()) @@ -84,7 +90,11 @@ pub async fn start_bot_if_config_valid( } } -pub async fn run_bot(bot_state: Arc>, shutdown: &mut Receiver) { +pub async fn run_bot( + bot_state: Arc>, + shutdown: &mut tokio::sync::watch::Receiver, + mut command_rx: tokio::sync::mpsc::Receiver, +) { let shutdown_rx_command = shutdown.clone(); let shutdown_rx = shutdown.clone(); @@ -161,6 +171,11 @@ pub async fn run_bot(bot_state: Arc>, shutdown: &mut Receiver>, shutdown: &mut Receiver { - println!("Shutdown signal received, shutting down..."); - shard_manager.shutdown_all().await; - } - _ = discord_task => { - println!("Discord task ended."); + loop { + select! { + _ = shutdown.changed() => { + println!("Shutdown signal received, shutting down..."); + shard_manager.shutdown_all().await; + break; + } + Some(cmd) = command_rx.recv() => { + match cmd { + BotCommand::CheckUserIsMember { user_id, resp} => { + println!("Checking if user {} is member of staff guild...", user_id); + let http = { + let state_lock = bot_state.lock().await; + state_lock.bot_http.clone().expect("Failed to get bot http") + }; + let is_member = is_member(http, config.bot.get_staff_guild_id(), user_id).await; + + let _ = resp.send(is_member); + continue; + } + _ => {} + } + } + + _ = &mut discord_task => { + println!("Discord task ended."); + break; + } } } diff --git a/rustmail/src/commands/edit/common.rs b/rustmail/src/commands/edit/common.rs index c8a7ece..93f81f4 100644 --- a/rustmail/src/commands/edit/common.rs +++ b/rustmail/src/commands/edit/common.rs @@ -1,6 +1,6 @@ use crate::config::Config; -use crate::errors::ModmailResult; use crate::errors::common::invalid_command; +use crate::errors::ModmailResult; use crate::utils::command::extract_reply_content::extract_reply_content; use serenity::all::Message; @@ -36,6 +36,8 @@ mod tests { enable_panel: false, features_channel_id: Some(12345), logs_channel_id: Some(15456), + client_id: 123456789012345678, + client_secret: "secret".to_string(), }, command: CommandConfig { prefix: "!".to_string(), diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index e7c7fd1..23f0df9 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -40,6 +40,8 @@ pub struct BotConfig { pub enable_logs: bool, pub enable_features: bool, pub enable_panel: bool, + pub client_id: u64, + pub client_secret: String, #[serde(default)] pub logs_channel_id: Option, diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 3cc0f9c..5da1d7b 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -23,7 +23,7 @@ use crate::db::operations::{ }; use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::get_thread_lock::get_thread_lock; diff --git a/rustmail/src/main.rs b/rustmail/src/main.rs index dc2a270..db999ec 100644 --- a/rustmail/src/main.rs +++ b/rustmail/src/main.rs @@ -4,7 +4,9 @@ use crate::config::Config; use axum::extract::Path; use axum::response::Response; use rust_embed::RustEmbed; +use serenity::http::Http; use std::borrow::Cow; +use std::sync::Arc; use tokio::signal; use tokio::sync::watch::Sender; use tokio::task::JoinHandle; @@ -19,11 +21,12 @@ mod features; mod handlers; mod i18n; mod modules; +mod panel_commands; mod utils; pub struct Database; -enum BotStatus { +pub enum BotStatus { Stopped, Running { handle: JoinHandle<()>, @@ -31,10 +34,25 @@ enum BotStatus { }, } +enum BotCommand { + CheckUserRole { + user_id: u64, + role_id: u64, + resp: tokio::sync::oneshot::Sender, + }, + CheckUserIsMember { + user_id: u64, + resp: tokio::sync::oneshot::Sender, + }, + Test, +} + struct BotState { config: Option, status: BotStatus, db_pool: Option, + command_tx: tokio::sync::mpsc::Sender, + bot_http: Option>, } #[derive(RustEmbed)] diff --git a/rustmail/src/panel_commands/mod.rs b/rustmail/src/panel_commands/mod.rs new file mode 100644 index 0000000..22d12a3 --- /dev/null +++ b/rustmail/src/panel_commands/mod.rs @@ -0,0 +1 @@ +pub mod user; diff --git a/rustmail/src/panel_commands/user/is_member.rs b/rustmail/src/panel_commands/user/is_member.rs new file mode 100644 index 0000000..3d3845f --- /dev/null +++ b/rustmail/src/panel_commands/user/is_member.rs @@ -0,0 +1,10 @@ +use serenity::all::{GuildId, UserId}; +use serenity::http::Http; +use std::sync::Arc; + +pub async fn is_member(http: Arc, guild_id: u64, user_id: u64) -> bool { + let guild = GuildId::new(guild_id); + let user = UserId::new(user_id); + + guild.member(&http, user).await.is_ok() +} diff --git a/rustmail/src/panel_commands/user/mod.rs b/rustmail/src/panel_commands/user/mod.rs new file mode 100644 index 0000000..6292c6e --- /dev/null +++ b/rustmail/src/panel_commands/user/mod.rs @@ -0,0 +1 @@ +pub mod is_member; diff --git a/rustmail/src/utils/command/wrap_command.rs b/rustmail/src/utils/command/wrap_command.rs index c44bc18..5820b0b 100644 --- a/rustmail/src/utils/command/wrap_command.rs +++ b/rustmail/src/utils/command/wrap_command.rs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! wrap_command { ($map:expr, [$($name:expr),+], $func:expr) => {{ - let command: std::sync::Arc) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = + let command: std::sync::Arc) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = std::sync::Arc::new(|ctx, msg, config, shutdown| { Box::pin(async move { $func(&ctx, &msg, &config, Arc::new(shutdown)).await @@ -12,7 +12,7 @@ macro_rules! wrap_command { )+ }}; ($map:expr, $name:expr, $func:expr) => {{ - let command: std::sync::Arc) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = + let command: std::sync::Arc) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = std::sync::Arc::new(|ctx, msg, config, shutdown| { Box::pin(async move { $func(&ctx, &msg, &config, Arc::new(shutdown)).await diff --git a/rustmail_panel/src/pages/home.rs b/rustmail_panel/src/pages/home.rs index b382bc2..ace2624 100644 --- a/rustmail_panel/src/pages/home.rs +++ b/rustmail_panel/src/pages/home.rs @@ -3,15 +3,25 @@ use yew::prelude::*; #[function_component(Home)] pub fn home() -> Html { html! { -
+
+ + {"Documentation"} + + Rustmail logo

{"Rustmail Panel"}

- {"Bienvenue sur le pannel de Rustmail"} + {"Bienvenue. Pour continuer, veuillez vous connecter avec votre compte Discord."}

} diff --git a/rustmail_panel/src/pages/mod.rs b/rustmail_panel/src/pages/mod.rs index 076abb9..ccb86ac 100644 --- a/rustmail_panel/src/pages/mod.rs +++ b/rustmail_panel/src/pages/mod.rs @@ -1,2 +1,3 @@ pub mod docs; pub mod home; +pub mod panel; diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs new file mode 100644 index 0000000..ff2601d --- /dev/null +++ b/rustmail_panel/src/pages/panel.rs @@ -0,0 +1,14 @@ +use yew::{function_component, html, Html}; + +#[function_component(Panel)] +pub fn panel() -> Html { + html! { +
+ Rustmail logo +

{"Rustmail Panel"}

+

+ {"Bienvenue sur le panel de Rustmail"} +

+
+ } +} diff --git a/rustmail_panel/src/router.rs b/rustmail_panel/src/router.rs index 8f1eae1..2fd5029 100644 --- a/rustmail_panel/src/router.rs +++ b/rustmail_panel/src/router.rs @@ -1,7 +1,8 @@ -use yew_router::prelude::*; -use yew::prelude::*; use crate::pages::docs::Docs; use crate::pages::home::Home; +use crate::pages::panel::Panel; +use yew::prelude::*; +use yew_router::prelude::*; #[derive(Clone, Routable, PartialEq)] pub enum Route { @@ -9,6 +10,8 @@ pub enum Route { Home, #[at("/docs")] Documentation, + #[at("/panel")] + Panel, #[not_found] #[at("/404")] NotFound, @@ -18,6 +21,7 @@ pub fn switch(routes: Route) -> Html { match routes { Route::Home => html! { }, Route::Documentation => html! { }, + Route::Panel => html! { }, Route::NotFound => html! {

{ "404" }

}, } -} \ No newline at end of file +} From 48fec3f54ef0e2349e32482bdd09fcffe89211ea Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Wed, 15 Oct 2025 21:47:42 +0200 Subject: [PATCH 007/100] feat(panel): session now stored in DB and panel can be accessed even if the bot if off --- ...309d80ad250895d183ad2dc48f588bcac79c8.json | 12 ++ ...2424b8f80ed91a6849b179c7f37bbdc9bc5b6.json | 12 ++ Cargo.lock | 32 +++++- migrations/20251015172228_sessions_panel.sql | 7 ++ rustmail/src/api/handler/auth/callback.rs | 71 +++++++++--- rustmail/src/api/handler/auth/login.rs | 31 ++++- rustmail/src/api/handler/panel/panel.rs | 7 +- rustmail/src/api/middleware/auth.rs | 107 ++++++++++++++---- rustmail/src/api/routes/panel.rs | 4 +- rustmail_panel/Cargo.toml | 3 + rustmail_panel/src/pages/panel.rs | 60 ++++++++-- 11 files changed, 289 insertions(+), 57 deletions(-) create mode 100644 .sqlx/query-437edc46a24b64d00cbbbce4481309d80ad250895d183ad2dc48f588bcac79c8.json create mode 100644 .sqlx/query-e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6.json create mode 100644 migrations/20251015172228_sessions_panel.sql diff --git a/.sqlx/query-437edc46a24b64d00cbbbce4481309d80ad250895d183ad2dc48f588bcac79c8.json b/.sqlx/query-437edc46a24b64d00cbbbce4481309d80ad250895d183ad2dc48f588bcac79c8.json new file mode 100644 index 0000000..b985de9 --- /dev/null +++ b/.sqlx/query-437edc46a24b64d00cbbbce4481309d80ad250895d183ad2dc48f588bcac79c8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM sessions_panel WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "437edc46a24b64d00cbbbce4481309d80ad250895d183ad2dc48f588bcac79c8" +} diff --git a/.sqlx/query-e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6.json b/.sqlx/query-e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6.json new file mode 100644 index 0000000..83d40d3 --- /dev/null +++ b/.sqlx/query-e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO sessions_panel (session_id, user_id, access_token, refresh_token, expires_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(session_id) DO UPDATE SET\n access_token = excluded.access_token,\n refresh_token = excluded.refresh_token,\n expires_at = excluded.expires_at\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6" +} diff --git a/Cargo.lock b/Cargo.lock index 4c1a65f..1622eee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -550,7 +550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -1006,6 +1006,27 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 1.3.1", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-render" version = "0.1.1" @@ -2406,7 +2427,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2525,6 +2546,9 @@ dependencies = [ name = "rustmail_panel" version = "0.1.0" dependencies = [ + "gloo-net 0.6.0", + "gloo-utils 0.2.0", + "wasm-bindgen-futures", "yew", "yew-router", ] @@ -3202,7 +3226,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3892,7 +3916,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/migrations/20251015172228_sessions_panel.sql b/migrations/20251015172228_sessions_panel.sql new file mode 100644 index 0000000..28ceea7 --- /dev/null +++ b/migrations/20251015172228_sessions_panel.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS sessions_panel ( + session_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT, + expires_at INTEGER NOT NULL +); diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index 249c774..cb1bb27 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -4,7 +4,9 @@ use axum::response::Redirect; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; use reqwest::Client; use std::sync::Arc; +use chrono::{Duration, Utc}; use tokio::sync::Mutex; +use uuid::Uuid; #[derive(serde::Deserialize)] pub struct AuthRequest { @@ -29,19 +31,19 @@ pub async fn handle_callback( return (jar, Redirect::to("/error?message=Missing+configuration")); }; - let client = Client::new(); - - let client_id = config.bot.client_id.to_string(); - let client_secret = config.bot.client_secret.clone(); - let code = params.code.clone(); + let Some(db_pool) = &state_lock.db_pool else { + return (jar, Redirect::to("/error?message=Database+not+initialized")); + }; + let client = Client::new(); + let token_response = client .post("https://discord.com/api/oauth2/token") .form(&[ - ("client_id", client_id.as_str()), - ("client_secret", client_secret.as_str()), + ("client_id", config.bot.client_id.to_string().as_str()), + ("client_secret", config.bot.client_secret.as_str()), ("grant_type", "authorization_code"), - ("code", code.as_str()), + ("code", params.code.as_str()), ("redirect_uri", "http://localhost:3002/api/auth/callback"), ]) .send() @@ -49,20 +51,59 @@ pub async fn handle_callback( .unwrap(); let token_data: serde_json::Value = token_response.json().await.unwrap(); - let access_token = token_data["access_token"].as_str().unwrap(); - let client_response = client + let access_token = token_data["access_token"].as_str().unwrap_or(""); + let refresh_token = token_data["refresh_token"].as_str().unwrap_or(""); + let expires_in = token_data["expires_in"].as_i64().unwrap_or(3600); + + let user_response = client .get("https://discord.com/api/users/@me") .bearer_auth(access_token) .send() .await .unwrap(); - let user: DiscordUser = client_response.json().await.unwrap(); + let user: DiscordUser = user_response.json().await.unwrap(); let user_id = user.id.clone(); - println!("Authenticated user: {:?}", user); + + let session_id = Uuid::new_v4().to_string(); + let expires_at = Utc::now() + Duration::seconds(expires_in); + let timestamp = expires_at.timestamp(); - let cookie_token = Cookie::build(("session_token", access_token.to_string())) + if let Err(e) = sqlx::query!( + r#"DELETE FROM sessions_panel WHERE user_id = ?"#, + user_id + ) + .execute(db_pool) + .await + { + eprintln!("⚠️ Failed to delete old session: {}", e); + return (jar, Redirect::to("/error?message=Database+delete+failed")); + } + + if let Err(e) = sqlx::query!( + r#" + INSERT INTO sessions_panel (session_id, user_id, access_token, refresh_token, expires_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(session_id) DO UPDATE SET + access_token = excluded.access_token, + refresh_token = excluded.refresh_token, + expires_at = excluded.expires_at + "#, + session_id, + user_id, + access_token, + refresh_token, + timestamp, + ) + .execute(db_pool) + .await + { + eprintln!("⚠️ Failed to store session in database: {}", e); + return (jar, Redirect::to("/error?message=Database+write+failed")); + } + + let cookie_session = Cookie::build(("session_id", session_id)) .path("/") .http_only(true) .same_site(SameSite::Lax) @@ -74,7 +115,7 @@ pub async fn handle_callback( .same_site(SameSite::Lax) .build(); - let jar = jar.add(cookie_token).add(cookie_user); - + let jar = jar.add(cookie_session).add(cookie_user); + (jar, Redirect::to("/panel")) } diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index deac770..8675a96 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -2,13 +2,38 @@ use crate::BotState; use axum::extract::State; use axum::response::Redirect; use std::sync::Arc; +use axum_extra::extract::CookieJar; +use sqlx::{query, Row}; use tokio::sync::Mutex; -pub async fn handle_login(State(bot_state): State>>) -> Redirect { - println!("Yo"); +pub async fn handle_login( + jar: CookieJar, + State(bot_state): State>>) -> Redirect { let state_lock = bot_state.lock().await; - println!("Handling login request"); + let session_cookie = jar.get("session_id"); + let user_id = jar.get("user_id"); + + if let (Some(session_cookie), Some(user_id)) = (session_cookie, user_id) { + let session_id = session_cookie.value(); + let user_id = user_id.value(); + + if let Some(db_pool) = &state_lock.db_pool { + if let Ok(row) = query("SELECT expires_at FROM sessions_panel WHERE session_id = ? AND user_id = ?") + .bind(session_id) + .bind(user_id) + .fetch_one(db_pool) + .await + { + let expires_at: i64 = row.get::("expires_at"); + let current_timestamp = chrono::Utc::now().timestamp(); + + if expires_at > current_timestamp { + return Redirect::to("/panel"); + } + } + } + } let Some(config) = &state_lock.config else { return Redirect::to("/error?message=Missing+configuration"); diff --git a/rustmail/src/api/handler/panel/panel.rs b/rustmail/src/api/handler/panel/panel.rs index ee45954..0956a9f 100644 --- a/rustmail/src/api/handler/panel/panel.rs +++ b/rustmail/src/api/handler/panel/panel.rs @@ -1,9 +1,10 @@ use crate::BotState; use axum::extract::State; -use axum::response::Redirect; +use axum::response::{IntoResponse, Redirect}; use std::sync::Arc; +use hyper::StatusCode; use tokio::sync::Mutex; -pub async fn handle_panel(State(bot_state): State>>) -> Redirect { - Redirect::to("/panel") +pub async fn handle_panel_check(State(bot_state): State>>) -> impl IntoResponse { + axum::response::Json(serde_json::json!({ "authorized": true })) } diff --git a/rustmail/src/api/middleware/auth.rs b/rustmail/src/api/middleware/auth.rs index 60bd5e2..2a58a26 100644 --- a/rustmail/src/api/middleware/auth.rs +++ b/rustmail/src/api/middleware/auth.rs @@ -1,10 +1,14 @@ -use crate::{BotCommand, BotState}; +use crate::{BotCommand, BotState, BotStatus}; use axum::extract::Request; use axum::extract::State; use axum::middleware::Next; -use axum::response::{IntoResponse, Redirect, Response}; +use axum::response::{IntoResponse, Response}; use axum_extra::extract::CookieJar; use std::sync::Arc; +use chrono::Utc; +use hyper::StatusCode; +use serenity::all::{GuildId, UserId}; +use sqlx::{query, Row}; use tokio::sync::Mutex; async fn check_user_with_bot(bot_state: Arc>, user_id: &str) -> bool { @@ -31,27 +35,44 @@ async fn check_user_with_bot(bot_state: Arc>, user_id: &str) -> return false; } - let test = resp_rx.await.unwrap(); - println!("User is member: {}", test); - - test + resp_rx.await.unwrap() } -async fn check_user_with_api(token: &str) -> bool { - false +async fn check_user_with_api(user_id: &str, guild_id: u64, bot_http: Arc) -> bool { + let guild_id = GuildId::new(guild_id); + let user_id = match user_id.parse::() { + Ok(id) => UserId::new(id), + Err(_) => return false, + }; + + match guild_id.member(bot_http, user_id).await { + Ok(_) => true, + Err(_) => false, + } } -async fn verify_token(token: &str, user_id: &str, bot_state: Arc>) -> bool { +async fn verify_user(user_id: &str, guild_id: u64, bot_state: Arc>) -> bool { let state_lock = bot_state.lock().await; - let bot_http = state_lock.bot_http.clone(); + let is_bot_on = match state_lock.status { + BotStatus::Running { .. } => true, + BotStatus::Stopped => false, + }; + drop(state_lock); - if let Some(http) = &bot_http { + let http = { + let state_lock = bot_state.lock().await; + match &state_lock.bot_http { + Some(bot_http) => bot_http.clone(), + None => return false, + } + }; + + if is_bot_on { return check_user_with_bot(bot_state, user_id).await; } - - check_user_with_api(token).await + check_user_with_api(user_id, guild_id, http).await } pub async fn auth_middleware( @@ -60,21 +81,63 @@ pub async fn auth_middleware( req: Request, next: Next, ) -> Response { - let token_cookie = jar.get("session_token"); + let session_cookie = jar.get("session_id"); let user_cookie = jar.get("user_id"); - if token_cookie.is_none() || user_cookie.is_none() { - return Redirect::to("/login").into_response(); + if session_cookie.is_none() || user_cookie.is_none() { + return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(); } - let token = token_cookie.unwrap().value().to_string(); + let session_id = session_cookie.unwrap().value().to_string(); let user_id = user_cookie.unwrap().value().to_string(); - let is_valid = verify_token(token.as_str(), user_id.as_str(), bot_state).await; + let db_pool = { + let state_lock = bot_state.lock().await; + match &state_lock.db_pool { + Some(pool) => pool.clone(), + None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not initialized").into_response(), + } + }; + + let guild_id = { + let state_lock = bot_state.lock().await; + match &state_lock.config { + Some(config) => config.bot.get_staff_guild_id(), + None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not initialized").into_response(), + } + }; - if !is_valid { - return Redirect::to("/login").into_response(); + let result = query("SELECT expires_at FROM sessions_panel WHERE session_id = ? AND user_id = ?") + .bind(&session_id) + .bind(&user_id) + .fetch_one(&db_pool) + .await; + + match result { + Ok(row) => { + let expires_at = row.get::("expires_at"); + let now = Utc::now().timestamp(); + + if expires_at < now { + let _ = query("DELETE FROM sessions_panel WHERE session_id = ?") + .bind(&session_id) + .execute(&db_pool) + .await; + return (StatusCode::UNAUTHORIZED, "Session expired").into_response(); + } + + if !verify_user(&user_id, guild_id, bot_state.clone()).await { + let _ = query("DELETE FROM sessions_panel WHERE session_id = ?") + .bind(&session_id) + .execute(&db_pool) + .await; + return (StatusCode::UNAUTHORIZED, "Invalid session").into_response(); + } + + next.run(req).await + } + Err(_) => { + (StatusCode::UNAUTHORIZED, "Unauthorized").into_response() + } } - - next.run(req).await } diff --git a/rustmail/src/api/routes/panel.rs b/rustmail/src/api/routes/panel.rs index 90db6e5..3ee880a 100644 --- a/rustmail/src/api/routes/panel.rs +++ b/rustmail/src/api/routes/panel.rs @@ -1,4 +1,4 @@ -use crate::api::handler::panel::panel::handle_panel; +use crate::api::handler::panel::panel::{handle_panel_check}; use crate::api::middleware::auth::auth_middleware; use crate::BotState; use axum::routing::get; @@ -9,7 +9,7 @@ use tokio::sync::Mutex; pub fn create_panel_router(bot_state: Arc>) -> Router>> { let panel_router = Router::new() - .route("/", get(handle_panel)) + .route("/check", get(handle_panel_check)) .layer(axum::middleware::from_fn_with_state( bot_state, auth_middleware, diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index ca140ac..d61e5fc 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -7,3 +7,6 @@ license = "MIT" [dependencies] yew = { version = "0.21", features = ["csr"] } yew-router = "0.18.0" +wasm-bindgen-futures = "0.4.50" +gloo-net = "0.6.0" +gloo-utils = "0.2.0" diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index ff2601d..651879f 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -1,14 +1,58 @@ -use yew::{function_component, html, Html}; +use yew::prelude::*; +use gloo_net::http::Request; +use gloo_utils::window; +use wasm_bindgen_futures::spawn_local; #[function_component(Panel)] pub fn panel() -> Html { + let authorized = use_state(|| None::); + + { + let authorized = authorized.clone(); + use_effect_with((), move |_| { + spawn_local(async move { + let ok = Request::get("/api/panel/check") + .send() + .await + .map(|r| r.status() == 200) + .unwrap_or(false); + authorized.set(Some(ok)); + }); + || () + }); + } + + { + let dep = (*authorized).clone(); + use_effect_with(dep, move |auth: &Option| { + if matches!(auth, Some(false)) { + let _ = window().location().set_href("/"); + } + || () + }); + } + html! { -
- Rustmail logo -

{"Rustmail Panel"}

-

- {"Bienvenue sur le panel de Rustmail"} -

-
+ <> + { + match *authorized { + None => html! { +
+

{"Vérification de l'accès..."}

+
+ }, + Some(true) => html! { +
+ Rustmail logo +

{"Rustmail Panel"}

+

+ {"Bienvenue sur le panel de Rustmail"} +

+
+ }, + Some(false) => html! {}, + } + } + } } From d75e23e72333ab19d3d61d250dbd08a1102f305f Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 16 Oct 2025 11:11:21 +0200 Subject: [PATCH 008/100] feat(panel): add i18n system for traductions --- Cargo.lock | 22 ++++-- rustmail/src/api/handler/auth/callback.rs | 56 +++++++++++----- rustmail/src/bot.rs | 2 +- rustmail_panel/Cargo.toml | 3 + .../src/components/language_switcher.rs | 67 +++++++++++++++++++ rustmail_panel/src/components/mod.rs | 1 + rustmail_panel/src/i18n/en/en.json | 10 +++ rustmail_panel/src/i18n/fr/fr.json | 10 +++ rustmail_panel/src/main.rs | 25 +++++-- rustmail_panel/src/pages/error.rs | 27 ++++++++ rustmail_panel/src/pages/home.rs | 7 +- rustmail_panel/src/pages/mod.rs | 1 + rustmail_panel/src/router.rs | 4 ++ 13 files changed, 208 insertions(+), 27 deletions(-) create mode 100644 rustmail_panel/src/components/language_switcher.rs create mode 100644 rustmail_panel/src/i18n/en/en.json create mode 100644 rustmail_panel/src/i18n/fr/fr.json create mode 100644 rustmail_panel/src/pages/error.rs diff --git a/Cargo.lock b/Cargo.lock index 1622eee..d28394f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -550,7 +550,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1469,6 +1469,17 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "i18nrs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09d2f33c772de8e151f781d95ac0809fe01b4cf106446e11a9b7172c08a3d6e" +dependencies = [ + "serde_json", + "web-sys", + "yew", +] + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -2427,7 +2438,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2548,7 +2559,10 @@ version = "0.1.0" dependencies = [ "gloo-net 0.6.0", "gloo-utils 0.2.0", + "i18nrs", + "serde_json", "wasm-bindgen-futures", + "web-sys", "yew", "yew-router", ] @@ -3226,7 +3240,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3916,7 +3930,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index cb1bb27..318044a 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -2,9 +2,9 @@ use crate::BotState; use axum::extract::{Query, State}; use axum::response::Redirect; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; +use chrono::{Duration, Utc}; use reqwest::Client; use std::sync::Arc; -use chrono::{Duration, Utc}; use tokio::sync::Mutex; use uuid::Uuid; @@ -36,8 +36,8 @@ pub async fn handle_callback( }; let client = Client::new(); - - let token_response = client + + let token_response = match client .post("https://discord.com/api/oauth2/token") .form(&[ ("client_id", config.bot.client_id.to_string().as_str()), @@ -48,32 +48,56 @@ pub async fn handle_callback( ]) .send() .await - .unwrap(); + { + Ok(resp) => { + if !resp.status().is_success() { + eprintln!("⚠️ Token exchange failed with status: {}", resp.status()); + return (jar, Redirect::to("/error?message=Token+exchange+failed")); + } + resp + } + Err(e) => { + eprintln!( + "⚠️ Failed to exchange code for token (maybe client_secret or client_id: {}", + e + ); + return ( + jar, + Redirect::to("/error?message=Failed+to+exchange+code+for+token"), + ); + } + }; let token_data: serde_json::Value = token_response.json().await.unwrap(); let access_token = token_data["access_token"].as_str().unwrap_or(""); let refresh_token = token_data["refresh_token"].as_str().unwrap_or(""); let expires_in = token_data["expires_in"].as_i64().unwrap_or(3600); - - let user_response = client + + let user_response = match client .get("https://discord.com/api/users/@me") .bearer_auth(access_token) .send() .await - .unwrap(); + { + Ok(resp) => resp, + Err(e) => { + eprintln!("⚠️ Failed to fetch user info: {}", e); + return ( + jar, + Redirect::to("/error?message=Failed+to+fetch+user+info"), + ); + } + }; let user: DiscordUser = user_response.json().await.unwrap(); let user_id = user.id.clone(); - + let session_id = Uuid::new_v4().to_string(); let expires_at = Utc::now() + Duration::seconds(expires_in); let timestamp = expires_at.timestamp(); - if let Err(e) = sqlx::query!( - r#"DELETE FROM sessions_panel WHERE user_id = ?"#, - user_id - ) + if let Err(e) = sqlx::query!(r#"DELETE FROM sessions_panel WHERE user_id = ?"#, user_id) .execute(db_pool) .await { @@ -96,13 +120,13 @@ pub async fn handle_callback( refresh_token, timestamp, ) - .execute(db_pool) - .await + .execute(db_pool) + .await { eprintln!("⚠️ Failed to store session in database: {}", e); return (jar, Redirect::to("/error?message=Database+write+failed")); } - + let cookie_session = Cookie::build(("session_id", session_id)) .path("/") .http_only(true) @@ -116,6 +140,6 @@ pub async fn handle_callback( .build(); let jar = jar.add(cookie_session).add(cookie_user); - + (jar, Redirect::to("/panel")) } diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index ec05efb..85e4965 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -216,7 +216,7 @@ pub async fn run_bot( Some(cmd) = command_rx.recv() => { match cmd { BotCommand::CheckUserIsMember { user_id, resp} => { - println!("Checking if user {} is member of staff guild...", user_id); + println!("Rustmail is checking if user {} is member of staff guild...", user_id); let http = { let state_lock = bot_state.lock().await; state_lock.bot_http.clone().expect("Failed to get bot http") diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index d61e5fc..432c48d 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -10,3 +10,6 @@ yew-router = "0.18.0" wasm-bindgen-futures = "0.4.50" gloo-net = "0.6.0" gloo-utils = "0.2.0" +web-sys = "0.3.77" +serde_json = "1.0.145" +i18nrs = { version = "0.1.7", features = ["yew"] } diff --git a/rustmail_panel/src/components/language_switcher.rs b/rustmail_panel/src/components/language_switcher.rs new file mode 100644 index 0000000..d8e86cf --- /dev/null +++ b/rustmail_panel/src/components/language_switcher.rs @@ -0,0 +1,67 @@ +use i18nrs::yew::use_translation; +use web_sys::HtmlInputElement; +use yew::prelude::*; + +#[function_component(LanguageSwitcher)] +pub fn language_switcher() -> Html { + let (i18n, set_language) = use_translation(); + + let language_ref = use_node_ref(); + let language_state = use_state(|| "".to_string()); + + { + let language_state = language_state.clone(); + let i18n = i18n.clone(); + use_effect(move || { + let current = i18n.get_current_language().to_string(); + if *language_state != current { + language_state.set(current); + } + || () + }); + } + + let onchange = { + let language_ref = language_ref.clone(); + let language_state = language_state.clone(); + let set_language = set_language.clone(); + + Callback::from(move |_| { + if let Some(input) = language_ref.cast::() { + let value = input.value(); + language_state.set(value.clone()); + set_language.emit(input.value()); + } + }) + }; + + html! { +
+
+ +
+ { "▼" } +
+
+
+ } +} diff --git a/rustmail_panel/src/components/mod.rs b/rustmail_panel/src/components/mod.rs index 9b86bcf..5681e6a 100644 --- a/rustmail_panel/src/components/mod.rs +++ b/rustmail_panel/src/components/mod.rs @@ -1 +1,2 @@ pub mod home; +pub mod language_switcher; diff --git a/rustmail_panel/src/i18n/en/en.json b/rustmail_panel/src/i18n/en/en.json new file mode 100644 index 0000000..84206cb --- /dev/null +++ b/rustmail_panel/src/i18n/en/en.json @@ -0,0 +1,10 @@ +{ + "home": { + "welcome": "Welcome. To continue, please log in with your Discord account.", + "connect_button": "Sign in" + }, + "back_to_home": "Back to home", + "error": { + "error": "Error" + } +} diff --git a/rustmail_panel/src/i18n/fr/fr.json b/rustmail_panel/src/i18n/fr/fr.json new file mode 100644 index 0000000..3628ce9 --- /dev/null +++ b/rustmail_panel/src/i18n/fr/fr.json @@ -0,0 +1,10 @@ +{ + "home": { + "welcome": "Bienvenue. Pour continuer, veuillez vous connecter avec votre compte Discord.", + "connect_button": "Se connecter" + }, + "back_to_home": "Retour à l'accueil", + "error": { + "error": "Erreur" + } +} \ No newline at end of file diff --git a/rustmail_panel/src/main.rs b/rustmail_panel/src/main.rs index 9ddb54b..8b14313 100644 --- a/rustmail_panel/src/main.rs +++ b/rustmail_panel/src/main.rs @@ -2,16 +2,33 @@ mod components; mod pages; mod router; +use crate::components::language_switcher::LanguageSwitcher; +use crate::router::{switch, Route}; +use i18nrs::yew::I18nProvider; +use i18nrs::StorageType; +use std::collections::HashMap; use yew::prelude::*; use yew_router::prelude::*; -use crate::router::{switch, Route}; #[function_component] fn App() -> Html { + let translations = HashMap::from([ + ("en", include_str!("i18n/en/en.json")), + ("fr", include_str!("i18n/fr/fr.json")), + ]); + html! { - - render={switch} /> - + + + render={switch} /> + + + } } diff --git a/rustmail_panel/src/pages/error.rs b/rustmail_panel/src/pages/error.rs new file mode 100644 index 0000000..bd58b3a --- /dev/null +++ b/rustmail_panel/src/pages/error.rs @@ -0,0 +1,27 @@ +use gloo_utils::window; +use i18nrs::yew::use_translation; +use web_sys::UrlSearchParams; +use yew::prelude::*; + +#[function_component(Error)] +pub fn error() -> Html { + let (i18n, _set_language) = use_translation(); + + let location = window().location(); + let search = location.search().unwrap_or_default(); + + let params = UrlSearchParams::new_with_str(&search).unwrap(); + let message = params + .get("message") + .unwrap_or_else(|| "Unknown error".into()); + + html! { +
+ + { i18n.t("back_to_home") } + +

{ i18n.t("error.error") }

+

{ message }

+
+ } +} diff --git a/rustmail_panel/src/pages/home.rs b/rustmail_panel/src/pages/home.rs index ace2624..8112ebc 100644 --- a/rustmail_panel/src/pages/home.rs +++ b/rustmail_panel/src/pages/home.rs @@ -1,7 +1,10 @@ +use i18nrs::yew::use_translation; use yew::prelude::*; #[function_component(Home)] pub fn home() -> Html { + let (i18n, _set_language) = use_translation(); + html! {
Html { Rustmail logo

{"Rustmail Panel"}

- {"Bienvenue. Pour continuer, veuillez vous connecter avec votre compte Discord."} + { i18n.t("home.welcome") }

diff --git a/rustmail_panel/src/pages/mod.rs b/rustmail_panel/src/pages/mod.rs index ccb86ac..2a1e40f 100644 --- a/rustmail_panel/src/pages/mod.rs +++ b/rustmail_panel/src/pages/mod.rs @@ -1,3 +1,4 @@ pub mod docs; +pub mod error; pub mod home; pub mod panel; diff --git a/rustmail_panel/src/router.rs b/rustmail_panel/src/router.rs index 2fd5029..57dda63 100644 --- a/rustmail_panel/src/router.rs +++ b/rustmail_panel/src/router.rs @@ -1,4 +1,5 @@ use crate::pages::docs::Docs; +use crate::pages::error::Error; use crate::pages::home::Home; use crate::pages::panel::Panel; use yew::prelude::*; @@ -12,6 +13,8 @@ pub enum Route { Documentation, #[at("/panel")] Panel, + #[at("/error")] + Error, #[not_found] #[at("/404")] NotFound, @@ -22,6 +25,7 @@ pub fn switch(routes: Route) -> Html { Route::Home => html! { }, Route::Documentation => html! { }, Route::Panel => html! { }, + Route::Error => html! { }, Route::NotFound => html! {

{ "404" }

}, } } From 45c19a850e80b1e9c5efaae26e69e824257cb6c4 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 16 Oct 2025 11:34:51 +0200 Subject: [PATCH 009/100] fix(panel): add the LanguageSwitcher component only on pages that require it --- rustmail_panel/src/i18n/en/en.json | 3 +++ rustmail_panel/src/i18n/fr/fr.json | 3 +++ rustmail_panel/src/main.rs | 2 -- rustmail_panel/src/pages/docs.rs | 18 ++++++++----- rustmail_panel/src/pages/error.rs | 18 ++++++++----- rustmail_panel/src/pages/home.rs | 43 +++++++++++++++++------------- rustmail_panel/src/pages/panel.rs | 12 ++++++++- 7 files changed, 63 insertions(+), 36 deletions(-) diff --git a/rustmail_panel/src/i18n/en/en.json b/rustmail_panel/src/i18n/en/en.json index 84206cb..551f95b 100644 --- a/rustmail_panel/src/i18n/en/en.json +++ b/rustmail_panel/src/i18n/en/en.json @@ -6,5 +6,8 @@ "back_to_home": "Back to home", "error": { "error": "Error" + }, + "panel": { + "logout": "Logout" } } diff --git a/rustmail_panel/src/i18n/fr/fr.json b/rustmail_panel/src/i18n/fr/fr.json index 3628ce9..d2e04eb 100644 --- a/rustmail_panel/src/i18n/fr/fr.json +++ b/rustmail_panel/src/i18n/fr/fr.json @@ -6,5 +6,8 @@ "back_to_home": "Retour à l'accueil", "error": { "error": "Erreur" + }, + "panel": { + "logout": "Se déconnecter" } } \ No newline at end of file diff --git a/rustmail_panel/src/main.rs b/rustmail_panel/src/main.rs index 8b14313..143071a 100644 --- a/rustmail_panel/src/main.rs +++ b/rustmail_panel/src/main.rs @@ -2,7 +2,6 @@ mod components; mod pages; mod router; -use crate::components::language_switcher::LanguageSwitcher; use crate::router::{switch, Route}; use i18nrs::yew::I18nProvider; use i18nrs::StorageType; @@ -26,7 +25,6 @@ fn App() -> Html { > render={switch} /> - } diff --git a/rustmail_panel/src/pages/docs.rs b/rustmail_panel/src/pages/docs.rs index d8094c7..406e416 100644 --- a/rustmail_panel/src/pages/docs.rs +++ b/rustmail_panel/src/pages/docs.rs @@ -1,14 +1,18 @@ +use crate::components::language_switcher::LanguageSwitcher; use yew::{function_component, html, Html}; #[function_component(Docs)] pub fn docs() -> Html { html! { -
- Rustmail logo -

{"Rustmail Panel"}

-

- {"Page de documentation en construction..."} -

-
+ <> +
+ Rustmail logo +

{"Rustmail Panel"}

+

+ {"Page de documentation en construction..."} +

+
+ + } } diff --git a/rustmail_panel/src/pages/error.rs b/rustmail_panel/src/pages/error.rs index bd58b3a..be9b628 100644 --- a/rustmail_panel/src/pages/error.rs +++ b/rustmail_panel/src/pages/error.rs @@ -1,3 +1,4 @@ +use crate::components::language_switcher::LanguageSwitcher; use gloo_utils::window; use i18nrs::yew::use_translation; use web_sys::UrlSearchParams; @@ -16,12 +17,15 @@ pub fn error() -> Html { .unwrap_or_else(|| "Unknown error".into()); html! { -
- - { i18n.t("back_to_home") } - -

{ i18n.t("error.error") }

-

{ message }

-
+ <> +
+ + { i18n.t("back_to_home") } + +

{ i18n.t("error.error") }

+

{ message }

+
+ + } } diff --git a/rustmail_panel/src/pages/home.rs b/rustmail_panel/src/pages/home.rs index 8112ebc..f84e570 100644 --- a/rustmail_panel/src/pages/home.rs +++ b/rustmail_panel/src/pages/home.rs @@ -1,3 +1,4 @@ +use crate::components::language_switcher::LanguageSwitcher; use i18nrs::yew::use_translation; use yew::prelude::*; @@ -6,26 +7,30 @@ pub fn home() -> Html { let (i18n, _set_language) = use_translation(); html! { -
- - {"Documentation"} - - Rustmail logo -

{"Rustmail Panel"}

-

- { i18n.t("home.welcome") } -

- -
+ + Rustmail logo +

{"Rustmail Panel"}

+

+ { i18n.t("home.welcome") } +

+ +
+ + } } diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index 651879f..4cbe954 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -1,10 +1,13 @@ -use yew::prelude::*; +use crate::components::language_switcher::LanguageSwitcher; use gloo_net::http::Request; use gloo_utils::window; use wasm_bindgen_futures::spawn_local; +use yew::prelude::*; #[function_component(Panel)] pub fn panel() -> Html { + let (i18n, _set_language) = i18nrs::yew::use_translation(); + let authorized = use_state(|| None::); { @@ -44,6 +47,12 @@ pub fn panel() -> Html { Some(true) => html! {
Rustmail logo + + + { i18n.t("panel.logout") } + +

{"Rustmail Panel"}

{"Bienvenue sur le panel de Rustmail"} @@ -53,6 +62,7 @@ pub fn panel() -> Html { Some(false) => html! {}, } } + } } From f1e9155f25a86d16ce1f593b0eb35ee17fa7e527 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 16 Oct 2025 11:35:17 +0200 Subject: [PATCH 010/100] feat(panel): add logout feature on panel --- rustmail/src/api/handler/auth/logout.rs | 31 +++++++++++++++++++++++++ rustmail/src/api/handler/auth/mod.rs | 1 + rustmail/src/api/routes/auth.rs | 4 +++- rustmail/src/bot.rs | 1 - 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 rustmail/src/api/handler/auth/logout.rs diff --git a/rustmail/src/api/handler/auth/logout.rs b/rustmail/src/api/handler/auth/logout.rs new file mode 100644 index 0000000..84556da --- /dev/null +++ b/rustmail/src/api/handler/auth/logout.rs @@ -0,0 +1,31 @@ +use crate::BotState; +use axum::extract::State; +use axum::response::Redirect; +use axum_extra::extract::CookieJar; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub async fn handle_logout( + jar: CookieJar, + State(bot_state): State>>, +) -> Redirect { + let state_lock = bot_state.lock().await; + + let session_cookie = jar.get("session_id"); + let user_id = jar.get("user_id"); + + if let (Some(session_cookie), Some(user_id)) = (session_cookie, user_id) { + let session_id = session_cookie.value(); + let user_id = user_id.value(); + + if let Some(db_pool) = &state_lock.db_pool { + let _ = sqlx::query("DELETE FROM sessions_panel WHERE session_id = ? AND user_id = ?") + .bind(session_id) + .bind(user_id) + .execute(db_pool) + .await; + } + } + + Redirect::to("/") +} diff --git a/rustmail/src/api/handler/auth/mod.rs b/rustmail/src/api/handler/auth/mod.rs index d1702fb..47c9997 100644 --- a/rustmail/src/api/handler/auth/mod.rs +++ b/rustmail/src/api/handler/auth/mod.rs @@ -1,2 +1,3 @@ pub mod callback; pub mod login; +pub mod logout; diff --git a/rustmail/src/api/routes/auth.rs b/rustmail/src/api/routes/auth.rs index 29b8720..b6a3377 100644 --- a/rustmail/src/api/routes/auth.rs +++ b/rustmail/src/api/routes/auth.rs @@ -1,5 +1,6 @@ use crate::api::handler::auth::callback::handle_callback; use crate::api::handler::auth::login::handle_login; +use crate::api::handler::auth::logout::handle_logout; use crate::BotState; use axum::routing::get; use axum::Router; @@ -9,7 +10,8 @@ use tokio::sync::Mutex; pub fn create_auth_router() -> Router>> { let auth_router = Router::new() .route("/login", get(handle_login)) - .route("/callback", get(handle_callback)); + .route("/callback", get(handle_callback)) + .route("/logout", get(handle_logout)); auth_router } diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 85e4965..14cb365 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -224,7 +224,6 @@ pub async fn run_bot( let is_member = is_member(http, config.bot.get_staff_guild_id(), user_id).await; let _ = resp.send(is_member); - continue; } _ => {} } From 62d67829b7042cce803c14e348cfc81e367d2021 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 16 Oct 2025 11:44:22 +0200 Subject: [PATCH 011/100] fix(panel): welcome message on panel is now translated --- rustmail_panel/src/i18n/en/en.json | 1 + rustmail_panel/src/i18n/fr/fr.json | 1 + rustmail_panel/src/pages/panel.rs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rustmail_panel/src/i18n/en/en.json b/rustmail_panel/src/i18n/en/en.json index 551f95b..ebd6356 100644 --- a/rustmail_panel/src/i18n/en/en.json +++ b/rustmail_panel/src/i18n/en/en.json @@ -8,6 +8,7 @@ "error": "Error" }, "panel": { + "welcome": "Welcome on the Rustmail Panel", "logout": "Logout" } } diff --git a/rustmail_panel/src/i18n/fr/fr.json b/rustmail_panel/src/i18n/fr/fr.json index d2e04eb..daa4155 100644 --- a/rustmail_panel/src/i18n/fr/fr.json +++ b/rustmail_panel/src/i18n/fr/fr.json @@ -8,6 +8,7 @@ "error": "Erreur" }, "panel": { + "welcome": "Bienvenue sur le panel de Rustmail", "logout": "Se déconnecter" } } \ No newline at end of file diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index 4cbe954..4d9a55a 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -55,7 +55,7 @@ pub fn panel() -> Html {

{"Rustmail Panel"}

- {"Bienvenue sur le panel de Rustmail"} + { i18n.t("panel.welcome") }

}, From effcc40a419501eed1c73ae1f5c177c038e900a1 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 16 Oct 2025 13:23:06 +0200 Subject: [PATCH 012/100] refactor(start): reload config when starting the bot with api --- rustmail/src/api/handler/bot/start.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rustmail/src/api/handler/bot/start.rs b/rustmail/src/api/handler/bot/start.rs index 3e29854..dae3f99 100644 --- a/rustmail/src/api/handler/bot/start.rs +++ b/rustmail/src/api/handler/bot/start.rs @@ -1,4 +1,5 @@ use crate::bot::run_bot; +use crate::config::load_config; use crate::{BotState, BotStatus}; use axum::extract::State; use axum::http::StatusCode; @@ -14,6 +15,8 @@ pub async fn handle_start_bot( match state_lock.status { BotStatus::Stopped => { + state_lock.config = load_config("config.toml"); + if state_lock.config.is_none() { return (StatusCode::BAD_REQUEST, Json("Missing configuration.")); } From 28073f0b13036f9aa28401d433a289219ead22cd Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 16 Oct 2025 13:23:37 +0200 Subject: [PATCH 013/100] refactor(panel): add traductions for docs panel page --- rustmail_panel/src/i18n/en/en.json | 3 +++ rustmail_panel/src/i18n/fr/fr.json | 3 +++ rustmail_panel/src/pages/docs.rs | 11 ++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/rustmail_panel/src/i18n/en/en.json b/rustmail_panel/src/i18n/en/en.json index ebd6356..3d91d77 100644 --- a/rustmail_panel/src/i18n/en/en.json +++ b/rustmail_panel/src/i18n/en/en.json @@ -10,5 +10,8 @@ "panel": { "welcome": "Welcome on the Rustmail Panel", "logout": "Logout" + }, + "docs": { + "back_to_home": "Back to home" } } diff --git a/rustmail_panel/src/i18n/fr/fr.json b/rustmail_panel/src/i18n/fr/fr.json index daa4155..6e75247 100644 --- a/rustmail_panel/src/i18n/fr/fr.json +++ b/rustmail_panel/src/i18n/fr/fr.json @@ -10,5 +10,8 @@ "panel": { "welcome": "Bienvenue sur le panel de Rustmail", "logout": "Se déconnecter" + }, + "docs": { + "back_to_home": "Retour à l'accueil" } } \ No newline at end of file diff --git a/rustmail_panel/src/pages/docs.rs b/rustmail_panel/src/pages/docs.rs index 406e416..6fe10c8 100644 --- a/rustmail_panel/src/pages/docs.rs +++ b/rustmail_panel/src/pages/docs.rs @@ -1,15 +1,24 @@ use crate::components::language_switcher::LanguageSwitcher; +use i18nrs::yew::use_translation; use yew::{function_component, html, Html}; #[function_component(Docs)] pub fn docs() -> Html { + let (i18n, _set_language) = use_translation(); + html! { <>
Rustmail logo + + + { i18n.t("docs.back_to_home") } + +

{"Rustmail Panel"}

- {"Page de documentation en construction..."} + {"Work in progress..."}

From 362a203dee488bf665bbdc3addf1487c104d63ab Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 16 Oct 2025 14:57:19 +0200 Subject: [PATCH 014/100] refactor(panel): create component for logout button --- rustmail_panel/src/components/logout_button.rs | 13 +++++++++++++ rustmail_panel/src/components/mod.rs | 1 + rustmail_panel/src/pages/panel.rs | 12 ++++++++---- 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 rustmail_panel/src/components/logout_button.rs diff --git a/rustmail_panel/src/components/logout_button.rs b/rustmail_panel/src/components/logout_button.rs new file mode 100644 index 0000000..7b8d3c0 --- /dev/null +++ b/rustmail_panel/src/components/logout_button.rs @@ -0,0 +1,13 @@ +use yew::{function_component, html, Html}; + +#[function_component(LogoutButton)] +pub fn logout_button() -> Html { + let (i18n, _set_language) = i18nrs::yew::use_translation(); + + html! { + + { i18n.t("panel.logout") } + + } +} diff --git a/rustmail_panel/src/components/mod.rs b/rustmail_panel/src/components/mod.rs index 5681e6a..7cb2306 100644 --- a/rustmail_panel/src/components/mod.rs +++ b/rustmail_panel/src/components/mod.rs @@ -1,2 +1,3 @@ pub mod home; pub mod language_switcher; +pub mod logout_button; diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index 4d9a55a..c63541f 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -1,9 +1,16 @@ use crate::components::language_switcher::LanguageSwitcher; +use crate::components::logout_button::LogoutButton; use gloo_net::http::Request; use gloo_utils::window; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; +#[derive(Clone, PartialEq)] +enum PanelTab { + Config, + Logs, +} + #[function_component(Panel)] pub fn panel() -> Html { let (i18n, _set_language) = i18nrs::yew::use_translation(); @@ -48,10 +55,7 @@ pub fn panel() -> Html {
Rustmail logo - - { i18n.t("panel.logout") } - +

{"Rustmail Panel"}

From ab295b8f2b198b2d147168e5f946dd5f787ac112 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 17 Oct 2025 11:54:15 +0200 Subject: [PATCH 015/100] feat(panel): add navbar and user's avatar to panel page --- Cargo.lock | 155 +++++++++++++++++- ....sql => 20251017082144_sessions_panel.sql} | 6 +- rustmail/src/api/handler/auth/callback.rs | 10 +- rustmail/src/api/handler/auth/login.rs | 18 +- rustmail/src/api/handler/bot/start.rs | 2 +- rustmail/src/api/handler/mod.rs | 1 + rustmail/src/api/handler/panel/panel.rs | 6 +- rustmail/src/api/handler/user/avatar.rs | 59 +++++++ rustmail/src/api/handler/user/mod.rs | 1 + rustmail/src/api/middleware/auth.rs | 41 +++-- rustmail/src/api/router.rs | 5 +- rustmail/src/api/routes/auth.rs | 4 +- rustmail/src/api/routes/bot.rs | 4 +- rustmail/src/api/routes/mod.rs | 1 + rustmail/src/api/routes/panel.rs | 19 +-- rustmail/src/api/routes/user.rs | 18 ++ rustmail/src/bot.rs | 1 - rustmail/src/commands/edit/common.rs | 2 +- rustmail/src/db/operations/init.rs | 2 +- .../src/handlers/guild_messages_handler.rs | 2 +- rustmail_panel/Cargo.toml | 2 + rustmail_panel/index.html | 1 + .../src/components/language_switcher.rs | 57 ++++--- .../src/components/logout_button.rs | 2 +- rustmail_panel/src/components/mod.rs | 1 + rustmail_panel/src/components/navbar.rs | 100 +++++++++++ rustmail_panel/src/main.rs | 4 +- rustmail_panel/src/pages/docs.rs | 2 +- rustmail_panel/src/pages/home.rs | 7 +- rustmail_panel/src/pages/panel.rs | 83 +++++++--- 30 files changed, 516 insertions(+), 100 deletions(-) rename migrations/{20251015172228_sessions_panel.sql => 20251017082144_sessions_panel.sql} (62%) create mode 100644 rustmail/src/api/handler/user/avatar.rs create mode 100644 rustmail/src/api/handler/user/mod.rs create mode 100644 rustmail/src/api/routes/user.rs create mode 100644 rustmail_panel/src/components/navbar.rs diff --git a/Cargo.lock b/Cargo.lock index d28394f..5c8462d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,6 +543,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.12" @@ -765,6 +776,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -840,6 +860,25 @@ dependencies = [ "gloo-worker 0.4.0", ] +[[package]] +name = "gloo" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" +dependencies = [ + "gloo-console 0.3.0", + "gloo-dialogs 0.2.0", + "gloo-events 0.2.0", + "gloo-file 0.3.0", + "gloo-history 0.2.2", + "gloo-net 0.5.0", + "gloo-render 0.2.0", + "gloo-storage 0.3.0", + "gloo-timers 0.3.0", + "gloo-utils 0.2.0", + "gloo-worker 0.5.0", +] + [[package]] name = "gloo-console" version = "0.2.3" @@ -1006,6 +1045,27 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-net" version = "0.6.0" @@ -1161,6 +1221,25 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gloo-worker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" +dependencies = [ + "bincode", + "futures", + "gloo-utils 0.2.0", + "gloo-worker-macros", + "js-sys", + "pinned", + "serde", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "gloo-worker-macros" version = "0.1.0" @@ -1282,6 +1361,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + [[package]] name = "http" version = "0.2.12" @@ -2181,6 +2269,25 @@ dependencies = [ "unicase", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.1", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quote" version = "1.0.40" @@ -2560,7 +2667,9 @@ dependencies = [ "gloo-net 0.6.0", "gloo-utils 0.2.0", "i18nrs", + "serde", "serde_json", + "tailyew", "wasm-bindgen-futures", "web-sys", "yew", @@ -2855,7 +2964,7 @@ dependencies = [ "cargo_metadata", "error-chain", "glob", - "pulldown-cmark", + "pulldown-cmark 0.9.6", "tempfile", "walkdir", ] @@ -3230,6 +3339,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" +[[package]] +name = "tailyew" +version = "0.1.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7bc870cf622199c42565c9fe3e5e5781a838f35b16d003be12890e8139f2" +dependencies = [ + "erased-serde", + "gloo 0.11.0", + "gloo-net 0.6.0", + "gloo-timers 0.3.0", + "js-sys", + "pulldown-cmark 0.13.0", + "regex", + "serde", + "serde_json", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yew", +] + [[package]] name = "tempfile" version = "3.20.0" @@ -3617,6 +3748,12 @@ dependencies = [ "utf-8", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typemap_rev" version = "0.3.0" @@ -3691,6 +3828,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" @@ -3721,6 +3864,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3735,6 +3884,7 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde", "wasm-bindgen", ] @@ -4203,9 +4353,12 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac" dependencies = [ + "base64ct", + "bincode", "console_error_panic_hook", "futures", "gloo 0.10.0", + "html-escape", "implicit-clone", "indexmap", "js-sys", diff --git a/migrations/20251015172228_sessions_panel.sql b/migrations/20251017082144_sessions_panel.sql similarity index 62% rename from migrations/20251015172228_sessions_panel.sql rename to migrations/20251017082144_sessions_panel.sql index 28ceea7..c88e03b 100644 --- a/migrations/20251015172228_sessions_panel.sql +++ b/migrations/20251017082144_sessions_panel.sql @@ -1,7 +1,9 @@ +-- Add migration script here CREATE TABLE IF NOT EXISTS sessions_panel ( session_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, access_token TEXT NOT NULL, refresh_token TEXT, - expires_at INTEGER NOT NULL -); + expires_at INTEGER NOT NULL, + avatar_hash TEXT NOT NULL +); \ No newline at end of file diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index 318044a..fed1ea2 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -18,7 +18,7 @@ struct DiscordUser { id: String, username: String, discriminator: String, - avatar: Option, + avatar: String, } pub async fn handle_callback( @@ -107,18 +107,20 @@ pub async fn handle_callback( if let Err(e) = sqlx::query!( r#" - INSERT INTO sessions_panel (session_id, user_id, access_token, refresh_token, expires_at) - VALUES (?, ?, ?, ?, ?) + INSERT INTO sessions_panel (session_id, user_id, access_token, refresh_token, expires_at, avatar_hash) + VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET access_token = excluded.access_token, refresh_token = excluded.refresh_token, - expires_at = excluded.expires_at + expires_at = excluded.expires_at, + avatar_hash = excluded.avatar_hash "#, session_id, user_id, access_token, refresh_token, timestamp, + user.avatar ) .execute(db_pool) .await diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index 8675a96..aba5936 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -1,14 +1,15 @@ use crate::BotState; use axum::extract::State; use axum::response::Redirect; -use std::sync::Arc; use axum_extra::extract::CookieJar; -use sqlx::{query, Row}; +use sqlx::{Row, query}; +use std::sync::Arc; use tokio::sync::Mutex; pub async fn handle_login( jar: CookieJar, - State(bot_state): State>>) -> Redirect { + State(bot_state): State>>, +) -> Redirect { let state_lock = bot_state.lock().await; let session_cookie = jar.get("session_id"); @@ -19,11 +20,12 @@ pub async fn handle_login( let user_id = user_id.value(); if let Some(db_pool) = &state_lock.db_pool { - if let Ok(row) = query("SELECT expires_at FROM sessions_panel WHERE session_id = ? AND user_id = ?") - .bind(session_id) - .bind(user_id) - .fetch_one(db_pool) - .await + if let Ok(row) = + query("SELECT expires_at FROM sessions_panel WHERE session_id = ? AND user_id = ?") + .bind(session_id) + .bind(user_id) + .fetch_one(db_pool) + .await { let expires_at: i64 = row.get::("expires_at"); let current_timestamp = chrono::Utc::now().timestamp(); diff --git a/rustmail/src/api/handler/bot/start.rs b/rustmail/src/api/handler/bot/start.rs index dae3f99..be34c0f 100644 --- a/rustmail/src/api/handler/bot/start.rs +++ b/rustmail/src/api/handler/bot/start.rs @@ -1,9 +1,9 @@ use crate::bot::run_bot; use crate::config::load_config; use crate::{BotState, BotStatus}; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use axum::Json; use std::sync::Arc; use tokio::spawn; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/mod.rs b/rustmail/src/api/handler/mod.rs index 9edd1e1..6c84d29 100644 --- a/rustmail/src/api/handler/mod.rs +++ b/rustmail/src/api/handler/mod.rs @@ -1,3 +1,4 @@ pub mod auth; pub mod bot; pub mod panel; +pub mod user; diff --git a/rustmail/src/api/handler/panel/panel.rs b/rustmail/src/api/handler/panel/panel.rs index 0956a9f..177ee59 100644 --- a/rustmail/src/api/handler/panel/panel.rs +++ b/rustmail/src/api/handler/panel/panel.rs @@ -1,10 +1,12 @@ use crate::BotState; use axum::extract::State; use axum::response::{IntoResponse, Redirect}; -use std::sync::Arc; use hyper::StatusCode; +use std::sync::Arc; use tokio::sync::Mutex; -pub async fn handle_panel_check(State(bot_state): State>>) -> impl IntoResponse { +pub async fn handle_panel_check( + State(bot_state): State>>, +) -> impl IntoResponse { axum::response::Json(serde_json::json!({ "authorized": true })) } diff --git a/rustmail/src/api/handler/user/avatar.rs b/rustmail/src/api/handler/user/avatar.rs new file mode 100644 index 0000000..3ec0528 --- /dev/null +++ b/rustmail/src/api/handler/user/avatar.rs @@ -0,0 +1,59 @@ +use crate::BotState; +use axum::extract::{Query, State}; +use axum::response::IntoResponse; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct UserAvatar { + pub avatar_url: Option, +} + +#[derive(serde::Deserialize)] +pub struct UserIdQuery { + id: String, +} + +pub async fn handle_get_user_avatar( + State(bot_state): State>>, + Query(params): Query, +) -> impl IntoResponse { + let state_lock = bot_state.lock().await; + + let pool = match &state_lock.db_pool { + Some(pool) => pool, + None => { + return axum::response::Json(serde_json::json!(UserAvatar { avatar_url: None })); + } + }; + + let user_id = params.id; + + let user_avatar = match sqlx::query!( + "SELECT avatar_hash FROM sessions_panel WHERE user_id = ?", + user_id + ) + .fetch_one(pool) + .await + { + Ok(record) => { + if let Some(avatar_hash) = record.avatar_hash { + let avatar_url = format!( + "https://cdn.discordapp.com/avatars/{}/{}.png", + user_id, avatar_hash + ); + UserAvatar { + avatar_url: Some(avatar_url), + } + } else { + UserAvatar { avatar_url: None } + } + } + Err(e) => { + eprintln!("Error fetching user avatar: {}", e); + UserAvatar { avatar_url: None } + } + }; + + axum::response::Json(serde_json::json!(user_avatar)) +} diff --git a/rustmail/src/api/handler/user/mod.rs b/rustmail/src/api/handler/user/mod.rs new file mode 100644 index 0000000..124369c --- /dev/null +++ b/rustmail/src/api/handler/user/mod.rs @@ -0,0 +1 @@ +pub mod avatar; diff --git a/rustmail/src/api/middleware/auth.rs b/rustmail/src/api/middleware/auth.rs index 2a58a26..3feb9e6 100644 --- a/rustmail/src/api/middleware/auth.rs +++ b/rustmail/src/api/middleware/auth.rs @@ -4,11 +4,11 @@ use axum::extract::State; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; use axum_extra::extract::CookieJar; -use std::sync::Arc; use chrono::Utc; use hyper::StatusCode; use serenity::all::{GuildId, UserId}; -use sqlx::{query, Row}; +use sqlx::{Row, query}; +use std::sync::Arc; use tokio::sync::Mutex; async fn check_user_with_bot(bot_state: Arc>, user_id: &str) -> bool { @@ -38,7 +38,11 @@ async fn check_user_with_bot(bot_state: Arc>, user_id: &str) -> resp_rx.await.unwrap() } -async fn check_user_with_api(user_id: &str, guild_id: u64, bot_http: Arc) -> bool { +async fn check_user_with_api( + user_id: &str, + guild_id: u64, + bot_http: Arc, +) -> bool { let guild_id = GuildId::new(guild_id); let user_id = match user_id.parse::() { Ok(id) => UserId::new(id), @@ -95,7 +99,13 @@ pub async fn auth_middleware( let state_lock = bot_state.lock().await; match &state_lock.db_pool { Some(pool) => pool.clone(), - None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not initialized").into_response(), + None => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Database not initialized", + ) + .into_response(); + } } }; @@ -103,15 +113,22 @@ pub async fn auth_middleware( let state_lock = bot_state.lock().await; match &state_lock.config { Some(config) => config.bot.get_staff_guild_id(), - None => return (StatusCode::INTERNAL_SERVER_ERROR, "Database not initialized").into_response(), + None => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + "Database not initialized", + ) + .into_response(); + } } }; - let result = query("SELECT expires_at FROM sessions_panel WHERE session_id = ? AND user_id = ?") - .bind(&session_id) - .bind(&user_id) - .fetch_one(&db_pool) - .await; + let result = + query("SELECT expires_at FROM sessions_panel WHERE session_id = ? AND user_id = ?") + .bind(&session_id) + .bind(&user_id) + .fetch_one(&db_pool) + .await; match result { Ok(row) => { @@ -136,8 +153,6 @@ pub async fn auth_middleware( next.run(req).await } - Err(_) => { - (StatusCode::UNAUTHORIZED, "Unauthorized").into_response() - } + Err(_) => (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(), } } diff --git a/rustmail/src/api/router.rs b/rustmail/src/api/router.rs index 7eb1e2e..726d7c2 100644 --- a/rustmail/src/api/router.rs +++ b/rustmail/src/api/router.rs @@ -1,7 +1,8 @@ +use crate::BotState; use crate::api::routes::auth::create_auth_router; use crate::api::routes::bot::create_bot_router; use crate::api::routes::panel::create_panel_router; -use crate::BotState; +use crate::api::routes::user::create_user_router; use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; @@ -10,11 +11,13 @@ pub fn create_api_router(bot_state: Arc>) -> Router { let bot_router = create_bot_router(); let auth_router = create_auth_router(); let panel_router = create_panel_router(bot_state.clone()); + let user_router = create_user_router(bot_state.clone()); let app = Router::new() .nest("/api/bot", bot_router) .nest("/api/auth", auth_router) .nest("/api/panel", panel_router) + .nest("/api/user", user_router) .with_state(bot_state.clone()); app diff --git a/rustmail/src/api/routes/auth.rs b/rustmail/src/api/routes/auth.rs index b6a3377..249f03f 100644 --- a/rustmail/src/api/routes/auth.rs +++ b/rustmail/src/api/routes/auth.rs @@ -1,9 +1,9 @@ +use crate::BotState; use crate::api::handler::auth::callback::handle_callback; use crate::api::handler::auth::login::handle_login; use crate::api::handler::auth::logout::handle_logout; -use crate::BotState; -use axum::routing::get; use axum::Router; +use axum::routing::get; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index e1a68dc..817acd4 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -1,8 +1,8 @@ +use crate::BotState; use crate::api::handler::bot::start::handle_start_bot; use crate::api::handler::bot::stop::handle_stop_bot; -use crate::BotState; -use axum::routing::post; use axum::Router; +use axum::routing::post; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/mod.rs b/rustmail/src/api/routes/mod.rs index 9edd1e1..6c84d29 100644 --- a/rustmail/src/api/routes/mod.rs +++ b/rustmail/src/api/routes/mod.rs @@ -1,3 +1,4 @@ pub mod auth; pub mod bot; pub mod panel; +pub mod user; diff --git a/rustmail/src/api/routes/panel.rs b/rustmail/src/api/routes/panel.rs index 3ee880a..f35c181 100644 --- a/rustmail/src/api/routes/panel.rs +++ b/rustmail/src/api/routes/panel.rs @@ -1,19 +1,18 @@ -use crate::api::handler::panel::panel::{handle_panel_check}; -use crate::api::middleware::auth::auth_middleware; use crate::BotState; -use axum::routing::get; +use crate::api::handler::panel::panel::handle_panel_check; +use crate::api::middleware::auth::auth_middleware; use axum::Router; +use axum::routing::get; use std::sync::Arc; use tokio::sync::Mutex; pub fn create_panel_router(bot_state: Arc>) -> Router>> { - let panel_router = - Router::new() - .route("/check", get(handle_panel_check)) - .layer(axum::middleware::from_fn_with_state( - bot_state, - auth_middleware, - )); + let panel_router = Router::new() + .route("/check", get(handle_panel_check)) + .layer(axum::middleware::from_fn_with_state( + bot_state, + auth_middleware, + )); panel_router } diff --git a/rustmail/src/api/routes/user.rs b/rustmail/src/api/routes/user.rs new file mode 100644 index 0000000..6af1d61 --- /dev/null +++ b/rustmail/src/api/routes/user.rs @@ -0,0 +1,18 @@ +use crate::BotState; +use crate::api::handler::user::avatar::handle_get_user_avatar; +use crate::api::middleware::auth::auth_middleware; +use axum::Router; +use axum::routing::get; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub fn create_user_router(bot_state: Arc>) -> Router>> { + let user_router = Router::new() + .route("/avatar", get(handle_get_user_avatar)) + .layer(axum::middleware::from_fn_with_state( + bot_state, + auth_middleware, + )); + + user_router +} diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 14cb365..63a402c 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -216,7 +216,6 @@ pub async fn run_bot( Some(cmd) = command_rx.recv() => { match cmd { BotCommand::CheckUserIsMember { user_id, resp} => { - println!("Rustmail is checking if user {} is member of staff guild...", user_id); let http = { let state_lock = bot_state.lock().await; state_lock.bot_http.clone().expect("Failed to get bot http") diff --git a/rustmail/src/commands/edit/common.rs b/rustmail/src/commands/edit/common.rs index 93f81f4..172b03b 100644 --- a/rustmail/src/commands/edit/common.rs +++ b/rustmail/src/commands/edit/common.rs @@ -1,6 +1,6 @@ use crate::config::Config; -use crate::errors::common::invalid_command; use crate::errors::ModmailResult; +use crate::errors::common::invalid_command; use crate::utils::command::extract_reply_content::extract_reply_content; use serenity::all::Message; diff --git a/rustmail/src/db/operations/init.rs b/rustmail/src/db/operations/init.rs index 7f951ff..38a2617 100644 --- a/rustmail/src/db/operations/init.rs +++ b/rustmail/src/db/operations/init.rs @@ -1,4 +1,4 @@ -use sqlx::{sqlite::SqlitePoolOptions, SqlitePool}; +use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; use std::fs; use std::path::Path; diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 5da1d7b..3cc0f9c 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -23,7 +23,7 @@ use crate::db::operations::{ }; use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::get_thread_lock::get_thread_lock; diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index 432c48d..bf32ead 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -13,3 +13,5 @@ gloo-utils = "0.2.0" web-sys = "0.3.77" serde_json = "1.0.145" i18nrs = { version = "0.1.7", features = ["yew"] } +tailyew = "0.1.38" +serde = { version = "1.0.228", features = ["derive"] } diff --git a/rustmail_panel/index.html b/rustmail_panel/index.html index e03beba..445a6a8 100644 --- a/rustmail_panel/index.html +++ b/rustmail_panel/index.html @@ -3,6 +3,7 @@ + Rustmail Panel diff --git a/rustmail_panel/src/components/language_switcher.rs b/rustmail_panel/src/components/language_switcher.rs index d8e86cf..7f3e35e 100644 --- a/rustmail_panel/src/components/language_switcher.rs +++ b/rustmail_panel/src/components/language_switcher.rs @@ -1,9 +1,14 @@ use i18nrs::yew::use_translation; -use web_sys::HtmlInputElement; use yew::prelude::*; +#[derive(Properties, PartialEq, Clone)] +pub struct LanguageSwitcherProps { + #[prop_or("top-left".to_string())] + pub placement: String, +} + #[function_component(LanguageSwitcher)] -pub fn language_switcher() -> Html { +pub fn language_switcher(props: &LanguageSwitcherProps) -> Html { let (i18n, set_language) = use_translation(); let language_ref = use_node_ref(); @@ -27,39 +32,49 @@ pub fn language_switcher() -> Html { let set_language = set_language.clone(); Callback::from(move |_| { - if let Some(input) = language_ref.cast::() { - let value = input.value(); + if let Some(select) = language_ref.cast::() { + let value = select.value(); language_state.set(value.clone()); - set_language.emit(input.value()); + set_language.emit(value); } }) }; + let container_classes = if props.placement == "nav" { + "relative inline-flex items-center ml-4" + } else { + "absolute top-6 left-6" + }; + html! { -

-
+
+
-
- { "▼" } + +
+ + +
diff --git a/rustmail_panel/src/components/logout_button.rs b/rustmail_panel/src/components/logout_button.rs index 7b8d3c0..7ce4bc5 100644 --- a/rustmail_panel/src/components/logout_button.rs +++ b/rustmail_panel/src/components/logout_button.rs @@ -1,4 +1,4 @@ -use yew::{function_component, html, Html}; +use yew::{Html, function_component, html}; #[function_component(LogoutButton)] pub fn logout_button() -> Html { diff --git a/rustmail_panel/src/components/mod.rs b/rustmail_panel/src/components/mod.rs index 7cb2306..8d39904 100644 --- a/rustmail_panel/src/components/mod.rs +++ b/rustmail_panel/src/components/mod.rs @@ -1,3 +1,4 @@ pub mod home; pub mod language_switcher; pub mod logout_button; +pub mod navbar; diff --git a/rustmail_panel/src/components/navbar.rs b/rustmail_panel/src/components/navbar.rs new file mode 100644 index 0000000..845ec75 --- /dev/null +++ b/rustmail_panel/src/components/navbar.rs @@ -0,0 +1,100 @@ +use crate::components::language_switcher::LanguageSwitcher; +use yew::{Callback, Html, Properties, function_component, html, use_state}; + +#[derive(Properties, PartialEq, Clone)] +pub struct RustmailNavbarProps { + pub avatar_url: String, +} + +#[function_component(RustmailNavbar)] +pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html { + let mobile_menu_open = use_state(|| false); + let profile_menu_open = use_state(|| false); + + let toggle_mobile_menu = { + let mobile_menu_open = mobile_menu_open.clone(); + Callback::from(move |_| mobile_menu_open.set(!*mobile_menu_open)) + }; + + let toggle_profile_menu = { + let profile_menu_open = profile_menu_open.clone(); + Callback::from(move |_| profile_menu_open.set(!*profile_menu_open)) + }; + + html! { + + } +} diff --git a/rustmail_panel/src/main.rs b/rustmail_panel/src/main.rs index 143071a..3d107b1 100644 --- a/rustmail_panel/src/main.rs +++ b/rustmail_panel/src/main.rs @@ -2,9 +2,9 @@ mod components; mod pages; mod router; -use crate::router::{switch, Route}; -use i18nrs::yew::I18nProvider; +use crate::router::{Route, switch}; use i18nrs::StorageType; +use i18nrs::yew::I18nProvider; use std::collections::HashMap; use yew::prelude::*; use yew_router::prelude::*; diff --git a/rustmail_panel/src/pages/docs.rs b/rustmail_panel/src/pages/docs.rs index 6fe10c8..fa7e5e5 100644 --- a/rustmail_panel/src/pages/docs.rs +++ b/rustmail_panel/src/pages/docs.rs @@ -1,6 +1,6 @@ use crate::components::language_switcher::LanguageSwitcher; use i18nrs::yew::use_translation; -use yew::{function_component, html, Html}; +use yew::{Html, function_component, html}; #[function_component(Docs)] pub fn docs() -> Html { diff --git a/rustmail_panel/src/pages/home.rs b/rustmail_panel/src/pages/home.rs index f84e570..23ca743 100644 --- a/rustmail_panel/src/pages/home.rs +++ b/rustmail_panel/src/pages/home.rs @@ -10,11 +10,6 @@ pub fn home() -> Html { <>
- - {"Documentation"} - - Rustmail logo

{"Rustmail Panel"}

@@ -30,7 +25,7 @@ pub fn home() -> Html {

- + } } diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index c63541f..907ee6b 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -1,14 +1,31 @@ -use crate::components::language_switcher::LanguageSwitcher; -use crate::components::logout_button::LogoutButton; +use crate::components::navbar::RustmailNavbar; use gloo_net::http::Request; use gloo_utils::window; use wasm_bindgen_futures::spawn_local; +use web_sys::HtmlDocument; +use web_sys::wasm_bindgen::JsCast; use yew::prelude::*; -#[derive(Clone, PartialEq)] -enum PanelTab { - Config, - Logs, +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct UserAvatar { + pub avatar_url: Option, +} + +fn get_cookie(name: &str) -> Option { + let document = web_sys::window() + .unwrap() + .document() + .unwrap() + .dyn_into::() + .unwrap(); + + let cookies = document.cookie().unwrap_or_default(); + + cookies + .split(';') + .map(|s| s.trim()) + .find(|s| s.starts_with(&format!("{}=", name))) + .map(|s| s.split_at(name.len() + 1).1.to_string()) } #[function_component(Panel)] @@ -16,7 +33,6 @@ pub fn panel() -> Html { let (i18n, _set_language) = i18nrs::yew::use_translation(); let authorized = use_state(|| None::); - { let authorized = authorized.clone(); use_effect_with((), move |_| { @@ -33,8 +49,8 @@ pub fn panel() -> Html { } { - let dep = (*authorized).clone(); - use_effect_with(dep, move |auth: &Option| { + let authorized = authorized.clone(); + use_effect_with((*authorized).clone(), move |auth| { if matches!(auth, Some(false)) { let _ = window().location().set_href("/"); } @@ -42,6 +58,37 @@ pub fn panel() -> Html { }); } + let user_id: u64 = get_cookie("user_id") + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + let avatar = use_state(|| None::); + { + let avatar = avatar.clone(); + use_effect_with(user_id, move |&user_id| { + let avatar = avatar.clone(); + spawn_local(async move { + if user_id == 0 { + avatar.set(None); + return; + } + + let url = format!("/api/user/avatar?id={}", user_id); + if let Ok(resp) = Request::get(&url).send().await { + if let Ok(user_avatar) = resp.json::().await { + avatar.set(user_avatar.avatar_url); + } else { + avatar.set(None); + } + } else { + avatar.set(None); + } + }); + || () + }); + } + let avatar_url = (*avatar).clone().unwrap_or_default(); + html! { <> { @@ -52,21 +99,19 @@ pub fn panel() -> Html { }, Some(true) => html! { -
- Rustmail logo + <> + - - -

{"Rustmail Panel"}

-

- { i18n.t("panel.welcome") } -

-
+
+ Rustmail logo +

{"Rustmail Panel"}

+

{ i18n.t("panel.welcome") }

+
+ }, Some(false) => html! {}, } } - } } From a9a5af2810cbe4d28fab292fa0cc4eaac694baaa Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 17 Oct 2025 13:06:11 +0200 Subject: [PATCH 016/100] refactor(panel): add missing sqlx files for new sessions_panel sql table --- ...d48a67d61296dc49624a62552f80002ae4c6e.json | 20 +++++++++++++++++++ ...c3417377e5a2ba4cd203c12354634a606c3d1.json | 12 +++++++++++ ...2424b8f80ed91a6849b179c7f37bbdc9bc5b6.json | 12 ----------- 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 .sqlx/query-05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e.json create mode 100644 .sqlx/query-2440129afcae5c46a5e74767be8c3417377e5a2ba4cd203c12354634a606c3d1.json delete mode 100644 .sqlx/query-e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6.json diff --git a/.sqlx/query-05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e.json b/.sqlx/query-05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e.json new file mode 100644 index 0000000..d1bdbb3 --- /dev/null +++ b/.sqlx/query-05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT avatar_hash FROM sessions_panel WHERE user_id = ?", + "describe": { + "columns": [ + { + "name": "avatar_hash", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true + ] + }, + "hash": "05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e" +} diff --git a/.sqlx/query-2440129afcae5c46a5e74767be8c3417377e5a2ba4cd203c12354634a606c3d1.json b/.sqlx/query-2440129afcae5c46a5e74767be8c3417377e5a2ba4cd203c12354634a606c3d1.json new file mode 100644 index 0000000..58bdcc1 --- /dev/null +++ b/.sqlx/query-2440129afcae5c46a5e74767be8c3417377e5a2ba4cd203c12354634a606c3d1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO sessions_panel (session_id, user_id, access_token, refresh_token, expires_at, avatar_hash)\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(session_id) DO UPDATE SET\n access_token = excluded.access_token,\n refresh_token = excluded.refresh_token,\n expires_at = excluded.expires_at,\n avatar_hash = excluded.avatar_hash\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "2440129afcae5c46a5e74767be8c3417377e5a2ba4cd203c12354634a606c3d1" +} diff --git a/.sqlx/query-e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6.json b/.sqlx/query-e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6.json deleted file mode 100644 index 83d40d3..0000000 --- a/.sqlx/query-e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO sessions_panel (session_id, user_id, access_token, refresh_token, expires_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(session_id) DO UPDATE SET\n access_token = excluded.access_token,\n refresh_token = excluded.refresh_token,\n expires_at = excluded.expires_at\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "e4c9d5e2f4d17f6780ca93b20f62424b8f80ed91a6849b179c7f37bbdc9bc5b6" -} From f172897088727085a0d65d3b30b7a98868999c07 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 17 Oct 2025 14:53:44 +0200 Subject: [PATCH 017/100] fix(panel): users who had a basic Discord profile picture were not seeing it on the panel. --- rustmail/src/api/handler/auth/callback.rs | 13 +++++++++-- rustmail/src/api/handler/user/avatar.rs | 28 ++++++++++++++++++----- rustmail_panel/src/components/navbar.rs | 2 +- rustmail_panel/src/pages/panel.rs | 2 +- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index fed1ea2..02eef21 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -18,7 +18,7 @@ struct DiscordUser { id: String, username: String, discriminator: String, - avatar: String, + avatar: Option, } pub async fn handle_callback( @@ -90,7 +90,16 @@ pub async fn handle_callback( } }; - let user: DiscordUser = user_response.json().await.unwrap(); + let user: DiscordUser = match user_response.json().await { + Ok(user) => user, + Err(e) => { + eprintln!("⚠️ Failed to parse user info: {}", e); + return ( + jar, + Redirect::to("/error?message=Failed+to+parse+user+info"), + ); + } + }; let user_id = user.id.clone(); let session_id = Uuid::new_v4().to_string(); diff --git a/rustmail/src/api/handler/user/avatar.rs b/rustmail/src/api/handler/user/avatar.rs index 3ec0528..dbeb061 100644 --- a/rustmail/src/api/handler/user/avatar.rs +++ b/rustmail/src/api/handler/user/avatar.rs @@ -21,32 +21,48 @@ pub async fn handle_get_user_avatar( let state_lock = bot_state.lock().await; let pool = match &state_lock.db_pool { - Some(pool) => pool, + Some(pool) => pool.clone(), None => { return axum::response::Json(serde_json::json!(UserAvatar { avatar_url: None })); } }; - let user_id = params.id; + drop(state_lock); + + let user_id_str = params.id; + + let user_id: u64 = match user_id_str.parse::() { + Ok(id) => id, + Err(e) => { + eprintln!("Error parsing user ID: {}", e); + return axum::response::Json(serde_json::json!(UserAvatar { avatar_url: None })); + } + }; let user_avatar = match sqlx::query!( "SELECT avatar_hash FROM sessions_panel WHERE user_id = ?", - user_id + user_id_str ) - .fetch_one(pool) + .fetch_one(&pool) .await { Ok(record) => { if let Some(avatar_hash) = record.avatar_hash { let avatar_url = format!( "https://cdn.discordapp.com/avatars/{}/{}.png", - user_id, avatar_hash + user_id_str, avatar_hash ); UserAvatar { avatar_url: Some(avatar_url), } } else { - UserAvatar { avatar_url: None } + let avatar_url = format!( + "https://cdn.discordapp.com/embed/avatars/{}.png", + (user_id >> 22) % 6 + ); + UserAvatar { + avatar_url: Some(avatar_url), + } } } Err(e) => { diff --git a/rustmail_panel/src/components/navbar.rs b/rustmail_panel/src/components/navbar.rs index 845ec75..1211fa2 100644 --- a/rustmail_panel/src/components/navbar.rs +++ b/rustmail_panel/src/components/navbar.rs @@ -1,5 +1,5 @@ use crate::components::language_switcher::LanguageSwitcher; -use yew::{Callback, Html, Properties, function_component, html, use_state}; +use yew::{function_component, html, use_state, Callback, Html, Properties}; #[derive(Properties, PartialEq, Clone)] pub struct RustmailNavbarProps { diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index 907ee6b..44a177b 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -2,8 +2,8 @@ use crate::components::navbar::RustmailNavbar; use gloo_net::http::Request; use gloo_utils::window; use wasm_bindgen_futures::spawn_local; -use web_sys::HtmlDocument; use web_sys::wasm_bindgen::JsCast; +use web_sys::HtmlDocument; use yew::prelude::*; #[derive(serde::Deserialize, Debug, serde::Serialize)] From 48a695f37f5e8b26eee8a76893c4daa1e923ab82 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 17 Oct 2025 19:04:50 +0200 Subject: [PATCH 018/100] fix(panel): fixed invalid match for building discord avatar url --- rustmail/src/api/handler/user/avatar.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rustmail/src/api/handler/user/avatar.rs b/rustmail/src/api/handler/user/avatar.rs index dbeb061..4dc62cf 100644 --- a/rustmail/src/api/handler/user/avatar.rs +++ b/rustmail/src/api/handler/user/avatar.rs @@ -47,10 +47,10 @@ pub async fn handle_get_user_avatar( .await { Ok(record) => { - if let Some(avatar_hash) = record.avatar_hash { + if !record.avatar_hash.is_empty() { let avatar_url = format!( "https://cdn.discordapp.com/avatars/{}/{}.png", - user_id_str, avatar_hash + user_id_str, record.avatar_hash ); UserAvatar { avatar_url: Some(avatar_url), From 5389cbd8ff4b4baba35d4f3eb1d214a4113b5c99 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 17 Oct 2025 22:56:43 +0200 Subject: [PATCH 019/100] fix(panel): remove user_id from cookie --- rustmail/src/api/handler/auth/callback.rs | 8 +-- rustmail/src/api/handler/auth/login.rs | 62 +++++++++++-------- rustmail/src/api/handler/auth/logout.rs | 14 ++++- rustmail/src/api/handler/user/avatar.rs | 44 +++++++------ rustmail/src/api/middleware/auth.rs | 10 +-- rustmail/src/api/mod.rs | 1 + .../src/api/utils/get_user_id_from_session.rs | 19 ++++++ rustmail/src/api/utils/mod.rs | 1 + rustmail_panel/src/pages/panel.rs | 34 ++-------- 9 files changed, 102 insertions(+), 91 deletions(-) create mode 100644 rustmail/src/api/utils/get_user_id_from_session.rs create mode 100644 rustmail/src/api/utils/mod.rs diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index 02eef21..1345f47 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -144,13 +144,7 @@ pub async fn handle_callback( .same_site(SameSite::Lax) .build(); - let cookie_user = Cookie::build(("user_id", user_id.clone())) - .path("/") - .http_only(false) - .same_site(SameSite::Lax) - .build(); - - let jar = jar.add(cookie_session).add(cookie_user); + let jar = jar.add(cookie_session); (jar, Redirect::to("/panel")) } diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index aba5936..1256072 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -5,47 +5,57 @@ use axum_extra::extract::CookieJar; use sqlx::{Row, query}; use std::sync::Arc; use tokio::sync::Mutex; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; pub async fn handle_login( jar: CookieJar, State(bot_state): State>>, ) -> Redirect { - let state_lock = bot_state.lock().await; + let db_pool = { + let state_lock = bot_state.lock().await; + + if let Some(pool) = &state_lock.db_pool { + pool.clone() + } else { + return Redirect::to("/error?message=Database+not+initialized"); + } + }; + + let bot_config = { + let state_lock = bot_state.lock().await; + + if let Some(config) = &state_lock.config { + config.clone() + } else { + return Redirect::to("/error?message=Bot+not+configured"); + } + }; let session_cookie = jar.get("session_id"); - let user_id = jar.get("user_id"); - if let (Some(session_cookie), Some(user_id)) = (session_cookie, user_id) { + if let Some(session_cookie) = session_cookie { let session_id = session_cookie.value(); - let user_id = user_id.value(); - - if let Some(db_pool) = &state_lock.db_pool { - if let Ok(row) = - query("SELECT expires_at FROM sessions_panel WHERE session_id = ? AND user_id = ?") - .bind(session_id) - .bind(user_id) - .fetch_one(db_pool) - .await - { - let expires_at: i64 = row.get::("expires_at"); - let current_timestamp = chrono::Utc::now().timestamp(); - - if expires_at > current_timestamp { - return Redirect::to("/panel"); - } + let user_id = get_user_id_from_session(session_id, &db_pool).await; + + if let Ok(row) = + query("SELECT expires_at FROM sessions_panel WHERE session_id = ? AND user_id = ?") + .bind(session_id) + .bind(user_id) + .fetch_one(&db_pool) + .await + { + let expires_at: i64 = row.get::("expires_at"); + let current_timestamp = chrono::Utc::now().timestamp(); + + if expires_at > current_timestamp { + return Redirect::to("/panel"); } } } - let Some(config) = &state_lock.config else { - return Redirect::to("/error?message=Missing+configuration"); - }; - - let client_id = config.bot.client_id; - let url = format!( "https://discord.com/oauth2/authorize?client_id={}&redirect_uri={}&response_type=code&scope=identify%20guilds", - client_id, + bot_config.bot.client_id, urlencoding::encode("http://localhost:3002/api/auth/callback") ); diff --git a/rustmail/src/api/handler/auth/logout.rs b/rustmail/src/api/handler/auth/logout.rs index 84556da..aff8936 100644 --- a/rustmail/src/api/handler/auth/logout.rs +++ b/rustmail/src/api/handler/auth/logout.rs @@ -4,6 +4,7 @@ use axum::response::Redirect; use axum_extra::extract::CookieJar; use std::sync::Arc; use tokio::sync::Mutex; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; pub async fn handle_logout( jar: CookieJar, @@ -12,11 +13,18 @@ pub async fn handle_logout( let state_lock = bot_state.lock().await; let session_cookie = jar.get("session_id"); - let user_id = jar.get("user_id"); - if let (Some(session_cookie), Some(user_id)) = (session_cookie, user_id) { + let db_pool = { + if let Some(pool) = &state_lock.db_pool { + pool.clone() + } else { + return Redirect::to("/error?message=Database+not+initialized"); + } + }; + + if let Some(session_cookie) = session_cookie { let session_id = session_cookie.value(); - let user_id = user_id.value(); + let user_id = get_user_id_from_session(&session_id, &db_pool).await; if let Some(db_pool) = &state_lock.db_pool { let _ = sqlx::query("DELETE FROM sessions_panel WHERE session_id = ? AND user_id = ?") diff --git a/rustmail/src/api/handler/user/avatar.rs b/rustmail/src/api/handler/user/avatar.rs index 4dc62cf..dd11793 100644 --- a/rustmail/src/api/handler/user/avatar.rs +++ b/rustmail/src/api/handler/user/avatar.rs @@ -2,36 +2,39 @@ use crate::BotState; use axum::extract::{Query, State}; use axum::response::IntoResponse; use std::sync::Arc; +use axum_extra::extract::CookieJar; +use sqlx::Row; use tokio::sync::Mutex; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct UserAvatar { pub avatar_url: Option, } -#[derive(serde::Deserialize)] -pub struct UserIdQuery { - id: String, -} - pub async fn handle_get_user_avatar( State(bot_state): State>>, - Query(params): Query, + jar: CookieJar, ) -> impl IntoResponse { - let state_lock = bot_state.lock().await; - let pool = match &state_lock.db_pool { - Some(pool) => pool.clone(), - None => { + let pool = { + let state_lock = bot_state.lock().await; + if let Some(pool) = &state_lock.db_pool { + pool.clone() + } else { return axum::response::Json(serde_json::json!(UserAvatar { avatar_url: None })); } }; - drop(state_lock); - - let user_id_str = params.id; + let session_id = match jar.get("session_id") { + Some(cookie) => cookie.value().to_string(), + None => { + return axum::response::Json(serde_json::json!(UserAvatar { avatar_url: None })); + } + }; - let user_id: u64 = match user_id_str.parse::() { + let user_id_str: String = get_user_id_from_session(&session_id, &pool).await; + let user_id: u64 = match user_id_str.parse() { Ok(id) => id, Err(e) => { eprintln!("Error parsing user ID: {}", e); @@ -39,18 +42,19 @@ pub async fn handle_get_user_avatar( } }; - let user_avatar = match sqlx::query!( - "SELECT avatar_hash FROM sessions_panel WHERE user_id = ?", - user_id_str - ) + let user_avatar = match sqlx::query( + "SELECT avatar_hash FROM sessions_panel WHERE user_id = ?") + .bind(user_id_str.clone()) .fetch_one(&pool) .await { Ok(record) => { - if !record.avatar_hash.is_empty() { + let avatar_hash: String = record.get::("avatar_hash"); + + if !avatar_hash.is_empty() { let avatar_url = format!( "https://cdn.discordapp.com/avatars/{}/{}.png", - user_id_str, record.avatar_hash + user_id_str, avatar_hash ); UserAvatar { avatar_url: Some(avatar_url), diff --git a/rustmail/src/api/middleware/auth.rs b/rustmail/src/api/middleware/auth.rs index 3feb9e6..1efefa6 100644 --- a/rustmail/src/api/middleware/auth.rs +++ b/rustmail/src/api/middleware/auth.rs @@ -10,6 +10,7 @@ use serenity::all::{GuildId, UserId}; use sqlx::{Row, query}; use std::sync::Arc; use tokio::sync::Mutex; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; async fn check_user_with_bot(bot_state: Arc>, user_id: &str) -> bool { let user_id_num = match user_id.parse::() { @@ -86,15 +87,11 @@ pub async fn auth_middleware( next: Next, ) -> Response { let session_cookie = jar.get("session_id"); - let user_cookie = jar.get("user_id"); - if session_cookie.is_none() || user_cookie.is_none() { + if session_cookie.is_none() { return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(); } - let session_id = session_cookie.unwrap().value().to_string(); - let user_id = user_cookie.unwrap().value().to_string(); - let db_pool = { let state_lock = bot_state.lock().await; match &state_lock.db_pool { @@ -109,6 +106,9 @@ pub async fn auth_middleware( } }; + let session_id = session_cookie.unwrap().value().to_string(); + let user_id = get_user_id_from_session(&session_id, &db_pool).await; + let guild_id = { let state_lock = bot_state.lock().await; match &state_lock.config { diff --git a/rustmail/src/api/mod.rs b/rustmail/src/api/mod.rs index 1f91172..ffd27fd 100644 --- a/rustmail/src/api/mod.rs +++ b/rustmail/src/api/mod.rs @@ -2,3 +2,4 @@ pub mod handler; pub mod middleware; pub mod router; pub mod routes; +pub mod utils; diff --git a/rustmail/src/api/utils/get_user_id_from_session.rs b/rustmail/src/api/utils/get_user_id_from_session.rs new file mode 100644 index 0000000..9feaa85 --- /dev/null +++ b/rustmail/src/api/utils/get_user_id_from_session.rs @@ -0,0 +1,19 @@ +use sqlx::{query, Row, SqlitePool}; + +pub async fn get_user_id_from_session(session_id: &str, db_pool: &SqlitePool) -> String{ + + let result = query("SELECT user_id FROM sessions_panel WHERE session_id = ?") + .bind(session_id) + .fetch_one(db_pool) + .await; + + match result { + Ok(row) => { + let user_id: String = row.get::("user_id"); + user_id + }, + Err(_) => { + "".to_string() + } + } +} \ No newline at end of file diff --git a/rustmail/src/api/utils/mod.rs b/rustmail/src/api/utils/mod.rs new file mode 100644 index 0000000..47ddc43 --- /dev/null +++ b/rustmail/src/api/utils/mod.rs @@ -0,0 +1 @@ +pub mod get_user_id_from_session; \ No newline at end of file diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index 44a177b..be41b64 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -2,8 +2,6 @@ use crate::components::navbar::RustmailNavbar; use gloo_net::http::Request; use gloo_utils::window; use wasm_bindgen_futures::spawn_local; -use web_sys::wasm_bindgen::JsCast; -use web_sys::HtmlDocument; use yew::prelude::*; #[derive(serde::Deserialize, Debug, serde::Serialize)] @@ -11,23 +9,6 @@ pub struct UserAvatar { pub avatar_url: Option, } -fn get_cookie(name: &str) -> Option { - let document = web_sys::window() - .unwrap() - .document() - .unwrap() - .dyn_into::() - .unwrap(); - - let cookies = document.cookie().unwrap_or_default(); - - cookies - .split(';') - .map(|s| s.trim()) - .find(|s| s.starts_with(&format!("{}=", name))) - .map(|s| s.split_at(name.len() + 1).1.to_string()) -} - #[function_component(Panel)] pub fn panel() -> Html { let (i18n, _set_language) = i18nrs::yew::use_translation(); @@ -58,23 +39,16 @@ pub fn panel() -> Html { }); } - let user_id: u64 = get_cookie("user_id") - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - let avatar = use_state(|| None::); { let avatar = avatar.clone(); - use_effect_with(user_id, move |&user_id| { + + use_effect_with((), move |()| { let avatar = avatar.clone(); + spawn_local(async move { - if user_id == 0 { - avatar.set(None); - return; - } - let url = format!("/api/user/avatar?id={}", user_id); - if let Ok(resp) = Request::get(&url).send().await { + if let Ok(resp) = Request::get(&"/api/user/avatar".to_string()).send().await { if let Ok(user_avatar) = resp.json::().await { avatar.set(user_avatar.avatar_url); } else { From 348c60e635952b79a358107f3c926016421cf57c Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sat, 18 Oct 2025 00:40:23 +0200 Subject: [PATCH 020/100] feat(api): add get status route to fetch bot's status --- rustmail/src/api/handler/bot/mod.rs | 1 + rustmail/src/api/handler/bot/status.rs | 18 ++++++++++++++++++ rustmail/src/api/routes/bot.rs | 6 ++++-- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 rustmail/src/api/handler/bot/status.rs diff --git a/rustmail/src/api/handler/bot/mod.rs b/rustmail/src/api/handler/bot/mod.rs index 99457f5..810d487 100644 --- a/rustmail/src/api/handler/bot/mod.rs +++ b/rustmail/src/api/handler/bot/mod.rs @@ -1,2 +1,3 @@ pub mod start; pub mod stop; +pub mod status; diff --git a/rustmail/src/api/handler/bot/status.rs b/rustmail/src/api/handler/bot/status.rs new file mode 100644 index 0000000..9421d16 --- /dev/null +++ b/rustmail/src/api/handler/bot/status.rs @@ -0,0 +1,18 @@ +use std::sync::Arc; +use axum::extract::State; +use axum::http::StatusCode; +use axum::Json; +use axum::response::IntoResponse; +use tokio::sync::Mutex; +use crate::BotState; + +pub async fn handle_status_bot( + State(bot_state): State>>, +) -> impl IntoResponse { + let state_lock = bot_state.lock().await; + + match state_lock.status { + crate::BotStatus::Running { .. } => (StatusCode::OK, Json(serde_json::json!({"status": "running"}))), + crate::BotStatus::Stopped => (StatusCode::OK, Json(serde_json::json!({"status": "stopped"}))), + } +} \ No newline at end of file diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index 817acd4..bddbdb9 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -2,14 +2,16 @@ use crate::BotState; use crate::api::handler::bot::start::handle_start_bot; use crate::api::handler::bot::stop::handle_stop_bot; use axum::Router; -use axum::routing::post; +use axum::routing::{get, post}; use std::sync::Arc; use tokio::sync::Mutex; +use crate::api::handler::bot::status::handle_status_bot; pub fn create_bot_router() -> Router>> { let bot_router = Router::new() .route("/start", post(handle_start_bot)) - .route("/stop", post(handle_stop_bot)); + .route("/stop", post(handle_stop_bot)) + .route("/status", get(handle_status_bot)); bot_router } From c9ed987ca81f89c617414678e6a4415cefa65c64 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sat, 18 Oct 2025 00:42:10 +0200 Subject: [PATCH 021/100] feat(panel): add configuration component for editing bot's config --- Cargo.lock | 2 + rustmail_panel/Cargo.toml | 2 + .../src/components/configuration.rs | 306 ++++++++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 rustmail_panel/src/components/configuration.rs diff --git a/Cargo.lock b/Cargo.lock index 5c8462d..d21dc53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2667,9 +2667,11 @@ dependencies = [ "gloo-net 0.6.0", "gloo-utils 0.2.0", "i18nrs", + "js-sys", "serde", "serde_json", "tailyew", + "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "yew", diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index bf32ead..2fe060d 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -15,3 +15,5 @@ serde_json = "1.0.145" i18nrs = { version = "0.1.7", features = ["yew"] } tailyew = "0.1.38" serde = { version = "1.0.228", features = ["derive"] } +js-sys = "0.3.77" +wasm-bindgen = "0.2.100" diff --git a/rustmail_panel/src/components/configuration.rs b/rustmail_panel/src/components/configuration.rs new file mode 100644 index 0000000..1547680 --- /dev/null +++ b/rustmail_panel/src/components/configuration.rs @@ -0,0 +1,306 @@ +use gloo_net::http::Request; +use wasm_bindgen_futures::spawn_local; +use yew::prelude::*; + +#[function_component(ConfigurationPage)] +pub fn configuration_page() -> Html { + let bot_status = use_state(|| "running".to_string()); + let is_loading = use_state(|| false); + + { + let bot_status = bot_status.clone(); + let is_loading = is_loading.clone(); + + use_effect_with((), move |_| { + spawn_local(async move { + is_loading.set(true); + + if let Ok(resp) = Request::get("/api/bot/status").send().await { + if resp.ok() { + if let Ok(json) = resp.json::().await { + if let Some(status) = json["status"].as_str() { + bot_status.set(status.to_string()); + } + } + } + } + + is_loading.set(false); + }); + + || () + }); + } + + let bot_token = use_state(|| "".to_string()); + let guild_id = use_state(|| "".to_string()); + let ticket_category = use_state(|| "".to_string()); + let log_channel = use_state(|| "".to_string()); + let support_role = use_state(|| "".to_string()); + let auto_close = use_state(|| "".to_string()); + let enable_transcripts = use_state(|| false); + + let handle_bot_action = { + let bot_status = bot_status.clone(); + let is_loading = is_loading.clone(); + + Callback::from(move |action: String| { + let bot_status = bot_status.clone(); + let is_loading = is_loading.clone(); + + spawn_local(async move { + is_loading.set(true); + + let url = format!("/api/bot/{}", action); + if let Ok(resp) = Request::post(&url).send().await { + if resp.ok() { + match action.as_str() { + "start" | "restart" => bot_status.set("running".to_string()), + "stop" => bot_status.set("stopped".to_string()), + _ => {} + } + } + } + + is_loading.set(false); + }); + }) + }; + + let handle_save = { + let bot_token = bot_token.clone(); + let guild_id = guild_id.clone(); + let ticket_category = ticket_category.clone(); + let log_channel = log_channel.clone(); + let support_role = support_role.clone(); + let auto_close = auto_close.clone(); + let enable_transcripts = enable_transcripts.clone(); + + Callback::from(move |_| { + let data = serde_json::json!({ + "bot_token": *bot_token, + "guild_id": *guild_id, + "ticket_category": *ticket_category, + "log_channel": *log_channel, + "support_role": *support_role, + "auto_close": *auto_close, + "enable_transcripts": *enable_transcripts, + }); + + spawn_local(async move { + let _ = Request::post("/api/bot/config") + .header("Content-Type", "application/json") + .body(serde_json::to_string(&data).unwrap()).expect("REASON") + .send() + .await; + }); + }) + }; + + html! { +
+
+ +
+

{"Configuration"}

+

{"Gérez les paramètres de votre bot Discord"}

+
+ +
+
+
+

{"Statut du Bot"}

+

{"Contrôlez l'état de votre bot"}

+
+ +
+
+ + { if *bot_status == "running" { "En ligne" } else { "Hors ligne" } } + +
+
+ +
+ + + + + +
+
+ +
+

{"Paramètres du Bot"}

+ +
+ +
+ + ().value()); + } + }} + class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

{"Le token de votre bot Discord"}

+
+ +
+ + ().value()); + } + }} + class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

{"L'ID de votre serveur Discord"}

+
+ +
+ + ().value()); + } + }} + class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

{"La catégorie où les tickets seront créés"}

+
+ +
+ + ().value()); + } + }} + class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

{"Le canal où les logs seront envoyés"}

+
+ +
+ + ().value()); + } + }} + class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

{"Le rôle qui peut gérer les tickets"}

+
+ +
+ + ().value()); + } + }} + class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +

{"Temps d'inactivité avant fermeture automatique"}

+
+ +
+
+ +

{"Sauvegarde automatique des conversations"}

+
+ + +
+ +
+ +
+
+
+
+
+ } +} From 260be7525eba4c44263c04baaabd3fa1f49d6045 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sat, 18 Oct 2025 00:43:03 +0200 Subject: [PATCH 022/100] feat(panel): add ticket component for showing closed tickets --- rustmail_panel/src/components/ticket.rs | 355 ++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 rustmail_panel/src/components/ticket.rs diff --git a/rustmail_panel/src/components/ticket.rs b/rustmail_panel/src/components/ticket.rs new file mode 100644 index 0000000..787cb7b --- /dev/null +++ b/rustmail_panel/src/components/ticket.rs @@ -0,0 +1,355 @@ +use gloo_net::http::Request; +use js_sys::Date; +use wasm_bindgen_futures::spawn_local; +use yew::prelude::*; +use serde::Deserialize; +use wasm_bindgen::JsValue; + +#[derive(Clone, PartialEq, Deserialize)] +pub struct Author { + pub name: String, + pub avatar: String, +} + +#[derive(Clone, PartialEq, Deserialize)] +pub struct Message { + pub id: String, + pub author: Author, + pub content: String, + pub timestamp: String, + #[serde(rename = "type")] + pub message_type: String, +} + +#[derive(Clone, PartialEq, Deserialize)] +pub struct Ticket { + pub id: String, + pub title: String, + pub author: Author, + pub category: String, + pub status: String, + pub created_at: String, + pub closed_at: Option, + pub messages: Vec, +} + +#[function_component(TicketsPage)] +pub fn tickets_page() -> Html { + let tickets = use_state(|| Vec::::new()); + let selected_ticket = use_state(|| None::); + let loading = use_state(|| true); + let selected_category = use_state(|| "all".to_string()); + let show_user_messages = use_state(|| true); + let show_staff_messages = use_state(|| true); + let show_system_messages = use_state(|| true); + + { + let tickets = tickets.clone(); + let loading = loading.clone(); + + use_effect_with((), move |_| { + let tickets_clone = tickets.clone(); + let loading_clone = loading.clone(); + + spawn_local(async move { + loading_clone.set(true); + + if let Ok(resp) = Request::get("/api/bot/tickets").send().await { + if let Ok(data) = resp.json::>().await { + tickets_clone.set(data); + } else { + tickets_clone.set(Vec::new()); + } + } else { + tickets_clone.set(Vec::new()); + } + + loading_clone.set(false); + }); + + || () + }); + } + + let categories = { + let mut cats: Vec = tickets.clone() + .iter() + .map(|t| t.category.clone()) + .collect::>() + .into_iter() + .collect(); + cats.sort(); + cats.insert(0, "all".into()); + cats + }; + + let filtered_tickets: Vec = { + if *selected_category == "all" { + (*tickets).clone() + } else { + tickets.iter() + .filter(|t| t.category == *selected_category) + .cloned() + .collect() + } + }; + + let get_filtered_messages = { + let show_user_messages = show_user_messages.clone(); + let show_staff_messages = show_staff_messages.clone(); + let show_system_messages = show_system_messages.clone(); + + move |ticket: &Ticket| -> Vec { + ticket.messages.iter().filter(|m| { + match m.message_type.as_str() { + "user" if !*show_user_messages => false, + "staff" if !*show_staff_messages => false, + "system" if !*show_system_messages => false, + _ => true, + } + }).cloned().collect() + } + }; + + let format_date = |date: &str| -> String { + let d = Date::new(&JsValue::from_str(date)); + let s = d.to_locale_string("fr-FR", &JsValue::UNDEFINED); + s.as_string().unwrap_or_else(|| date.to_string()) + }; + + let status_color = |status: &str| match status { + "open" => "bg-green-500", + "closed" => "bg-gray-500", + "pending" => "bg-yellow-500", + _ => "bg-gray-500", + }; + + html! { +
+
+ +
+

{"Tickets Fermés"}

+

{"Consultez l'historique des tickets fermés"}

+
+ +
+ +
+
+ +
+ + +
+ +
+ { + if *loading { + html! { +
+ + + + + {"Chargement..."} +
+ } + } else if filtered_tickets.is_empty() { + html! { +
{"Aucun ticket trouvé"}
+ } + } else { + html! { +
+ { for filtered_tickets.iter().map(|ticket| { + let is_selected = selected_ticket.as_ref().map(|t| t.id == ticket.id).unwrap_or(false); + let ticket_clone = ticket.clone(); + let selected_ticket = selected_ticket.clone(); + + html! { + + } + }) } +
+ } + } + } +
+
+
+ +
+
+ { + if let Some(ticket) = &*selected_ticket { + let messages = get_filtered_messages(ticket); + html! { + <> +
+
+
+

{ &ticket.title }

+
+ { &ticket.author.name } + {"•"} + { &ticket.category } + {"•"} + { format!("Ouvert: {}", format_date(&ticket.created_at)) } + { + if let Some(closed) = &ticket.closed_at { + html! { + <> + {"•"} + { format!("Fermé: {}", format_date(closed)) } + + } + } else { + html! {} + } + } +
+
+ +
+
+ { &ticket.status } +
+
+ +
+ + + + + +
+
+ +
+ { + if messages.is_empty() { + html! { +
+ {"Aucun message à afficher avec les filtres actuels"} +
+ } + } else { + html! { + { for messages.iter().map(|m| html! { +
+ +
+
+ { &m.author.name } + "bg-blue-500/20 text-blue-300", + "staff" => "bg-green-500/20 text-green-300", + _ => "bg-gray-500/20 text-gray-300" + } + )}> + { + match m.message_type.as_str() { + "user" => "Utilisateur", + "staff" => "Staff", + _ => "Système" + } + } + + { format_date(&m.timestamp) } +
+
+

{ &m.content }

+
+
+
+ }) } + } + } + } +
+ + } + } else { + html! { +
+ + + + {"Sélectionnez un ticket pour voir les détails"} +
+ } + } + } +
+
+
+
+
+ } +} From 872c62acc4d378c8e6c6a0461d0a73b123943f05 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sat, 18 Oct 2025 00:43:38 +0200 Subject: [PATCH 023/100] refactor(panel): adapt config and ticket component to home panel page --- rustmail_panel/src/components/mod.rs | 2 + rustmail_panel/src/components/navbar.rs | 132 ++++++++++++++++++++++-- rustmail_panel/src/pages/panel.rs | 50 +++++++-- rustmail_panel/tailwind.config.js | 2 +- 4 files changed, 167 insertions(+), 19 deletions(-) diff --git a/rustmail_panel/src/components/mod.rs b/rustmail_panel/src/components/mod.rs index 8d39904..bee6b70 100644 --- a/rustmail_panel/src/components/mod.rs +++ b/rustmail_panel/src/components/mod.rs @@ -2,3 +2,5 @@ pub mod home; pub mod language_switcher; pub mod logout_button; pub mod navbar; +pub mod configuration; +pub mod ticket; diff --git a/rustmail_panel/src/components/navbar.rs b/rustmail_panel/src/components/navbar.rs index 1211fa2..3363959 100644 --- a/rustmail_panel/src/components/navbar.rs +++ b/rustmail_panel/src/components/navbar.rs @@ -1,9 +1,12 @@ use crate::components::language_switcher::LanguageSwitcher; -use yew::{function_component, html, use_state, Callback, Html, Properties}; +use crate::pages::panel::Page; +use yew::{function_component, html, use_state, Callback, Html, Properties, classes}; #[derive(Properties, PartialEq, Clone)] pub struct RustmailNavbarProps { pub avatar_url: String, + pub on_page_change: Callback, + pub current_page: Page, } #[function_component(RustmailNavbar)] @@ -27,18 +30,68 @@ pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html {
- +
@@ -49,7 +102,7 @@ pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html { - + + + + +
} diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index be41b64..1a23a77 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -3,12 +3,21 @@ use gloo_net::http::Request; use gloo_utils::window; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; +use crate::components::configuration::ConfigurationPage; +use crate::components::ticket::TicketsPage; #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct UserAvatar { pub avatar_url: Option, } +#[derive(Clone, PartialEq)] +pub enum Page { + Home, + Configuration, + Tickets, +} + #[function_component(Panel)] pub fn panel() -> Html { let (i18n, _set_language) = i18nrs::yew::use_translation(); @@ -42,13 +51,11 @@ pub fn panel() -> Html { let avatar = use_state(|| None::); { let avatar = avatar.clone(); - use_effect_with((), move |()| { let avatar = avatar.clone(); spawn_local(async move { - - if let Ok(resp) = Request::get(&"/api/user/avatar".to_string()).send().await { + if let Ok(resp) = Request::get("/api/user/avatar").send().await { if let Ok(user_avatar) = resp.json::().await { avatar.set(user_avatar.avatar_url); } else { @@ -61,7 +68,9 @@ pub fn panel() -> Html { || () }); } + let avatar_url = (*avatar).clone().unwrap_or_default(); + let current_page = use_state(|| Page::Home); html! { <> @@ -74,12 +83,37 @@ pub fn panel() -> Html { }, Some(true) => html! { <> - + -
- Rustmail logo -

{"Rustmail Panel"}

-

{ i18n.t("panel.welcome") }

+
+
+ { + match &*current_page { + Page::Home => html! { +
+ Rustmail logo +

{"Rustmail Panel"}

+

+ { i18n.t("panel.welcome") } +

+
+ }, + Page::Configuration => html! { + + }, + Page::Tickets => html! { + + }, + } + } +
}, diff --git a/rustmail_panel/tailwind.config.js b/rustmail_panel/tailwind.config.js index b86ae22..ccb419d 100644 --- a/rustmail_panel/tailwind.config.js +++ b/rustmail_panel/tailwind.config.js @@ -1,7 +1,7 @@ module.exports = { mode: "jit", content: { - files: ["src/**/*.rs", "index.html", "assets/**/*.png"], + files: ["index.html", "src/**/*.rs", "assets/**/*.png"], }, darkMode: "media", theme: { From eb0e153c50aa7938d4b9a0a2bbc5501e7e388ffd Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sat, 18 Oct 2025 14:39:25 +0200 Subject: [PATCH 024/100] refactor(panel): protect bot's api routes with auth middleware --- rustmail/src/api/router.rs | 2 +- rustmail/src/api/routes/bot.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/rustmail/src/api/router.rs b/rustmail/src/api/router.rs index 726d7c2..3a667ee 100644 --- a/rustmail/src/api/router.rs +++ b/rustmail/src/api/router.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use tokio::sync::Mutex; pub fn create_api_router(bot_state: Arc>) -> Router { - let bot_router = create_bot_router(); + let bot_router = create_bot_router(bot_state.clone()); let auth_router = create_auth_router(); let panel_router = create_panel_router(bot_state.clone()); let user_router = create_user_router(bot_state.clone()); diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index bddbdb9..6a640e9 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -6,12 +6,17 @@ use axum::routing::{get, post}; use std::sync::Arc; use tokio::sync::Mutex; use crate::api::handler::bot::status::handle_status_bot; +use crate::api::middleware::auth::auth_middleware; -pub fn create_bot_router() -> Router>> { +pub fn create_bot_router(bot_state: Arc>) -> Router>> { let bot_router = Router::new() .route("/start", post(handle_start_bot)) .route("/stop", post(handle_stop_bot)) - .route("/status", get(handle_status_bot)); + .route("/status", get(handle_status_bot)) + .layer(axum::middleware::from_fn_with_state( + bot_state, + auth_middleware, + )); bot_router } From 741dc1b74617aaa0e73e815b7d92f6a8c9187cb1 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sat, 18 Oct 2025 18:24:23 +0200 Subject: [PATCH 025/100] feat(panel): added a system that certifies the authenticity of requests that the API makes to itself (see /api/bot/restart endpoint). --- Cargo.lock | 3 ++ rustmail/Cargo.toml | 3 ++ rustmail/src/api/handler/bot/mod.rs | 1 + rustmail/src/api/handler/bot/restart.rs | 49 +++++++++++++++++++++++++ rustmail/src/api/handler/bot/start.rs | 1 - rustmail/src/api/handler/bot/stop.rs | 1 + rustmail/src/api/middleware/auth.rs | 20 +++++++++- rustmail/src/api/routes/bot.rs | 2 + rustmail/src/api/utils/mod.rs | 3 +- rustmail/src/api/utils/ping_internal.rs | 11 ++++++ rustmail/src/bot.rs | 7 ++++ rustmail/src/main.rs | 6 ++- 12 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 rustmail/src/api/handler/bot/restart.rs create mode 100644 rustmail/src/api/utils/ping_internal.rs diff --git a/Cargo.lock b/Cargo.lock index d21dc53..7708ac5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2644,9 +2644,11 @@ dependencies = [ "async-trait", "axum", "axum-extra", + "base64 0.22.1", "chrono", "hyper 1.7.0", "mime_guess", + "rand", "regex", "reqwest 0.12.23", "rust-embed", @@ -2654,6 +2656,7 @@ dependencies = [ "serde_json", "serenity", "sqlx", + "subtle", "tokio", "toml", "urlencoding", diff --git a/rustmail/Cargo.toml b/rustmail/Cargo.toml index 2c4ae97..539ccc9 100644 --- a/rustmail/Cargo.toml +++ b/rustmail/Cargo.toml @@ -21,6 +21,9 @@ rust-embed = "8.7.2" mime_guess = "2.0.5" urlencoding = "2.1.3" serde_json = "1.0.145" +rand = "0.8.5" +base64 = "0.22.1" +subtle = "2.6.1" [dependencies.uuid] version = "1.18.1" diff --git a/rustmail/src/api/handler/bot/mod.rs b/rustmail/src/api/handler/bot/mod.rs index 810d487..169152d 100644 --- a/rustmail/src/api/handler/bot/mod.rs +++ b/rustmail/src/api/handler/bot/mod.rs @@ -1,3 +1,4 @@ pub mod start; pub mod stop; pub mod status; +pub mod restart; diff --git a/rustmail/src/api/handler/bot/restart.rs b/rustmail/src/api/handler/bot/restart.rs new file mode 100644 index 0000000..2337fd7 --- /dev/null +++ b/rustmail/src/api/handler/bot/restart.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; +use std::time::Duration; +use axum::extract::State; +use axum::http::StatusCode; +use axum::Json; +use sqlx::__rt::sleep; +use tokio::sync::Mutex; +use crate::{BotState, BotStatus}; +use crate::api::utils::ping_internal::ping_internal; + +pub async fn handle_restart_bot( + State(bot_state): State>>, +) -> (StatusCode, Json<&'static str>) { + let token = { + let state_lock = bot_state.lock().await; + state_lock.internal_token.clone() + }; + + let state_lock = bot_state.lock().await; + + match state_lock.status { + BotStatus::Stopped => { + drop(state_lock); + + let result = ping_internal("/api/bot/start", &token) + .await + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json("Failed to restart bot"))); + + println!("Restart result: {:?}", result); + + (StatusCode::OK, Json("Bot is starting")) + } + BotStatus::Running { .. } => { + drop(state_lock); + + let _ = ping_internal("/api/bot/stop", &token) + .await + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json("Failed to stop bot"))); + + sleep(Duration::from_secs(2)).await; + + let _ = ping_internal("/api/bot/start", &token) + .await + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json("Failed to stop bot"))); + + (StatusCode::OK, Json("Bot is restarting")) + }, + } +} \ No newline at end of file diff --git a/rustmail/src/api/handler/bot/start.rs b/rustmail/src/api/handler/bot/start.rs index be34c0f..e5d92c0 100644 --- a/rustmail/src/api/handler/bot/start.rs +++ b/rustmail/src/api/handler/bot/start.rs @@ -12,7 +12,6 @@ pub async fn handle_start_bot( State(bot_state): State>>, ) -> (StatusCode, Json<&'static str>) { let mut state_lock = bot_state.lock().await; - match state_lock.status { BotStatus::Stopped => { state_lock.config = load_config("config.toml"); diff --git a/rustmail/src/api/handler/bot/stop.rs b/rustmail/src/api/handler/bot/stop.rs index dc156e5..1f9e4b1 100644 --- a/rustmail/src/api/handler/bot/stop.rs +++ b/rustmail/src/api/handler/bot/stop.rs @@ -22,6 +22,7 @@ pub async fn handle_stop_bot( handle.await.unwrap(); (StatusCode::OK, Json("Bot stopped")) } else { + println!("Not Starting bot stop"); (StatusCode::CONFLICT, Json("Bot is not running")) } } diff --git a/rustmail/src/api/middleware/auth.rs b/rustmail/src/api/middleware/auth.rs index 1efefa6..6b5cb74 100644 --- a/rustmail/src/api/middleware/auth.rs +++ b/rustmail/src/api/middleware/auth.rs @@ -1,5 +1,7 @@ +use subtle::ConstantTimeEq; +use std::net::SocketAddr; use crate::{BotCommand, BotState, BotStatus}; -use axum::extract::Request; +use axum::extract::{ConnectInfo, Request}; use axum::extract::State; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; @@ -82,10 +84,26 @@ async fn verify_user(user_id: &str, guild_id: u64, bot_state: Arc>>, + ConnectInfo(addr): ConnectInfo, jar: CookieJar, req: Request, next: Next, ) -> Response { + + if addr.ip().is_loopback() { + if let Some(h) = req.headers().get("x-internal-call") { + if let Ok(s) = h.to_str() { + let state_lock = bot_state.lock().await; + let expected = state_lock.internal_token.as_bytes(); + + if expected.ct_eq(s.as_bytes()).unwrap_u8() == 1 { + drop(state_lock); + return next.run(req).await; + } + } + } + } + let session_cookie = jar.get("session_id"); if session_cookie.is_none() { diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index 6a640e9..acddeec 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -5,6 +5,7 @@ use axum::Router; use axum::routing::{get, post}; use std::sync::Arc; use tokio::sync::Mutex; +use crate::api::handler::bot::restart::handle_restart_bot; use crate::api::handler::bot::status::handle_status_bot; use crate::api::middleware::auth::auth_middleware; @@ -12,6 +13,7 @@ pub fn create_bot_router(bot_state: Arc>) -> Router Result { + let client = Client::new(); + let url = format!("http://127.0.0.1:3002{}", endpoint); + client + .post(&url) + .header("x-internal-call", token) + .send() + .await +} \ No newline at end of file diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 63a402c..0a09514 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -32,6 +32,8 @@ use serenity::cache::Settings as CacheSettings; use std::process; use std::sync::Arc; use std::time::Duration; +use base64::Engine; +use rand::RngCore; use tokio::sync::Mutex; use tokio::{select, spawn}; @@ -41,6 +43,10 @@ pub async fn init_bot_state() -> Arc> { .expect("An error occured!"); println!("Database connected!"); + let mut bytes = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut bytes); + let token = base64::engine::general_purpose::STANDARD.encode(&bytes); + let config = load_config("config.toml"); let (command_tx, _command_rx) = tokio::sync::mpsc::channel(32); @@ -51,6 +57,7 @@ pub async fn init_bot_state() -> Arc> { db_pool: Some(pool), command_tx: command_tx.clone(), bot_http: None, + internal_token: token, }; Arc::new(Mutex::new(bot_state)) diff --git a/rustmail/src/main.rs b/rustmail/src/main.rs index db999ec..dc01b82 100644 --- a/rustmail/src/main.rs +++ b/rustmail/src/main.rs @@ -6,6 +6,7 @@ use axum::response::Response; use rust_embed::RustEmbed; use serenity::http::Http; use std::borrow::Cow; +use std::net::SocketAddr; use std::sync::Arc; use tokio::signal; use tokio::sync::watch::Sender; @@ -53,6 +54,7 @@ struct BotState { db_pool: Option, command_tx: tokio::sync::mpsc::Sender, bot_http: Option>, + internal_token: String, } #[derive(RustEmbed)] @@ -120,10 +122,10 @@ async fn main() { .route("/", axum::routing::get(static_handler)) .route("/{*path}", axum::routing::get(static_handler)); - let listener = tokio::net::TcpListener::bind("0.0.0.0:3002").await.unwrap(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:3002").await.unwrap(); println!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app) + axum::serve(listener, app.into_make_service_with_connect_info::()) .with_graceful_shutdown(shutdown_signal()) .await .unwrap(); From 17f96ed1534b8fa2fa2a543ceb68023d83ba2fcb Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sat, 18 Oct 2025 19:20:00 +0200 Subject: [PATCH 026/100] refactor(db): add missing fields to threads table usefull for the rustmail panel --- migrations/20251018171827_permissions.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 migrations/20251018171827_permissions.sql diff --git a/migrations/20251018171827_permissions.sql b/migrations/20251018171827_permissions.sql new file mode 100644 index 0000000..7b06014 --- /dev/null +++ b/migrations/20251018171827_permissions.sql @@ -0,0 +1,15 @@ +-- Add migration script here +ALTER TABLE "threads" + ADD COLUMN "closed_at" DATETIME NULL; + +ALTER TABLE "threads" + ADD COLUMN "closed_by" TEXT NULL; + +ALTER TABLE "threads" + ADD COLUMN "category_id" TEXT NULL; + +ALTER TABLE "threads" + ADD COLUMN "category_name" TEXT NULL; + +ALTER TABLE "threads" + ADD COLUMN "required_permissions" TEXT NULL; From 8365799683fb5326de2adedd0eee5c2379605b90 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sat, 18 Oct 2025 19:42:01 +0200 Subject: [PATCH 027/100] refactor(db): add missing fields to scheduled_closures table usefull for the rustmail panel --- ...51018174044_update_scheduled_closure_table.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 migrations/20251018174044_update_scheduled_closure_table.sql diff --git a/migrations/20251018174044_update_scheduled_closure_table.sql b/migrations/20251018174044_update_scheduled_closure_table.sql new file mode 100644 index 0000000..ec84fa8 --- /dev/null +++ b/migrations/20251018174044_update_scheduled_closure_table.sql @@ -0,0 +1,15 @@ +-- Add migration script here +ALTER TABLE "scheduled_closures" + ADD COLUMN "closed_at" DATETIME NULL; + +ALTER TABLE "scheduled_closures" + ADD COLUMN "closed_by" TEXT NULL; + +ALTER TABLE "scheduled_closures" + ADD COLUMN "category_id" TEXT NULL; + +ALTER TABLE "scheduled_closures" + ADD COLUMN "category_name" TEXT NULL; + +ALTER TABLE "scheduled_closures" + ADD COLUMN "required_permissions" TEXT NULL; \ No newline at end of file From 64aa1e28e62769624953cf9dc212e6ca5ab81af9 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 19 Oct 2025 16:37:10 +0200 Subject: [PATCH 028/100] feat(api): add tickets route to fetch bot's tickets --- Cargo.lock | 7 + rustmail/src/api/handler/bot/mod.rs | 1 + rustmail/src/api/handler/bot/tickets.rs | 281 ++++++++++++++++++++++++ rustmail/src/api/handler/panel/panel.rs | 3 +- rustmail/src/api/handler/user/avatar.rs | 2 +- rustmail/src/api/routes/bot.rs | 2 + 6 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 rustmail/src/api/handler/bot/tickets.rs diff --git a/Cargo.lock b/Cargo.lock index 7708ac5..6ec4f8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1928,6 +1928,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "navigator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acd7563c38e388f6d2a568ae781a71ca4e957768655923526d2e0d64a67bc4d6" + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2671,6 +2677,7 @@ dependencies = [ "gloo-utils 0.2.0", "i18nrs", "js-sys", + "navigator", "serde", "serde_json", "tailyew", diff --git a/rustmail/src/api/handler/bot/mod.rs b/rustmail/src/api/handler/bot/mod.rs index 169152d..88ba233 100644 --- a/rustmail/src/api/handler/bot/mod.rs +++ b/rustmail/src/api/handler/bot/mod.rs @@ -2,3 +2,4 @@ pub mod start; pub mod stop; pub mod status; pub mod restart; +pub mod tickets; diff --git a/rustmail/src/api/handler/bot/tickets.rs b/rustmail/src/api/handler/bot/tickets.rs new file mode 100644 index 0000000..aee7180 --- /dev/null +++ b/rustmail/src/api/handler/bot/tickets.rs @@ -0,0 +1,281 @@ +use std::sync::Arc; +use axum::{ + extract::{Query, State}, + http::StatusCode, + Json, +}; +use tokio::sync::Mutex; +use serde::{Deserialize, Serialize}; +use crate::BotState; + +#[derive(Debug, Clone, Serialize)] +pub struct ThreadMessage { + pub id: i64, + pub thread_id: String, + pub user_id: i64, + pub user_name: String, + pub is_anonymous: bool, + pub dm_message_id: Option, + pub inbox_message_id: Option, + pub message_number: Option, + pub created_at: String, + pub content: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CompleteThread { + pub id: String, + pub user_id: i64, + pub user_name: String, + pub channel_id: String, + pub created_at: i64, + pub new_message_number: i64, + pub status: i64, + pub user_left: bool, + pub closed_at: Option, + pub closed_by: Option, + pub category_id: Option, + pub category_name: Option, + pub required_permissions: Option, + pub messages: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct TicketQuery { + pub id: Option, +} + +pub async fn handle_tickets_bot( + State(bot_state): State>>, + Query(params): Query, +) -> (StatusCode, Json) { + let db_pool = { + let state_lock = bot_state.lock().await; + match &state_lock.db_pool { + Some(pool) => pool.clone(), + None => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "Database pool not initialized" + })), + ); + } + } + }; + + if let Some(id) = params.id { + + let thread = match sqlx::query!( + r#" + SELECT + id, + user_id, + user_name, + channel_id, + created_at as "created_at: String", + next_message_number as new_message_number, + status, + user_left, + closed_at as "closed_at: Option", + closed_by, + category_id, + category_name, + required_permissions + FROM threads + WHERE id = ? + "#, + id + ) + .fetch_optional(&db_pool) + .await + { + Ok(Some(row)) => row, + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": "Thread not found" + })), + ); + } + Err(err) => { + eprintln!("Erreur SQL thread {}: {:?}", id, err); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "Failed to fetch thread" + })), + ); + } + }; + + let messages_query = match sqlx::query!( + r#" + SELECT + id, + thread_id, + user_id, + user_name, + is_anonymous, + dm_message_id, + inbox_message_id, + message_number, + created_at as "created_at: String", + content + FROM thread_messages + WHERE thread_id = ? + ORDER BY created_at ASC + "#, + thread.id + ) + .fetch_all(&db_pool) + .await + { + Ok(rows) => rows, + Err(err) => { + eprintln!("Erreur SQL messages pour {}: {:?}", thread.id, err); + Vec::new() + } + }; + + let messages: Vec = messages_query + .into_iter() + .map(|m| ThreadMessage { + id: m.id, + thread_id: m.thread_id, + user_id: m.user_id, + user_name: m.user_name, + is_anonymous: m.is_anonymous, + dm_message_id: m.dm_message_id, + inbox_message_id: m.inbox_message_id, + message_number: m.message_number, + created_at: m.created_at, + content: m.content, + }) + .collect(); + + let complete = CompleteThread { + id: thread.id, + user_id: thread.user_id, + user_name: thread.user_name, + channel_id: thread.channel_id, + created_at: thread.created_at.parse::().unwrap_or_default(), + new_message_number: thread.new_message_number.unwrap_or_default(), + status: thread.status, + user_left: thread.user_left, + closed_at: thread.closed_at.and_then(|ts| ts?.parse::().ok()), + closed_by: thread.closed_by, + category_id: thread.category_id, + category_name: thread.category_name, + required_permissions: thread.required_permissions, + messages, + }; + + return (StatusCode::OK, Json(serde_json::json!(complete))); + } + + let threads_query = match sqlx::query!( + r#" + SELECT + id, + user_id, + user_name, + channel_id, + created_at as "created_at: String", + next_message_number as new_message_number, + status, + user_left, + closed_at as "closed_at: Option", + closed_by, + category_id, + category_name, + required_permissions + FROM threads + WHERE status = 0 + ORDER BY closed_at DESC + "# + ) + .fetch_all(&db_pool) + .await + { + Ok(rows) => rows, + Err(err) => { + eprintln!("Erreur SQL threads: {:?}", err); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": "Failed to fetch threads" + })), + ); + } + }; + + let mut threads: Vec = Vec::new(); + + for thread in threads_query { + let messages_query = match sqlx::query!( + r#" + SELECT + id, + thread_id, + user_id, + user_name, + is_anonymous, + dm_message_id, + inbox_message_id, + message_number, + created_at as "created_at: String", + content + FROM thread_messages + WHERE thread_id = ? + ORDER BY created_at ASC + "#, + thread.id + ) + .fetch_all(&db_pool) + .await + { + Ok(rows) => rows, + Err(err) => { + eprintln!("Erreur SQL messages pour {}: {:?}", thread.id, err); + Vec::new() + } + }; + + let messages: Vec = messages_query + .into_iter() + .map(|m| ThreadMessage { + id: m.id, + thread_id: m.thread_id, + user_id: m.user_id, + user_name: m.user_name, + is_anonymous: m.is_anonymous, + dm_message_id: m.dm_message_id, + inbox_message_id: m.inbox_message_id, + message_number: m.message_number, + created_at: m.created_at, + content: m.content, + }) + .collect(); + + threads.push(CompleteThread { + id: thread.id, + user_id: thread.user_id, + user_name: thread.user_name, + channel_id: thread.channel_id, + created_at: thread.created_at.parse::().unwrap_or_default(), + new_message_number: thread.new_message_number.unwrap_or_default(), + status: thread.status, + user_left: thread.user_left, + closed_at: thread.closed_at.and_then(|ts| ts?.parse::().ok()), + closed_by: thread.closed_by, + category_id: thread.category_id, + category_name: thread.category_name, + required_permissions: thread.required_permissions, + messages, + }); + } + + (StatusCode::OK, Json(serde_json::json!(threads))) +} diff --git a/rustmail/src/api/handler/panel/panel.rs b/rustmail/src/api/handler/panel/panel.rs index 177ee59..ad61332 100644 --- a/rustmail/src/api/handler/panel/panel.rs +++ b/rustmail/src/api/handler/panel/panel.rs @@ -1,7 +1,6 @@ use crate::BotState; use axum::extract::State; -use axum::response::{IntoResponse, Redirect}; -use hyper::StatusCode; +use axum::response::IntoResponse; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/user/avatar.rs b/rustmail/src/api/handler/user/avatar.rs index dd11793..b350042 100644 --- a/rustmail/src/api/handler/user/avatar.rs +++ b/rustmail/src/api/handler/user/avatar.rs @@ -1,5 +1,5 @@ use crate::BotState; -use axum::extract::{Query, State}; +use axum::extract::State; use axum::response::IntoResponse; use std::sync::Arc; use axum_extra::extract::CookieJar; diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index acddeec..ff977a8 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use tokio::sync::Mutex; use crate::api::handler::bot::restart::handle_restart_bot; use crate::api::handler::bot::status::handle_status_bot; +use crate::api::handler::bot::tickets::{handle_tickets_bot}; use crate::api::middleware::auth::auth_middleware; pub fn create_bot_router(bot_state: Arc>) -> Router>> { @@ -15,6 +16,7 @@ pub fn create_bot_router(bot_state: Arc>) -> Router Date: Sun, 19 Oct 2025 16:40:41 +0200 Subject: [PATCH 029/100] refactor(close): registered all missing threads data when closing a thread --- .../src/commands/close/slash_command/close.rs | 25 +++++--- .../src/commands/close/text_command/close.rs | 60 +++++++++++++++++-- rustmail/src/commands/mod.rs | 8 +-- rustmail/src/commands/move_thread/common.rs | 24 -------- .../src/commands/reply/text_command/reply.rs | 2 +- rustmail/src/config.rs | 2 - 6 files changed, 77 insertions(+), 44 deletions(-) diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index 12aac07..08bfa75 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -10,15 +10,13 @@ use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::fetch_thread::fetch_thread; use chrono::Utc; -use serenity::all::{ - CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, - CreateCommandOption, GuildId, ResolvedOption, UserId, -}; +use serenity::all::{CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, GuildId, ResolvedOption, UserId}; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tokio::sync::watch::Receiver; use tokio::time::sleep; +use crate::utils::command::category::{get_category_id_from_command, get_category_name_from_command, get_required_permissions_channel_from_command}; pub struct CloseCommand; @@ -238,8 +236,14 @@ impl RegistrableCommand for CloseCommand { let thread_id = thread.id.clone(); let close_at = Utc::now().timestamp() + delay.as_secs() as i64; let silent = silent.unwrap_or(false); + + let closed_by = command.user.id.to_string(); + let category_id = get_category_id_from_command(&ctx, &command).await; + let category_name = get_category_name_from_command(&ctx, &command).await; + let required_permissions = get_required_permissions_channel_from_command(&ctx, &command).await; + if let Err(e) = - upsert_scheduled_closure(&thread_id, close_at, silent, db_pool).await + upsert_scheduled_closure(&thread_id, close_at, silent, &closed_by, &category_id, &category_name, &required_permissions.to_string(), db_pool).await { eprintln!("Failed to persist scheduled closure: {e:?}"); } @@ -256,7 +260,7 @@ impl RegistrableCommand for CloseCommand { get_scheduled_closure(&thread_id_for_task, pool).await { if record.close_at <= Utc::now().timestamp() { - let _ = close_thread(&thread_id_for_task, pool).await; + let _ = close_thread(&thread_id_for_task, &record.closed_by, &record.category_id, &record.category_name, record.required_permissions.parse::().unwrap_or(0), pool).await; let _ = delete_scheduled_closure(&thread_id_for_task, pool).await; let community_guild_id = @@ -290,7 +294,7 @@ impl RegistrableCommand for CloseCommand { get_scheduled_closure(&thread_id_again, pool2).await { if r2.close_at <= Utc::now().timestamp() { - let _ = close_thread(&thread_id_again, pool2).await; + let _ = close_thread(&thread_id_again, &r2.closed_by, &r2.category_id, &r2.category_name, r2.required_permissions.parse::().unwrap_or(0), pool2).await; let _ = delete_scheduled_closure( &thread_id_again, pool2, @@ -327,6 +331,11 @@ impl RegistrableCommand for CloseCommand { let user_still_member = community_guild_id.member(&ctx.http, user_id).await.is_ok(); + let closed_by = command.user.id.to_string(); + let category_id = get_category_id_from_command(&ctx, &command).await; + let category_name = get_category_name_from_command(&ctx, &command).await; + let required_permissions = get_required_permissions_channel_from_command(&ctx, &command).await; + if user_still_member && let Some(silent) = silent && !silent @@ -355,7 +364,7 @@ impl RegistrableCommand for CloseCommand { let _ = command.create_followup(&ctx.http, response).await; } - close_thread(&thread.id, db_pool).await?; + close_thread(&thread.id, &closed_by, &category_id, &category_name, required_permissions, db_pool).await?; let _ = delete_scheduled_closure(&thread.id, db_pool).await; let _ = command.channel_id.delete(&ctx.http).await?; diff --git a/rustmail/src/commands/close/text_command/close.rs b/rustmail/src/commands/close/text_command/close.rs index 1305c52..b1ceb36 100644 --- a/rustmail/src/commands/close/text_command/close.rs +++ b/rustmail/src/commands/close/text_command/close.rs @@ -7,12 +7,13 @@ use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::fetch_thread::fetch_thread; use chrono::Utc; -use serenity::all::{Context, GuildId, Message, UserId}; +use serenity::all::{Channel, Context, GuildId, Message, PermissionOverwriteType, RoleId, UserId}; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use tokio::sync::watch::Receiver; use tokio::time::sleep; +use crate::utils::message::category::{get_category_id_from_message, get_category_name_from_message, get_required_permissions_channel_from_message}; pub async fn close( ctx: &Context, @@ -169,9 +170,53 @@ pub async fn close( .await; }; + let closed_by = msg.author.id.to_string(); + let category_id = match msg.channel_id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.category() { + Some(category) => category.id.to_string(), + None => String::new(), + } + } + _ => String::new(), + }; + let category_name = match msg.channel_id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.category() { + Some(category) => category.name.clone(), + None => String::new(), + } + } + _ => String::new(), + }; + + let required_permissions = match msg.channel_id.to_channel(&ctx.http).await { + Ok(Channel::Guild(guild_channel)) => { + let guild_id = guild_channel.guild_id; + let guild = guild_id.to_partial_guild(&ctx.http).await.ok(); + + let everyone_role_id = RoleId::new(guild_id.get()); + + let mut perms = guild + .and_then(|g| g.roles.get(&everyone_role_id).map(|r| r.permissions.bits())) + .unwrap_or(0u64); + + for overwrite in &guild_channel.permission_overwrites { + if let PermissionOverwriteType::Role(_) = overwrite.kind { + let allow = overwrite.allow.bits(); + let deny = overwrite.deny.bits(); + perms = (perms & !deny) | allow; + } + } + + perms + } + _ => 0u64, + }; + let thread_id = thread.id.clone(); let close_at = Utc::now().timestamp() + delay.as_secs() as i64; - if let Err(e) = upsert_scheduled_closure(&thread_id, close_at, silent, db_pool).await { + if let Err(e) = upsert_scheduled_closure(&thread_id, close_at, silent, &closed_by, &category_id, &category_name, &required_permissions.to_string(), db_pool).await { eprintln!("Failed to persist scheduled closure: {e:?}"); } let channel_id = msg.channel_id; @@ -185,7 +230,7 @@ pub async fn close( if let Some(pool) = config_clone.db_pool.as_ref() { if let Ok(Some(record)) = get_scheduled_closure(&thread_id_for_task, pool).await { if record.close_at <= Utc::now().timestamp() { - let _ = close_thread(&thread_id_for_task, pool).await; + let _ = close_thread(&thread_id_for_task, &record.closed_by, &record.category_id, &category_name, record.required_permissions.parse::().unwrap_or(0), pool).await; let _ = delete_scheduled_closure(&thread_id_for_task, pool).await; let community_guild_id = @@ -217,7 +262,7 @@ pub async fn close( get_scheduled_closure(&thread_id_again, pool2).await { if r2.close_at <= Utc::now().timestamp() { - let _ = close_thread(&thread_id_again, pool2).await; + let _ = close_thread(&thread_id_again, &r2.closed_by, &r2.category_id, &r2.category_id, r2.required_permissions.parse::().unwrap_or(0), pool2).await; let _ = delete_scheduled_closure(&thread_id_again, pool2).await; let community_guild_id = GuildId::new( @@ -251,6 +296,11 @@ pub async fn close( let user_still_member = community_guild_id.member(&ctx.http, user_id).await.is_ok(); + let closed_by = msg.author.id.to_string(); + let category_id = get_category_id_from_message(&ctx, &msg).await; + let category_name = get_category_name_from_message(&ctx, &msg).await; + let required_permissions = get_required_permissions_channel_from_message(&ctx, &msg).await; + if user_still_member && !silent { let _ = MessageBuilder::system_message(ctx, config) .content(&config.bot.close_message) @@ -274,7 +324,7 @@ pub async fn close( .await; } - close_thread(&thread.id, db_pool).await?; + close_thread(&thread.id, &closed_by, &category_id, &category_name, required_permissions, db_pool).await?; let _ = delete_scheduled_closure(&thread.id, db_pool).await; let _ = msg.channel_id.delete(&ctx.http).await?; diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index a3cf5f4..ab552ba 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -42,19 +42,19 @@ pub trait RegistrableCommand: Send + Sync { pub struct CommandRegistry { commands: HashMap<&'static str, Arc>, - shutdown: Arc>, + _shutdown: Arc>, } impl CommandRegistry { pub fn new(shutdown: Receiver) -> Self { Self { commands: HashMap::new(), - shutdown: Arc::new(shutdown), + _shutdown: Arc::new(shutdown), } } - pub fn shutdown(&self) -> Arc> { - self.shutdown.clone() + pub fn _shutdown(&self) -> Arc> { + self._shutdown.clone() } pub fn register_command(&mut self, command: C) { diff --git a/rustmail/src/commands/move_thread/common.rs b/rustmail/src/commands/move_thread/common.rs index 4e7d50d..6c2b9dd 100644 --- a/rustmail/src/commands/move_thread/common.rs +++ b/rustmail/src/commands/move_thread/common.rs @@ -66,30 +66,6 @@ pub async fn move_channel_to_category_by_command_option( .await } -pub async fn send_error_message( - ctx: &Context, - msg: &Message, - config: &Config, - error_key: &str, - params: Option<&HashMap>, -) { - let error_msg = get_translated_message( - config, - error_key, - params, - Some(msg.author.id), - msg.guild_id.map(|g| g.get()), - None, - ) - .await; - - let _ = MessageBuilder::system_message(ctx, config) - .content(error_msg) - .to_channel(msg.channel_id) - .send() - .await; -} - pub async fn send_success_message( ctx: &Context, msg: &Message, diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index 77872a0..ac0201b 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -15,7 +15,7 @@ pub async fn reply( ctx: &Context, msg: &Message, config: &Config, - shutdown: Arc>, + _shutdown: Arc>, ) -> ModmailResult<()> { let db_pool = config .db_pool diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index 23f0df9..7723ecd 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -7,8 +7,6 @@ use sqlx::SqlitePool; use std::fs; use std::sync::Arc; -pub const MODMAIL_MANAGED_TOPIC: &str = "modmail:managed"; - #[derive(Debug, Deserialize, Clone)] pub struct Config { pub bot: BotConfig, From 9e25d629927130d48fae8ccd710d1832a7523663 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 19 Oct 2025 16:41:51 +0200 Subject: [PATCH 030/100] feat(utils): add functions to retrieve data from closed threads --- rustmail/src/utils/command/category.rs | 66 ++++++++++++++++++++++++++ rustmail/src/utils/message/category.rs | 65 +++++++++++++++++++++++++ rustmail/src/utils/thread/category.rs | 65 +++++++++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 rustmail/src/utils/command/category.rs create mode 100644 rustmail/src/utils/message/category.rs create mode 100644 rustmail/src/utils/thread/category.rs diff --git a/rustmail/src/utils/command/category.rs b/rustmail/src/utils/command/category.rs new file mode 100644 index 0000000..2780432 --- /dev/null +++ b/rustmail/src/utils/command/category.rs @@ -0,0 +1,66 @@ +use serenity::all::{Channel, Context, PermissionOverwriteType, RoleId}; +use serenity::all::CommandInteraction; + +pub async fn get_category_id_from_command(ctx: &Context, command: &CommandInteraction) -> String { + match command.channel_id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => category_id.to_string(), + None => String::new(), + }, + None => String::new(), + } + } + _ => String::new(), + } +} + +pub async fn get_category_name_from_command(ctx: &Context, command: &CommandInteraction) -> String { + match command.channel_id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => { + match category_id.name(&ctx.http).await { + Ok(category_name) => category_name.clone(), + _ => String::new(), + } + } + None => String::new(), + }, + None => String::new(), + } + } + _ => String::new(), + } +} + +pub async fn get_required_permissions_channel_from_command( + ctx: &Context, + command: &CommandInteraction, +) -> u64 { + match command.channel_id.to_channel(&ctx.http).await { + Ok(Channel::Guild(guild_channel)) => { + let guild_id = guild_channel.guild_id; + let guild = guild_id.to_partial_guild(&ctx.http).await.ok(); + + let everyone_role_id = RoleId::new(guild_id.get()); + + let mut perms = guild + .and_then(|g| g.roles.get(&everyone_role_id).map(|r| r.permissions.bits())) + .unwrap_or(0u64); + + for overwrite in &guild_channel.permission_overwrites { + if let PermissionOverwriteType::Role(_) = overwrite.kind { + let allow = overwrite.allow.bits(); + let deny = overwrite.deny.bits(); + perms = (perms & !deny) | allow; + } + } + + perms + } + _ => 0u64, + } +} \ No newline at end of file diff --git a/rustmail/src/utils/message/category.rs b/rustmail/src/utils/message/category.rs new file mode 100644 index 0000000..8b11e1a --- /dev/null +++ b/rustmail/src/utils/message/category.rs @@ -0,0 +1,65 @@ +use serenity::all::{Channel, Context, Message, PermissionOverwriteType, RoleId}; + +pub async fn get_category_id_from_message(ctx: &Context, message: &Message) -> String { + match message.channel_id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => category_id.to_string(), + None => String::new(), + }, + None => String::new(), + } + } + _ => String::new(), + } +} + +pub async fn get_category_name_from_message(ctx: &Context, message: &Message) -> String { + match message.channel_id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => { + match category_id.name(&ctx.http).await { + Ok(category_name) => category_name.clone(), + _ => String::new(), + } + } + None => String::new(), + }, + None => String::new(), + } + } + _ => String::new(), + } +} + +pub async fn get_required_permissions_channel_from_message( + ctx: &Context, + message: &Message, +) -> u64 { + match message.channel_id.to_channel(&ctx.http).await { + Ok(Channel::Guild(guild_channel)) => { + let guild_id = guild_channel.guild_id; + let guild = guild_id.to_partial_guild(&ctx.http).await.ok(); + + let everyone_role_id = RoleId::new(guild_id.get()); + + let mut perms = guild + .and_then(|g| g.roles.get(&everyone_role_id).map(|r| r.permissions.bits())) + .unwrap_or(0u64); + + for overwrite in &guild_channel.permission_overwrites { + if let PermissionOverwriteType::Role(_) = overwrite.kind { + let allow = overwrite.allow.bits(); + let deny = overwrite.deny.bits(); + perms = (perms & !deny) | allow; + } + } + + perms + } + _ => 0u64, + } +} \ No newline at end of file diff --git a/rustmail/src/utils/thread/category.rs b/rustmail/src/utils/thread/category.rs new file mode 100644 index 0000000..f03d94d --- /dev/null +++ b/rustmail/src/utils/thread/category.rs @@ -0,0 +1,65 @@ +use serenity::all::{Channel, Context, GuildChannel, PermissionOverwriteType, RoleId}; + +pub async fn get_category_id_from_guild_channel(ctx: &Context, channel: &GuildChannel) -> String { + match channel.id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => category_id.to_string(), + None => String::new(), + }, + None => String::new(), + } + } + _ => String::new(), + } +} + +pub async fn get_category_name_from_guild_channel(ctx: &Context, channel: &GuildChannel) -> String { + match channel.id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => { + match category_id.name(&ctx.http).await { + Ok(category_name) => category_name.clone(), + _ => String::new(), + } + } + None => String::new(), + }, + None => String::new(), + } + } + _ => String::new(), + } +} + +pub async fn get_required_permissions_channel_from_guild_channel( + ctx: &Context, + channel: &GuildChannel, +) -> u64 { + match channel.id.to_channel(&ctx.http).await { + Ok(Channel::Guild(guild_channel)) => { + let guild_id = guild_channel.guild_id; + let guild = guild_id.to_partial_guild(&ctx.http).await.ok(); + + let everyone_role_id = RoleId::new(guild_id.get()); + + let mut perms = guild + .and_then(|g| g.roles.get(&everyone_role_id).map(|r| r.permissions.bits())) + .unwrap_or(0u64); + + for overwrite in &guild_channel.permission_overwrites { + if let PermissionOverwriteType::Role(_) = overwrite.kind { + let allow = overwrite.allow.bits(); + let deny = overwrite.deny.bits(); + perms = (perms & !deny) | allow; + } + } + + perms + } + _ => 0u64, + } +} \ No newline at end of file From 5f8b3749ec98f223c10d7af6cd04a406a4215589 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 19 Oct 2025 16:44:10 +0200 Subject: [PATCH 031/100] refactor(db): deleted useless db functions --- rustmail/src/db/operations/messages.rs | 73 ------------------------- rustmail/src/db/operations/mod.rs | 2 +- rustmail/src/db/operations/scheduled.rs | 57 ++++++++++++++++--- rustmail/src/db/operations/threads.rs | 28 +++++++++- 4 files changed, 75 insertions(+), 85 deletions(-) diff --git a/rustmail/src/db/operations/messages.rs b/rustmail/src/db/operations/messages.rs index 2896d78..9b4f4bb 100644 --- a/rustmail/src/db/operations/messages.rs +++ b/rustmail/src/db/operations/messages.rs @@ -55,49 +55,6 @@ pub async fn insert_staff_message( Ok(()) } -pub async fn insert_staff_message_from_command( - inbox_msg_id: String, - inbox_msg_content: &str, - dm_msg_id: Option, - thread_id: &str, - staff_user_id: UserId, - is_anonymous: bool, - pool: &SqlitePool, - config: &Config, - message_number: u64, -) -> Result<(), Error> { - let user_id = staff_user_id.get() as i64; - - let user_name = get_user_name_from_thread_id(thread_id, pool) - .await - .unwrap_or_else(|| "Unknown".to_string()); - - let message_number_i64 = message_number as i64; - - sqlx::query!( - r#" - INSERT INTO thread_messages ( - thread_id, user_id, user_name, is_anonymous, dm_message_id, inbox_message_id, message_number, content, thread_status - ) VALUES ( - ?, ?, ?, ?, ?, ?, ?, ?, ? - ) - "#, - thread_id, - user_id, - user_name, - is_anonymous, - dm_msg_id, - inbox_msg_id, - message_number_i64, - inbox_msg_content, - 1 - ) - .execute(pool) - .await?; - - Ok(()) -} - pub async fn get_message_ids_by_number( message_number: i64, _user_id: UserId, @@ -234,36 +191,6 @@ pub async fn get_latest_thread_message( Ok(latest) } -pub async fn insert_recovered_message( - thread_id: &str, - user_id: i64, - user_name: &str, - dm_message_id: &str, - content: &str, - pool: &SqlitePool, -) -> Result<(), Error> { - sqlx::query!( - r#" - INSERT INTO thread_messages ( - thread_id, user_id, user_name, is_anonymous, dm_message_id, content, thread_status - ) VALUES ( - ?, ?, ?, ?, ?, ?, ? - ) - "#, - thread_id, - user_id, - user_name, - false, - dm_message_id, - content, - 1 - ) - .execute(pool) - .await?; - - Ok(()) -} - pub async fn get_message_ids_by_message_id( message_id: &str, pool: &SqlitePool, diff --git a/rustmail/src/db/operations/mod.rs b/rustmail/src/db/operations/mod.rs index 8fe0a6e..bdbe213 100644 --- a/rustmail/src/db/operations/mod.rs +++ b/rustmail/src/db/operations/mod.rs @@ -9,7 +9,7 @@ pub use features::{get_feature_message, upsert_feature_message}; pub use init::{get_last_recovery_timestamp, init_database, update_last_recovery_timestamp}; pub use messages::{ delete_message, get_latest_thread_message, get_message_ids_by_message_id, - get_message_ids_by_number, get_thread_message_by_inbox_message_id, insert_recovered_message, + get_message_ids_by_number, get_thread_message_by_inbox_message_id, insert_staff_message, insert_user_message_with_ids, update_message_content, update_message_numbers_after_deletion, }; diff --git a/rustmail/src/db/operations/scheduled.rs b/rustmail/src/db/operations/scheduled.rs index e00b39c..94705d0 100644 --- a/rustmail/src/db/operations/scheduled.rs +++ b/rustmail/src/db/operations/scheduled.rs @@ -6,24 +6,57 @@ pub struct ScheduledClosure { pub thread_id: String, pub close_at: i64, pub silent: bool, + pub closed_by: String, + pub category_id: String, + pub category_name: String, + pub required_permissions: String, } pub async fn upsert_scheduled_closure( thread_id: &str, close_at: i64, silent: bool, + closed_by: &str, + category_id: &str, + category_name: &str, + required_permissions: &str, pool: &SqlitePool, ) -> ModmailResult<()> { sqlx::query( - r#"INSERT INTO scheduled_closures(thread_id, close_at, silent) VALUES(?, ?, ?) - ON CONFLICT(thread_id) DO UPDATE SET close_at=excluded.close_at, silent=excluded.silent"#, + r#" + INSERT INTO scheduled_closures ( + thread_id, + close_at, + silent, + closed_by, + category_id, + category_name, + required_permissions + ) VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(thread_id) + DO UPDATE SET + close_at = excluded.close_at, + silent = excluded.silent, + closed_by = excluded.closed_by, + category_id = excluded.category_id, + category_name = excluded.category_name, + required_permissions = excluded.required_permissions + "#, ) - .bind(thread_id) - .bind(close_at) - .bind(silent) - .execute(pool) - .await - .map_err(|_| common::validation_failed("Failed to upsert scheduled closure"))?; + .bind(thread_id) + .bind(close_at) + .bind(silent) + .bind(closed_by) + .bind(category_id) + .bind(category_name) + .bind(required_permissions) + .execute(pool) + .await + .map_err(|e| { + eprintln!("Failed to upsert scheduled closure: {e:?}"); + common::validation_failed("Failed to upsert scheduled closure") + })?; + Ok(()) } @@ -52,6 +85,10 @@ pub async fn get_scheduled_closure( thread_id: row.get::(0), close_at: row.get::(1), silent: row.get::(2) != 0, + closed_by: row.get::(3), + category_id: row.get::(4), + category_name: row.get::(5), + required_permissions: row.get::(6), })) } @@ -67,6 +104,10 @@ pub async fn get_all_scheduled_closures(pool: &SqlitePool) -> ModmailResult(0), close_at: row.get::(1), silent: row.get::(2) != 0, + closed_by: row.get::(3), + category_id: row.get::(4), + category_name: row.get::(5), + required_permissions: row.get::(6), }) .collect()) } diff --git a/rustmail/src/db/operations/threads.rs b/rustmail/src/db/operations/threads.rs index d7217ff..a4430cf 100644 --- a/rustmail/src/db/operations/threads.rs +++ b/rustmail/src/db/operations/threads.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use crate::db::repr::Thread; use crate::errors::ModmailResult; use serenity::all::{ChannelId, GuildChannel, UserId}; @@ -144,8 +145,29 @@ pub async fn create_thread_for_user( } } -pub async fn close_thread(thread_id: &str, pool: &SqlitePool) -> ModmailResult<()> { - sqlx::query!("UPDATE threads SET status = 0 WHERE id = ?", thread_id) +pub async fn close_thread(thread_id: &str, closed_by: &str, category_id: &str, category_name: &str, required_permissions: u64, pool: &SqlitePool) -> ModmailResult<()> { + let closed_at = Utc::now().timestamp(); + let required_permissions = required_permissions.clone().to_string(); + + sqlx::query!( + r#" + UPDATE threads + SET + status = 0, + closed_at = ?, + closed_by = ?, + category_id = ?, + category_name = ?, + required_permissions = ? + WHERE id = ? + "#, + closed_at, + closed_by, + category_id, + category_name, + required_permissions, + thread_id + ) .execute(pool) .await?; @@ -233,7 +255,7 @@ pub async fn cancel_alert_for_staff( } pub async fn set_alert_for_staff( - staff_user_id: serenity::all::UserId, + staff_user_id: UserId, thread_user_id: i64, pool: &SqlitePool, ) -> Result<(), Error> { From 03efa40b40821fa3958f4272f35a42eb9dee9f85 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 19 Oct 2025 16:47:46 +0200 Subject: [PATCH 032/100] feat(panel): added complete tickets and configuration page --- rustmail/src/handlers/guild_handler.rs | 10 +- .../src/handlers/guild_members_handler.rs | 49 +- rustmail/src/modules/message_recovery.rs | 2 +- rustmail/src/modules/scheduled_closures.rs | 5 +- rustmail/src/utils/command/mod.rs | 1 + rustmail/src/utils/message/mod.rs | 2 + rustmail/src/utils/thread/mod.rs | 1 + rustmail_panel/Cargo.toml | 2 + rustmail_panel/src/components/navbar.rs | 206 +++---- rustmail_panel/src/components/ticket.rs | 509 ++++++++---------- rustmail_panel/src/pages/panel.rs | 85 +-- rustmail_panel/src/router.rs | 5 +- 12 files changed, 456 insertions(+), 421 deletions(-) diff --git a/rustmail/src/handlers/guild_handler.rs b/rustmail/src/handlers/guild_handler.rs index aeda4dc..5a2cb9c 100644 --- a/rustmail/src/handlers/guild_handler.rs +++ b/rustmail/src/handlers/guild_handler.rs @@ -5,6 +5,7 @@ use crate::db::threads::is_an_opened_ticket_channel; use async_trait::async_trait; use serenity::all::{Context, GuildChannel, Message}; use serenity::client::EventHandler; +use crate::utils::thread::category::{get_category_id_from_guild_channel, get_category_name_from_guild_channel, get_required_permissions_channel_from_guild_channel}; pub struct GuildHandler { pub config: Config, @@ -22,7 +23,7 @@ impl GuildHandler { impl EventHandler for GuildHandler { async fn channel_delete( &self, - _ctx: Context, + ctx: Context, channel: GuildChannel, _messages: Option>, ) { @@ -42,7 +43,12 @@ impl EventHandler for GuildHandler { } }; - match close_thread(&thread.id, pool).await { + let closed_by = "deleted_by_client".to_string(); + let category_id = get_category_id_from_guild_channel(&ctx, &channel).await; + let category_name = get_category_name_from_guild_channel(&ctx, &channel).await; + let required_permissions = get_required_permissions_channel_from_guild_channel(&ctx, &channel).await; + + match close_thread(&thread.id, &closed_by, &category_id, &category_name, required_permissions, pool).await { Ok(_) => { println!("Close thread successfully by deleted channel!"); } diff --git a/rustmail/src/handlers/guild_members_handler.rs b/rustmail/src/handlers/guild_members_handler.rs index 9a7e066..cc2492d 100644 --- a/rustmail/src/handlers/guild_members_handler.rs +++ b/rustmail/src/handlers/guild_members_handler.rs @@ -5,12 +5,13 @@ use crate::db::threads::get_thread_by_user_id; use crate::features::make_buttons; use crate::i18n::get_translated_message; use crate::utils::message::message_builder::MessageBuilder; -use serenity::all::{ButtonStyle, Member}; +use serenity::all::{ButtonStyle, Channel, Member, PermissionOverwriteType, RoleId}; use serenity::{ all::{Context, EventHandler, GuildId, User}, async_trait, }; use std::collections::HashMap; +use crate::utils::thread::category::get_required_permissions_channel_from_guild_channel; pub struct GuildMembersHandler { pub config: Config, @@ -101,6 +102,50 @@ impl EventHandler for GuildMembersHandler { eprintln!("Erreur lors de la mise à jour du statut du thread: {:?}", e); } - let _ = close_thread(&thread.id, pool).await; + let closed_by = "user_left_server".to_string(); + let category_id = match channel_id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.category() { + Some(category) => category.id.to_string(), + None => String::new(), + } + } + _ => String::new(), + }; + let category_name = match channel_id.to_channel(&ctx.http).await { + Ok(channel) => { + match channel.category() { + Some(category) => category.name.clone(), + None => String::new(), + } + } + _ => String::new(), + }; + + let required_permissions = match channel_id.to_channel(&ctx.http).await { + Ok(Channel::Guild(guild_channel)) => { + let guild_id = guild_channel.guild_id; + let guild = guild_id.to_partial_guild(&ctx.http).await.ok(); + + let everyone_role_id = RoleId::new(guild_id.get()); + + let mut perms = guild + .and_then(|g| g.roles.get(&everyone_role_id).map(|r| r.permissions.bits())) + .unwrap_or(0u64); + + for overwrite in &guild_channel.permission_overwrites { + if let PermissionOverwriteType::Role(_) = overwrite.kind { + let allow = overwrite.allow.bits(); + let deny = overwrite.deny.bits(); + perms = (perms & !deny) | allow; + } + } + + perms + } + _ => 0u64, + }; + + let _ = close_thread(&thread.id, &closed_by, &category_id, &category_name, required_permissions, pool).await; } } diff --git a/rustmail/src/modules/message_recovery.rs b/rustmail/src/modules/message_recovery.rs index 62ebd00..4312f45 100644 --- a/rustmail/src/modules/message_recovery.rs +++ b/rustmail/src/modules/message_recovery.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::db::operations::{ get_all_opened_threads, get_last_recovery_timestamp, get_latest_thread_message, - insert_recovered_message, update_last_recovery_timestamp, + update_last_recovery_timestamp, }; use crate::db::repr::Thread; use crate::i18n::get_translated_message; diff --git a/rustmail/src/modules/scheduled_closures.rs b/rustmail/src/modules/scheduled_closures.rs index d923bc7..5af25e3 100644 --- a/rustmail/src/modules/scheduled_closures.rs +++ b/rustmail/src/modules/scheduled_closures.rs @@ -25,7 +25,8 @@ fn schedule_one(ctx: &Context, config: &Config, thread_id: String, close_at: i64 let channel_id = ChannelId::new(thread.channel_id.parse::().unwrap_or(0)); let user_id = UserId::new(thread.user_id as u64); - let _ = close_thread(&thread_id, pool).await; + + let _ = close_thread(&thread_id, ¤t.closed_by, ¤t.category_id, ¤t.category_name, current.required_permissions.parse::().unwrap_or(0), pool).await; let _ = delete_scheduled_closure(&thread_id, pool).await; if !current.silent { let _ = MessageBuilder::system_message(&ctx_clone, &config_clone) @@ -67,7 +68,7 @@ pub async fn hydrate_scheduled_closures(ctx: &Context, config: &Config) { if sc.close_at <= Utc::now().timestamp() { let channel_id = ChannelId::new(thread.channel_id.parse::().unwrap_or(0)); let user_id = UserId::new(thread.user_id as u64); - let _ = close_thread(&thread.id, pool).await; + let _ = close_thread(&thread.id, &sc.closed_by, &sc.category_id, &sc.category_name, sc.required_permissions.parse::().unwrap_or(0), pool).await; let _ = delete_scheduled_closure(&thread.id, pool).await; if !sc.silent { let _ = MessageBuilder::system_message(ctx, config) diff --git a/rustmail/src/utils/command/mod.rs b/rustmail/src/utils/command/mod.rs index 5281cea..85a1c21 100644 --- a/rustmail/src/utils/command/mod.rs +++ b/rustmail/src/utils/command/mod.rs @@ -1,3 +1,4 @@ pub mod defer_response; pub mod extract_reply_content; pub mod wrap_command; +pub mod category; diff --git a/rustmail/src/utils/message/mod.rs b/rustmail/src/utils/message/mod.rs index 6ae5ff1..f4e7124 100644 --- a/rustmail/src/utils/message/mod.rs +++ b/rustmail/src/utils/message/mod.rs @@ -2,6 +2,8 @@ pub mod message_builder; pub mod reply_intent; pub mod ui_components; +pub mod category; + pub mod ui { use super::{ui_components::ButtonsBuilder, ui_components::ModalBuilder}; pub fn modal(id: impl Into, title: impl Into) -> ModalBuilder { diff --git a/rustmail/src/utils/thread/mod.rs b/rustmail/src/utils/thread/mod.rs index 74698dd..7523d79 100644 --- a/rustmail/src/utils/thread/mod.rs +++ b/rustmail/src/utils/thread/mod.rs @@ -1,3 +1,4 @@ pub mod fetch_thread; pub mod get_thread_lock; pub mod send_to_thread; +pub mod category; diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index 2fe060d..3b33d96 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -17,3 +17,5 @@ tailyew = "0.1.38" serde = { version = "1.0.228", features = ["derive"] } js-sys = "0.3.77" wasm-bindgen = "0.2.100" +[build-dependencies] +navigator = "0.3.0" diff --git a/rustmail_panel/src/components/navbar.rs b/rustmail_panel/src/components/navbar.rs index 3363959..78446ce 100644 --- a/rustmail_panel/src/components/navbar.rs +++ b/rustmail_panel/src/components/navbar.rs @@ -1,18 +1,21 @@ use crate::components::language_switcher::LanguageSwitcher; -use crate::pages::panel::Page; +use crate::pages::panel::PanelRoute; use yew::{function_component, html, use_state, Callback, Html, Properties, classes}; +use yew_router::hooks::{use_location, use_navigator}; #[derive(Properties, PartialEq, Clone)] pub struct RustmailNavbarProps { pub avatar_url: String, - pub on_page_change: Callback, - pub current_page: Page, } #[function_component(RustmailNavbar)] pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html { let mobile_menu_open = use_state(|| false); let profile_menu_open = use_state(|| false); + let navigator = use_navigator(); + let current_path = use_location() + .map(|loc| loc.path().to_string()) + .unwrap_or_default(); let toggle_mobile_menu = { let mobile_menu_open = mobile_menu_open.clone(); @@ -24,6 +27,10 @@ pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html { Callback::from(move |_| profile_menu_open.set(!*profile_menu_open)) }; + let home_active = current_path == "/panel"; + let config_active = current_path == "/panel/configuration"; + let tickets_active = current_path.starts_with("/panel/tickets"); + html! { } } diff --git a/rustmail_panel/src/components/ticket.rs b/rustmail_panel/src/components/ticket.rs index 787cb7b..39e6cbb 100644 --- a/rustmail_panel/src/components/ticket.rs +++ b/rustmail_panel/src/components/ticket.rs @@ -2,46 +2,82 @@ use gloo_net::http::Request; use js_sys::Date; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; -use serde::Deserialize; +use yew_router::prelude::*; use wasm_bindgen::JsValue; +use serde::Deserialize; -#[derive(Clone, PartialEq, Deserialize)] -pub struct Author { - pub name: String, - pub avatar: String, +#[derive(Clone, PartialEq, Deserialize, Debug)] +pub struct ThreadMessage { + pub id: i64, + pub thread_id: String, + pub user_id: i64, + pub user_name: String, + pub is_anonymous: bool, + pub dm_message_id: Option, + pub inbox_message_id: Option, + pub message_number: Option, + pub created_at: String, + pub content: String, } -#[derive(Clone, PartialEq, Deserialize)] -pub struct Message { +#[derive(Clone, PartialEq, Deserialize, Debug)] +pub struct CompleteThread { pub id: String, - pub author: Author, - pub content: String, - pub timestamp: String, - #[serde(rename = "type")] - pub message_type: String, + pub user_id: i64, + pub user_name: String, + pub channel_id: String, + pub created_at: i64, + pub new_message_number: i64, + pub status: i64, + pub user_left: bool, + pub closed_at: Option, + pub closed_by: Option, + pub category_id: Option, + pub category_name: Option, + pub required_permissions: Option, + pub messages: Vec, } -#[derive(Clone, PartialEq, Deserialize)] -pub struct Ticket { - pub id: String, - pub title: String, - pub author: Author, - pub category: String, - pub status: String, - pub created_at: String, - pub closed_at: Option, - pub messages: Vec, +#[derive(Clone, Routable, PartialEq)] +pub enum TicketsRoute { + #[at("/panel/tickets")] + TicketsList, + #[at("/panel/tickets/:id")] + TicketDetails { id: String }, } #[function_component(TicketsPage)] pub fn tickets_page() -> Html { - let tickets = use_state(|| Vec::::new()); - let selected_ticket = use_state(|| None::); + let location = use_location().unwrap(); + let navigator = use_navigator().unwrap(); + + let path = location.path(); + + if let Some(id) = path.strip_prefix("/panel/tickets/") { + html! { +
+
+ +
+
+ } + } else { + html! { } + } +} + +#[function_component(TicketsList)] +pub fn tickets_list() -> Html { + let tickets = use_state(|| Vec::::new()); let loading = use_state(|| true); let selected_category = use_state(|| "all".to_string()); - let show_user_messages = use_state(|| true); - let show_staff_messages = use_state(|| true); - let show_system_messages = use_state(|| true); + let navigator = use_navigator().unwrap(); { let tickets = tickets.clone(); @@ -53,9 +89,8 @@ pub fn tickets_page() -> Html { spawn_local(async move { loading_clone.set(true); - if let Ok(resp) = Request::get("/api/bot/tickets").send().await { - if let Ok(data) = resp.json::>().await { + if let Ok(data) = resp.json::>().await { tickets_clone.set(data); } else { tickets_clone.set(Vec::new()); @@ -63,18 +98,16 @@ pub fn tickets_page() -> Html { } else { tickets_clone.set(Vec::new()); } - loading_clone.set(false); }); - || () }); } let categories = { - let mut cats: Vec = tickets.clone() + let mut cats: Vec = tickets .iter() - .map(|t| t.category.clone()) + .filter_map(|t| t.category_name.clone()) .collect::>() .into_iter() .collect(); @@ -83,273 +116,195 @@ pub fn tickets_page() -> Html { cats }; - let filtered_tickets: Vec = { - if *selected_category == "all" { - (*tickets).clone() - } else { - tickets.iter() - .filter(|t| t.category == *selected_category) - .cloned() - .collect() - } - }; - - let get_filtered_messages = { - let show_user_messages = show_user_messages.clone(); - let show_staff_messages = show_staff_messages.clone(); - let show_system_messages = show_system_messages.clone(); - - move |ticket: &Ticket| -> Vec { - ticket.messages.iter().filter(|m| { - match m.message_type.as_str() { - "user" if !*show_user_messages => false, - "staff" if !*show_staff_messages => false, - "system" if !*show_system_messages => false, - _ => true, - } - }).cloned().collect() - } + let filtered_tickets: Vec = if *selected_category == "all" { + (*tickets).clone() + } else { + tickets + .iter() + .filter(|t| t.category_name.as_deref() == Some(&*selected_category)) + .cloned() + .collect() }; - let format_date = |date: &str| -> String { - let d = Date::new(&JsValue::from_str(date)); - let s = d.to_locale_string("fr-FR", &JsValue::UNDEFINED); - s.as_string().unwrap_or_else(|| date.to_string()) + let format_date = |timestamp: i64| -> String { + let d = Date::new_0(); + d.set_time(timestamp as f64 * 1000.0); + d.to_locale_string("fr-FR", &JsValue::UNDEFINED) + .as_string() + .unwrap_or_else(|| "date inconnue".into()) }; - let status_color = |status: &str| match status { - "open" => "bg-green-500", - "closed" => "bg-gray-500", - "pending" => "bg-yellow-500", - _ => "bg-gray-500", + let status_color = |status: i64| match status { + 1 => "bg-green-500", + 0 => "bg-gray-500", + _ => "bg-yellow-500", }; html! { -
-
+
+
+

{"Tickets Fermés"}

+

{"Consultez l'historique des tickets fermés"}

+
-
-

{"Tickets Fermés"}

-

{"Consultez l'historique des tickets fermés"}

+
+
+ +
-
- -
-
- -
- - + { + if *loading { + html! { +
+ {"Chargement..."}
- -
- { - if *loading { - html! { -
- - - - - {"Chargement..."} -
- } - } else if filtered_tickets.is_empty() { - html! { -
{"Aucun ticket trouvé"}
- } - } else { - html! { -
- { for filtered_tickets.iter().map(|ticket| { - let is_selected = selected_ticket.as_ref().map(|t| t.id == ticket.id).unwrap_or(false); - let ticket_clone = ticket.clone(); - let selected_ticket = selected_ticket.clone(); - - html! { - - } - }) } + } + } else if filtered_tickets.is_empty() { + html! { +
{"Aucun ticket trouvé"}
+ } + } else { + html! { +
+ { for filtered_tickets.iter().map(|ticket| { + let id = ticket.id.clone(); + html! { + } - } + }) }
-
-
+ } + } + } +
+
+ } +} -
-
- { - if let Some(ticket) = &*selected_ticket { - let messages = get_filtered_messages(ticket); - html! { - <> -
-
-
-

{ &ticket.title }

-
- { &ticket.author.name } - {"•"} - { &ticket.category } - {"•"} - { format!("Ouvert: {}", format_date(&ticket.created_at)) } - { - if let Some(closed) = &ticket.closed_at { - html! { - <> - {"•"} - { format!("Fermé: {}", format_date(closed)) } - - } - } else { - html! {} - } - } -
-
+#[derive(Properties, PartialEq)] +pub struct TicketDetailsProps { + pub id: String, + pub on_back: Callback<()>, +} -
-
- { &ticket.status } -
-
+#[function_component(TicketDetails)] +pub fn ticket_details(props: &TicketDetailsProps) -> Html { + let ticket = use_state(|| None::); + let loading = use_state(|| true); -
- + { + let id = props.id.clone(); + let ticket = ticket.clone(); + let loading = loading.clone(); - + use_effect_with(id.clone(), move |_| { + spawn_local(async move { + loading.set(true); + let url = format!("/api/bot/tickets?id={}", id); + if let Ok(resp) = Request::get(&url).send().await { + if let Ok(data) = resp.json::().await { + ticket.set(Some(data)); + } + } + loading.set(false); + }); + || () + }); + } - -
-
+ let format_date = |timestamp: i64| -> String { + let d = Date::new_0(); + d.set_time(timestamp as f64 * 1000.0); + d.to_locale_string("fr-FR", &JsValue::UNDEFINED) + .as_string() + .unwrap_or_else(|| "date inconnue".into()) + }; -
- { - if messages.is_empty() { - html! { -
- {"Aucun message à afficher avec les filtres actuels"} -
- } - } else { - html! { - { for messages.iter().map(|m| html! { -
- -
-
- { &m.author.name } - "bg-blue-500/20 text-blue-300", - "staff" => "bg-green-500/20 text-green-300", - _ => "bg-gray-500/20 text-gray-300" - } - )}> - { - match m.message_type.as_str() { - "user" => "Utilisateur", - "staff" => "Staff", - _ => "Système" - } - } - - { format_date(&m.timestamp) } -
-
-

{ &m.content }

-
-
-
- }) } - } - } - } -
- - } - } else { - html! { -
- - - - {"Sélectionnez un ticket pour voir les détails"} -
+ html! { +
+ + + { + if *loading { + html! {

{"Chargement du ticket..."}

} + } else if let Some(ticket) = &*ticket { + html! { +
+

{ format!("Ticket #{}", ticket.id) }

+

+ { format!("Utilisateur : {} • Catégorie : {} • Ouvert : {}", ticket.user_name, ticket.category_name.clone().unwrap_or_default(), format_date(ticket.created_at)) } + { + if let Some(closed) = ticket.closed_at { + html! { + { format!(" • Fermé : {}", format_date(closed)) } + } + } else { + html! {} } } - } +

+ +
+ { for ticket.messages.iter().map(|m| html! { +
+
+ { &m.user_name } + { &m.created_at } +
+

{ &m.content }

+
+ }) } +
-
-
-
+ } + } else { + html! {

{"Ticket introuvable."}

} + } + }
} } diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index 1a23a77..cf259f4 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -3,26 +3,34 @@ use gloo_net::http::Request; use gloo_utils::window; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; +use yew_router::hooks::use_navigator; +use yew_router::{navigator, BrowserRouter, Routable, Switch}; +use yew_router::navigator::Navigator; use crate::components::configuration::ConfigurationPage; -use crate::components::ticket::TicketsPage; +use crate::components::ticket::{TicketDetails, TicketsList, TicketsPage}; #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct UserAvatar { pub avatar_url: Option, } -#[derive(Clone, PartialEq)] -pub enum Page { +#[derive(Clone, Routable, PartialEq)] +pub enum PanelRoute { + #[at("/panel")] Home, + #[at("/panel/configuration")] Configuration, - Tickets, + #[at("/panel/tickets")] + TicketsList, + #[at("/panel/tickets/:id")] + TicketDetails { id: String }, } + #[function_component(Panel)] pub fn panel() -> Html { - let (i18n, _set_language) = i18nrs::yew::use_translation(); - let authorized = use_state(|| None::); + let navigator = use_navigator(); { let authorized = authorized.clone(); use_effect_with((), move |_| { @@ -70,7 +78,7 @@ pub fn panel() -> Html { } let avatar_url = (*avatar).clone().unwrap_or_default(); - let current_page = use_state(|| Page::Home); + html! { <> @@ -82,40 +90,14 @@ pub fn panel() -> Html {
}, Some(true) => html! { - <> - - + +
- { - match &*current_page { - Page::Home => html! { -
- Rustmail logo -

{"Rustmail Panel"}

-

- { i18n.t("panel.welcome") } -

-
- }, - Page::Configuration => html! { - - }, - Page::Tickets => html! { - - }, - } - } + render={move |route| switch(route, navigator.clone())} />
- +
}, Some(false) => html! {}, } @@ -123,3 +105,32 @@ pub fn panel() -> Html { } } + +fn switch(route: PanelRoute, navigator: Option) -> Html { + match route { + PanelRoute::Home => html! { +
+ Rustmail logo +

{"Rustmail Panel"}

+

+ {"Bienvenue sur le panneau d'administration Rustmail"} +

+
+ }, + PanelRoute::Configuration => html! { }, + PanelRoute::TicketsList => html! { }, + PanelRoute::TicketDetails { id } => { + let nav = navigator.clone(); + html! { + + } + } + } +} diff --git a/rustmail_panel/src/router.rs b/rustmail_panel/src/router.rs index 57dda63..202e541 100644 --- a/rustmail_panel/src/router.rs +++ b/rustmail_panel/src/router.rs @@ -12,6 +12,8 @@ pub enum Route { #[at("/docs")] Documentation, #[at("/panel")] + PanelRoot, + #[at("/panel/*")] Panel, #[at("/error")] Error, @@ -24,8 +26,9 @@ pub fn switch(routes: Route) -> Html { match routes { Route::Home => html! { }, Route::Documentation => html! { }, - Route::Panel => html! { }, + Route::PanelRoot | Route::Panel => html! { }, Route::Error => html! { }, Route::NotFound => html! {

{ "404" }

}, } } + From ad5d03260fd074fedce65c83ee7c2c99004c5724 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 19 Oct 2025 16:57:10 +0200 Subject: [PATCH 033/100] fix(panel): add missing sqlx file --- ...d48a67d61296dc49624a62552f80002ae4c6e.json | 20 ---- ...9614f407b0ceb5a82f218a8451ddc43b897a2.json | 92 +++++++++++++++++++ ...7f268f5a140c04524e7dde35a3b6c9d05fa35.json | 12 +++ ...eb7da682d9f1a150ddcfe682f8eca468158e3.json | 92 +++++++++++++++++++ ...646a09f34516482d72af781423bb1789b98b1.json | 12 --- ...20cce28a72d5755f57e0b874569311f569b51.json | 74 +++++++++++++++ ...f8a34facae0f1e0fe0f73d720a65b4c7ef6b6.json | 12 --- 7 files changed, 270 insertions(+), 44 deletions(-) delete mode 100644 .sqlx/query-05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e.json create mode 100644 .sqlx/query-140d108e6307585f146a559bed69614f407b0ceb5a82f218a8451ddc43b897a2.json create mode 100644 .sqlx/query-2eff567a5668138fed25817ebd77f268f5a140c04524e7dde35a3b6c9d05fa35.json create mode 100644 .sqlx/query-40ac76471ce84e90822cd6585fbeb7da682d9f1a150ddcfe682f8eca468158e3.json delete mode 100644 .sqlx/query-4dad72c50271ccb618c9bc460a0646a09f34516482d72af781423bb1789b98b1.json create mode 100644 .sqlx/query-78851afb4bcef9737b5bb4a299320cce28a72d5755f57e0b874569311f569b51.json delete mode 100644 .sqlx/query-911cf434a65a1fb08918598f465f8a34facae0f1e0fe0f73d720a65b4c7ef6b6.json diff --git a/.sqlx/query-05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e.json b/.sqlx/query-05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e.json deleted file mode 100644 index d1bdbb3..0000000 --- a/.sqlx/query-05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT avatar_hash FROM sessions_panel WHERE user_id = ?", - "describe": { - "columns": [ - { - "name": "avatar_hash", - "ordinal": 0, - "type_info": "Text" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - true - ] - }, - "hash": "05639003441cc75567d9a447fa8d48a67d61296dc49624a62552f80002ae4c6e" -} diff --git a/.sqlx/query-140d108e6307585f146a559bed69614f407b0ceb5a82f218a8451ddc43b897a2.json b/.sqlx/query-140d108e6307585f146a559bed69614f407b0ceb5a82f218a8451ddc43b897a2.json new file mode 100644 index 0000000..40bb1b0 --- /dev/null +++ b/.sqlx/query-140d108e6307585f146a559bed69614f407b0ceb5a82f218a8451ddc43b897a2.json @@ -0,0 +1,92 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n user_id,\n user_name,\n channel_id,\n created_at as \"created_at: String\",\n next_message_number as new_message_number,\n status,\n user_left,\n closed_at as \"closed_at: Option\",\n closed_by,\n category_id,\n category_name,\n required_permissions\n FROM threads\n WHERE status = 0\n ORDER BY closed_at DESC\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "user_name", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "channel_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at: String", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "new_message_number", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "status", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "user_left", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "closed_at: Option", + "ordinal": 8, + "type_info": "Datetime" + }, + { + "name": "closed_by", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "category_id", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "category_name", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "required_permissions", + "ordinal": 12, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + true, + true, + true, + true, + true + ] + }, + "hash": "140d108e6307585f146a559bed69614f407b0ceb5a82f218a8451ddc43b897a2" +} diff --git a/.sqlx/query-2eff567a5668138fed25817ebd77f268f5a140c04524e7dde35a3b6c9d05fa35.json b/.sqlx/query-2eff567a5668138fed25817ebd77f268f5a140c04524e7dde35a3b6c9d05fa35.json new file mode 100644 index 0000000..80f89af --- /dev/null +++ b/.sqlx/query-2eff567a5668138fed25817ebd77f268f5a140c04524e7dde35a3b6c9d05fa35.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE threads\n SET\n status = 0,\n closed_at = ?,\n closed_by = ?,\n category_id = ?,\n category_name = ?,\n required_permissions = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "2eff567a5668138fed25817ebd77f268f5a140c04524e7dde35a3b6c9d05fa35" +} diff --git a/.sqlx/query-40ac76471ce84e90822cd6585fbeb7da682d9f1a150ddcfe682f8eca468158e3.json b/.sqlx/query-40ac76471ce84e90822cd6585fbeb7da682d9f1a150ddcfe682f8eca468158e3.json new file mode 100644 index 0000000..fcdcbbe --- /dev/null +++ b/.sqlx/query-40ac76471ce84e90822cd6585fbeb7da682d9f1a150ddcfe682f8eca468158e3.json @@ -0,0 +1,92 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n user_id,\n user_name,\n channel_id,\n created_at as \"created_at: String\",\n next_message_number as new_message_number,\n status,\n user_left,\n closed_at as \"closed_at: Option\",\n closed_by,\n category_id,\n category_name,\n required_permissions\n FROM threads\n WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "user_name", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "channel_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at: String", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "new_message_number", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "status", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "user_left", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "closed_at: Option", + "ordinal": 8, + "type_info": "Datetime" + }, + { + "name": "closed_by", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "category_id", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "category_name", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "required_permissions", + "ordinal": 12, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false, + false, + true, + true, + true, + true, + true + ] + }, + "hash": "40ac76471ce84e90822cd6585fbeb7da682d9f1a150ddcfe682f8eca468158e3" +} diff --git a/.sqlx/query-4dad72c50271ccb618c9bc460a0646a09f34516482d72af781423bb1789b98b1.json b/.sqlx/query-4dad72c50271ccb618c9bc460a0646a09f34516482d72af781423bb1789b98b1.json deleted file mode 100644 index 7a85344..0000000 --- a/.sqlx/query-4dad72c50271ccb618c9bc460a0646a09f34516482d72af781423bb1789b98b1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "UPDATE threads SET status = 0 WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "4dad72c50271ccb618c9bc460a0646a09f34516482d72af781423bb1789b98b1" -} diff --git a/.sqlx/query-78851afb4bcef9737b5bb4a299320cce28a72d5755f57e0b874569311f569b51.json b/.sqlx/query-78851afb4bcef9737b5bb4a299320cce28a72d5755f57e0b874569311f569b51.json new file mode 100644 index 0000000..5e7dbce --- /dev/null +++ b/.sqlx/query-78851afb4bcef9737b5bb4a299320cce28a72d5755f57e0b874569311f569b51.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n thread_id,\n user_id,\n user_name,\n is_anonymous,\n dm_message_id,\n inbox_message_id,\n message_number,\n created_at as \"created_at: String\",\n content\n FROM thread_messages\n WHERE thread_id = ?\n ORDER BY created_at ASC\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "thread_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "user_id", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "user_name", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "is_anonymous", + "ordinal": 4, + "type_info": "Bool" + }, + { + "name": "dm_message_id", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "inbox_message_id", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "message_number", + "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "created_at: String", + "ordinal": 8, + "type_info": "Datetime" + }, + { + "name": "content", + "ordinal": 9, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + true, + false, + false + ] + }, + "hash": "78851afb4bcef9737b5bb4a299320cce28a72d5755f57e0b874569311f569b51" +} diff --git a/.sqlx/query-911cf434a65a1fb08918598f465f8a34facae0f1e0fe0f73d720a65b4c7ef6b6.json b/.sqlx/query-911cf434a65a1fb08918598f465f8a34facae0f1e0fe0f73d720a65b4c7ef6b6.json deleted file mode 100644 index ade4597..0000000 --- a/.sqlx/query-911cf434a65a1fb08918598f465f8a34facae0f1e0fe0f73d720a65b4c7ef6b6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO thread_messages (\n thread_id, user_id, user_name, is_anonymous, dm_message_id, content, thread_status\n ) VALUES (\n ?, ?, ?, ?, ?, ?, ?\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "911cf434a65a1fb08918598f465f8a34facae0f1e0fe0f73d720a65b4c7ef6b6" -} From 75f8ffb7d0222cddce205f46695b9065c8d06d4f Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 20 Oct 2025 09:00:34 +0200 Subject: [PATCH 034/100] refactor(panel): added all missing panel's traductions --- rustmail/src/api/handler/bot/tickets.rs | 23 +++++----- .../src/components/configuration.rs | 24 +++++----- rustmail_panel/src/components/home.rs | 17 ++++--- rustmail_panel/src/components/mod.rs | 2 +- rustmail_panel/src/components/navbar.rs | 18 ++++---- rustmail_panel/src/components/ticket.rs | 37 +++++++++------- rustmail_panel/src/i18n/en/en.json | 44 +++++++++++++++++-- rustmail_panel/src/i18n/fr/fr.json | 44 +++++++++++++++++-- rustmail_panel/src/pages/docs.rs | 27 ------------ rustmail_panel/src/pages/mod.rs | 1 - rustmail_panel/src/pages/panel.rs | 24 ++++------ rustmail_panel/src/router.rs | 5 --- 12 files changed, 154 insertions(+), 112 deletions(-) delete mode 100644 rustmail_panel/src/pages/docs.rs diff --git a/rustmail/src/api/handler/bot/tickets.rs b/rustmail/src/api/handler/bot/tickets.rs index aee7180..0ac78c5 100644 --- a/rustmail/src/api/handler/bot/tickets.rs +++ b/rustmail/src/api/handler/bot/tickets.rs @@ -1,12 +1,12 @@ -use std::sync::Arc; +use crate::BotState; use axum::{ extract::{Query, State}, http::StatusCode, Json, }; -use tokio::sync::Mutex; use serde::{Deserialize, Serialize}; -use crate::BotState; +use std::sync::Arc; +use tokio::sync::Mutex; #[derive(Debug, Clone, Serialize)] pub struct ThreadMessage { @@ -65,7 +65,6 @@ pub async fn handle_tickets_bot( }; if let Some(id) = params.id { - let thread = match sqlx::query!( r#" SELECT @@ -87,8 +86,8 @@ pub async fn handle_tickets_bot( "#, id ) - .fetch_optional(&db_pool) - .await + .fetch_optional(&db_pool) + .await { Ok(Some(row)) => row, Ok(None) => { @@ -129,8 +128,8 @@ pub async fn handle_tickets_bot( "#, thread.id ) - .fetch_all(&db_pool) - .await + .fetch_all(&db_pool) + .await { Ok(rows) => rows, Err(err) => { @@ -196,8 +195,8 @@ pub async fn handle_tickets_bot( ORDER BY closed_at DESC "# ) - .fetch_all(&db_pool) - .await + .fetch_all(&db_pool) + .await { Ok(rows) => rows, Err(err) => { @@ -233,8 +232,8 @@ pub async fn handle_tickets_bot( "#, thread.id ) - .fetch_all(&db_pool) - .await + .fetch_all(&db_pool) + .await { Ok(rows) => rows, Err(err) => { diff --git a/rustmail_panel/src/components/configuration.rs b/rustmail_panel/src/components/configuration.rs index 1547680..933d487 100644 --- a/rustmail_panel/src/components/configuration.rs +++ b/rustmail_panel/src/components/configuration.rs @@ -1,9 +1,12 @@ use gloo_net::http::Request; +use i18nrs::yew::use_translation; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; #[function_component(ConfigurationPage)] pub fn configuration_page() -> Html { + let (i18n, _set_language) = use_translation(); + let bot_status = use_state(|| "running".to_string()); let is_loading = use_state(|| false); @@ -90,7 +93,8 @@ pub fn configuration_page() -> Html { spawn_local(async move { let _ = Request::post("/api/bot/config") .header("Content-Type", "application/json") - .body(serde_json::to_string(&data).unwrap()).expect("REASON") + .body(serde_json::to_string(&data).unwrap()) + .expect("REASON") .send() .await; }); @@ -102,15 +106,15 @@ pub fn configuration_page() -> Html {
-

{"Configuration"}

-

{"Gérez les paramètres de votre bot Discord"}

+

{i18n.t("panel.configuration.title")}

+

{i18n.t("panel.configuration.description")}

-

{"Statut du Bot"}

-

{"Contrôlez l'état de votre bot"}

+

{i18n.t("panel.configuration.bot_status")}

+

{i18n.t("panel.configuration.bot_status_description")}

@@ -119,7 +123,7 @@ pub fn configuration_page() -> Html { if *bot_status == "running" { "bg-green-500" } else { "bg-red-500" } )}>
- { if *bot_status == "running" { "En ligne" } else { "Hors ligne" } } + { if *bot_status == "running" { i18n.t("panel.configuration.online") } else { i18n.t("panel.configuration.offline") } }
@@ -134,7 +138,7 @@ pub fn configuration_page() -> Html { class="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-green-600/50 disabled:cursor-not-allowed text-white rounded-md transition flex items-center gap-2" > - {"Démarrer"} + {i18n.t("panel.configuration.start_bot")}
-

{"Paramètres du Bot"}

+

{i18n.t("panel.configuration.config_file.title")}

diff --git a/rustmail_panel/src/components/home.rs b/rustmail_panel/src/components/home.rs index b382bc2..83e7d43 100644 --- a/rustmail_panel/src/components/home.rs +++ b/rustmail_panel/src/components/home.rs @@ -1,18 +1,17 @@ +use i18nrs::yew::use_translation; use yew::prelude::*; #[function_component(Home)] pub fn home() -> Html { + let (i18n, _set_language) = use_translation(); + html! { -
+
Rustmail logo -

{"Rustmail Panel"}

-

- {"Bienvenue sur le pannel de Rustmail"} +

{i18n.t("panel.title")}

+

+ {i18n.t("panel.welcome")}

- -
+
} } diff --git a/rustmail_panel/src/components/mod.rs b/rustmail_panel/src/components/mod.rs index bee6b70..ca18b53 100644 --- a/rustmail_panel/src/components/mod.rs +++ b/rustmail_panel/src/components/mod.rs @@ -1,6 +1,6 @@ +pub mod configuration; pub mod home; pub mod language_switcher; pub mod logout_button; pub mod navbar; -pub mod configuration; pub mod ticket; diff --git a/rustmail_panel/src/components/navbar.rs b/rustmail_panel/src/components/navbar.rs index 78446ce..27a6f42 100644 --- a/rustmail_panel/src/components/navbar.rs +++ b/rustmail_panel/src/components/navbar.rs @@ -1,6 +1,6 @@ use crate::components::language_switcher::LanguageSwitcher; use crate::pages::panel::PanelRoute; -use yew::{function_component, html, use_state, Callback, Html, Properties, classes}; +use yew::{classes, function_component, html, use_state, Callback, Html, Properties}; use yew_router::hooks::{use_location, use_navigator}; #[derive(Properties, PartialEq, Clone)] @@ -10,6 +10,8 @@ pub struct RustmailNavbarProps { #[function_component(RustmailNavbar)] pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html { + let (i18n, _set_language) = i18nrs::yew::use_translation(); + let mobile_menu_open = use_state(|| false); let profile_menu_open = use_state(|| false); let navigator = use_navigator(); @@ -63,7 +65,7 @@ pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html { } )} > - {"Accueil"} + {i18n.t("navbar.home")}
@@ -128,7 +130,7 @@ pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html { { if *profile_menu_open { html! { } } else { @@ -165,7 +167,7 @@ pub fn rustmail_navbar(props: &RustmailNavbarProps) -> Html { } )} > - {"Accueil"} + {i18n.t("navbar.home")} diff --git a/rustmail_panel/src/components/ticket.rs b/rustmail_panel/src/components/ticket.rs index 39e6cbb..7e3b700 100644 --- a/rustmail_panel/src/components/ticket.rs +++ b/rustmail_panel/src/components/ticket.rs @@ -1,10 +1,11 @@ use gloo_net::http::Request; +use i18nrs::yew::use_translation; use js_sys::Date; +use serde::Deserialize; +use wasm_bindgen::JsValue; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::prelude::*; -use wasm_bindgen::JsValue; -use serde::Deserialize; #[derive(Clone, PartialEq, Deserialize, Debug)] pub struct ThreadMessage { @@ -74,6 +75,8 @@ pub fn tickets_page() -> Html { #[function_component(TicketsList)] pub fn tickets_list() -> Html { + let (i18n, _set_language) = use_translation(); + let tickets = use_state(|| Vec::::new()); let loading = use_state(|| true); let selected_category = use_state(|| "all".to_string()); @@ -131,7 +134,7 @@ pub fn tickets_list() -> Html { d.set_time(timestamp as f64 * 1000.0); d.to_locale_string("fr-FR", &JsValue::UNDEFINED) .as_string() - .unwrap_or_else(|| "date inconnue".into()) + .unwrap_or_else(|| i18n.t("panel.tickets.unknown_date").into()) }; let status_color = |status: i64| match status { @@ -143,13 +146,13 @@ pub fn tickets_list() -> Html { html! {
-

{"Tickets Fermés"}

-

{"Consultez l'historique des tickets fermés"}

+

{i18n.t("panel.tickets.title")}

+

{i18n.t("panel.tickets.description")}

- +
@@ -171,12 +174,12 @@ pub fn tickets_list() -> Html { if *loading { html! {
- {"Chargement..."} + { i18n.t("panel.tickets.loading") }
} } else if filtered_tickets.is_empty() { html! { -
{"Aucun ticket trouvé"}
+
{ i18n.t("panel.tickets.none") }
} } else { html! { @@ -199,7 +202,7 @@ pub fn tickets_list() -> Html {

{ &ticket.user_name }

- { ticket.category_name.clone().unwrap_or_else(|| "Inconnue".into()) } + { ticket.category_name.clone().unwrap_or_else(|| i18n.t("panel.tickets.none_category").into()) } {"•"} { format_date(ticket.created_at) }
@@ -225,6 +228,8 @@ pub struct TicketDetailsProps { #[function_component(TicketDetails)] pub fn ticket_details(props: &TicketDetailsProps) -> Html { + let (i18n, _set_language) = use_translation(); + let ticket = use_state(|| None::); let loading = use_state(|| true); @@ -253,7 +258,7 @@ pub fn ticket_details(props: &TicketDetailsProps) -> Html { d.set_time(timestamp as f64 * 1000.0); d.to_locale_string("fr-FR", &JsValue::UNDEFINED) .as_string() - .unwrap_or_else(|| "date inconnue".into()) + .unwrap_or_else(|| i18n.t("panel.tickets.unknown_date").into()) }; html! { @@ -265,22 +270,22 @@ pub fn ticket_details(props: &TicketDetailsProps) -> Html { }} class="text-blue-400 hover:underline mb-6" > - {"← Retour à la liste"} + {i18n.t("panel.tickets.back_to_tickets")} { if *loading { - html! {

{"Chargement du ticket..."}

} + html! {

{i18n.t("panel.tickets.loading_ticket")}

} } else if let Some(ticket) = &*ticket { html! {

{ format!("Ticket #{}", ticket.id) }

- { format!("Utilisateur : {} • Catégorie : {} • Ouvert : {}", ticket.user_name, ticket.category_name.clone().unwrap_or_default(), format_date(ticket.created_at)) } + { format!("{} : {} • {} : {} • {} : {}", i18n.t("panel.tickets.user"), ticket.user_name, i18n.t("panel.tickets.category"), ticket.category_name.clone().unwrap_or_default(), i18n.t("panel.tickets.opened"), format_date(ticket.created_at)) } { if let Some(closed) = ticket.closed_at { html! { - { format!(" • Fermé : {}", format_date(closed)) } + { format!(" • {} : {}", i18n.t("panel.tickets.closed"), format_date(closed)) } } } else { html! {} @@ -302,7 +307,7 @@ pub fn ticket_details(props: &TicketDetailsProps) -> Html {

} } else { - html! {

{"Ticket introuvable."}

} + html! {

{i18n.t("panel.tickets.ticket_not_found")}

} } }
diff --git a/rustmail_panel/src/i18n/en/en.json b/rustmail_panel/src/i18n/en/en.json index 3d91d77..77eb135 100644 --- a/rustmail_panel/src/i18n/en/en.json +++ b/rustmail_panel/src/i18n/en/en.json @@ -7,11 +7,47 @@ "error": { "error": "Error" }, - "panel": { - "welcome": "Welcome on the Rustmail Panel", + "navbar": { + "home": "Home", + "config": "Configuration", + "tickets": "Tickets", "logout": "Logout" }, - "docs": { - "back_to_home": "Back to home" + "panel": { + "title": "Rustmail Panel", + "welcome": "Welcome to the Rustmail admin panel", + "check_access": "Check your access rights...", + "logout": "Logout", + "configuration": { + "title": "Configuration", + "description": "Manage Rustmail settings here.", + "bot_status": "Status", + "bot_status_description": "Check the status of Rustmail", + "start_bot": "Start", + "stop_bot": "Stop", + "restart_bot": "Restart", + "online": "Online", + "offline": "Offline", + "config_file": { + "title": "Rustmail Settings (not implemented yet)" + } + }, + "tickets": { + "title": "Closed tickets", + "description": "View the history of closed tickets", + "sort_by": "Filter by category", + "all": "All categories", + "loading": "Loading tickets...", + "none": "No closed tickets found.", + "none_category": "Unknown", + "back_to_tickets": "← Back to tickets", + "user": "User", + "category": "Category", + "opened": "Opened", + "closed": "Closed", + "unknown_date": "Unknown date", + "loading_ticket": "Loading ticket...", + "ticket_not_found": "Ticket not found." + } } } diff --git a/rustmail_panel/src/i18n/fr/fr.json b/rustmail_panel/src/i18n/fr/fr.json index 6e75247..3d44478 100644 --- a/rustmail_panel/src/i18n/fr/fr.json +++ b/rustmail_panel/src/i18n/fr/fr.json @@ -7,11 +7,47 @@ "error": { "error": "Erreur" }, - "panel": { - "welcome": "Bienvenue sur le panel de Rustmail", + "navbar": { + "home": "Accueil", + "config": "Configuration", + "tickets": "Tickets", "logout": "Se déconnecter" }, - "docs": { - "back_to_home": "Retour à l'accueil" + "panel": { + "title": "Panel Rustmail", + "welcome": "Bienvenue sur le panneau d'administration Rustmail", + "check_access": "Vérification de vos droits d'accès...", + "logout": "Se déconnecter", + "configuration": { + "title": "Configuration", + "description": "Gérez les paramètres de Rustmail ici.", + "bot_status": "Statut", + "bot_status_description": "Contrôlez l'état de Rustmail", + "start_bot": "Démarrer", + "stop_bot": "Arrêter", + "restart_bot": "Redémarrer", + "online": "En ligne", + "offline": "Hors ligne", + "config_file": { + "title": "Paramètres de Rustmail (pas encore implémenté)" + } + }, + "tickets": { + "title": "Tickets fermés", + "description": "Consultez l'historique des tickets fermés", + "sort_by": "Filtrer par catégorie", + "all": "Toutes les catégories", + "loading": "Chargement des tickets...", + "none": "Aucun ticket fermé trouvé.", + "none_category": "Inconnue", + "back_to_tickets": "← Retour aux tickets", + "user": "Utilisateur", + "category": "Catégorie", + "opened": "Ouvert", + "closed": "Fermé", + "unknown_date": "Date inconnue", + "loading_ticket": "Chargement du ticket...", + "ticket_not_found": "Ticket introuvable." + } } } \ No newline at end of file diff --git a/rustmail_panel/src/pages/docs.rs b/rustmail_panel/src/pages/docs.rs deleted file mode 100644 index fa7e5e5..0000000 --- a/rustmail_panel/src/pages/docs.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::components::language_switcher::LanguageSwitcher; -use i18nrs::yew::use_translation; -use yew::{Html, function_component, html}; - -#[function_component(Docs)] -pub fn docs() -> Html { - let (i18n, _set_language) = use_translation(); - - html! { - <> -
- Rustmail logo - - - { i18n.t("docs.back_to_home") } - - -

{"Rustmail Panel"}

-

- {"Work in progress..."} -

-
- - - } -} diff --git a/rustmail_panel/src/pages/mod.rs b/rustmail_panel/src/pages/mod.rs index 2a1e40f..e257814 100644 --- a/rustmail_panel/src/pages/mod.rs +++ b/rustmail_panel/src/pages/mod.rs @@ -1,4 +1,3 @@ -pub mod docs; pub mod error; pub mod home; pub mod panel; diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index cf259f4..103aa76 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -1,13 +1,15 @@ +use crate::components::configuration::ConfigurationPage; +use crate::components::home::Home; use crate::components::navbar::RustmailNavbar; +use crate::components::ticket::{TicketDetails, TicketsList}; use gloo_net::http::Request; use gloo_utils::window; +use i18nrs::yew::use_translation; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; use yew_router::hooks::use_navigator; -use yew_router::{navigator, BrowserRouter, Routable, Switch}; use yew_router::navigator::Navigator; -use crate::components::configuration::ConfigurationPage; -use crate::components::ticket::{TicketDetails, TicketsList, TicketsPage}; +use yew_router::{BrowserRouter, Routable, Switch}; #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct UserAvatar { @@ -26,9 +28,10 @@ pub enum PanelRoute { TicketDetails { id: String }, } - #[function_component(Panel)] pub fn panel() -> Html { + let (i18n, _set_language) = use_translation(); + let authorized = use_state(|| None::); let navigator = use_navigator(); { @@ -79,14 +82,13 @@ pub fn panel() -> Html { let avatar_url = (*avatar).clone().unwrap_or_default(); - html! { <> { match *authorized { None => html! {
-

{"Vérification de l'accès..."}

+

{i18n.t("panel.check_access")}

}, Some(true) => html! { @@ -108,15 +110,7 @@ pub fn panel() -> Html { fn switch(route: PanelRoute, navigator: Option) -> Html { match route { - PanelRoute::Home => html! { -
- Rustmail logo -

{"Rustmail Panel"}

-

- {"Bienvenue sur le panneau d'administration Rustmail"} -

-
- }, + PanelRoute::Home => html! { }, PanelRoute::Configuration => html! { }, PanelRoute::TicketsList => html! { }, PanelRoute::TicketDetails { id } => { diff --git a/rustmail_panel/src/router.rs b/rustmail_panel/src/router.rs index 202e541..90e4f64 100644 --- a/rustmail_panel/src/router.rs +++ b/rustmail_panel/src/router.rs @@ -1,4 +1,3 @@ -use crate::pages::docs::Docs; use crate::pages::error::Error; use crate::pages::home::Home; use crate::pages::panel::Panel; @@ -9,8 +8,6 @@ use yew_router::prelude::*; pub enum Route { #[at("/")] Home, - #[at("/docs")] - Documentation, #[at("/panel")] PanelRoot, #[at("/panel/*")] @@ -25,10 +22,8 @@ pub enum Route { pub fn switch(routes: Route) -> Html { match routes { Route::Home => html! { }, - Route::Documentation => html! { }, Route::PanelRoot | Route::Panel => html! { }, Route::Error => html! { }, Route::NotFound => html! {

{ "404" }

}, } } - From b3b94a50ca8efce71abe4ddb0bef5bd2ea221d28 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 20 Oct 2025 15:00:59 +0200 Subject: [PATCH 035/100] feat(panel): add 'redirect_url' config option for discord auth --- rustmail/src/api/handler/auth/callback.rs | 2 +- rustmail/src/api/handler/auth/login.rs | 6 +++--- rustmail/src/commands/edit/common.rs | 3 ++- rustmail/src/config.rs | 1 + rustmail/src/db/operations/messages.rs | 4 +++- rustmail/src/main.rs | 13 ++++++++----- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index 1345f47..0d283da 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -44,7 +44,7 @@ pub async fn handle_callback( ("client_secret", config.bot.client_secret.as_str()), ("grant_type", "authorization_code"), ("code", params.code.as_str()), - ("redirect_uri", "http://localhost:3002/api/auth/callback"), + ("redirect_uri", config.bot.redirect_url.as_str()), ]) .send() .await diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index 1256072..b1de68f 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -1,11 +1,11 @@ +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; use crate::BotState; use axum::extract::State; use axum::response::Redirect; use axum_extra::extract::CookieJar; -use sqlx::{Row, query}; +use sqlx::{query, Row}; use std::sync::Arc; use tokio::sync::Mutex; -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; pub async fn handle_login( jar: CookieJar, @@ -56,7 +56,7 @@ pub async fn handle_login( let url = format!( "https://discord.com/oauth2/authorize?client_id={}&redirect_uri={}&response_type=code&scope=identify%20guilds", bot_config.bot.client_id, - urlencoding::encode("http://localhost:3002/api/auth/callback") + urlencoding::encode(bot_config.bot.redirect_url.as_str()) ); Redirect::to(&url) diff --git a/rustmail/src/commands/edit/common.rs b/rustmail/src/commands/edit/common.rs index 172b03b..a475dbc 100644 --- a/rustmail/src/commands/edit/common.rs +++ b/rustmail/src/commands/edit/common.rs @@ -1,6 +1,6 @@ use crate::config::Config; -use crate::errors::ModmailResult; use crate::errors::common::invalid_command; +use crate::errors::ModmailResult; use crate::utils::command::extract_reply_content::extract_reply_content; use serenity::all::Message; @@ -38,6 +38,7 @@ mod tests { logs_channel_id: Some(15456), client_id: 123456789012345678, client_secret: "secret".to_string(), + redirect_url: "http://localhost:3002/api/auth/callback".to_string(), }, command: CommandConfig { prefix: "!".to_string(), diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index 7723ecd..25b4dbc 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -40,6 +40,7 @@ pub struct BotConfig { pub enable_panel: bool, pub client_id: u64, pub client_secret: String, + pub redirect_url: String, #[serde(default)] pub logs_channel_id: Option, diff --git a/rustmail/src/db/operations/messages.rs b/rustmail/src/db/operations/messages.rs index 9b4f4bb..fd9bd6c 100644 --- a/rustmail/src/db/operations/messages.rs +++ b/rustmail/src/db/operations/messages.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::db::operations::threads::get_user_name_from_thread_id; -use crate::errors::ModmailResult; use crate::errors::common::message_not_found; +use crate::errors::ModmailResult; use serenity::all::{Message, MessageId, UserId}; use sqlx::{Error, SqlitePool}; @@ -175,6 +175,8 @@ pub async fn get_latest_thread_message( .fetch_optional(pool) .await?; + println!("Debug: Fetched latest thread message row: {:?}", row); + let latest = row.map(|row| ThreadMessage { id: row.id, thread_id: row.thread_id, diff --git a/rustmail/src/main.rs b/rustmail/src/main.rs index dc01b82..5d5f382 100644 --- a/rustmail/src/main.rs +++ b/rustmail/src/main.rs @@ -122,13 +122,16 @@ async fn main() { .route("/", axum::routing::get(static_handler)) .route("/{*path}", axum::routing::get(static_handler)); - let listener = tokio::net::TcpListener::bind("127.0.0.1:3002").await.unwrap(); + let listener = tokio::net::TcpListener::bind("0.0.0.0:3002").await.unwrap(); println!("listening on {}", listener.local_addr().unwrap()); - axum::serve(listener, app.into_make_service_with_connect_info::()) - .with_graceful_shutdown(shutdown_signal()) - .await - .unwrap(); + axum::serve( + listener, + app.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); }); tokio::select! { From 4d298f0b1ab349bf48470e13ca73b6a1003caf37 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 21 Oct 2025 11:20:58 +0200 Subject: [PATCH 036/100] fix(recover): fixed a bug that prevented messages from being retrieved from a user when the thread was created by a staff member --- rustmail/src/api/handler/auth/login.rs | 4 +- rustmail/src/api/handler/auth/logout.rs | 2 +- rustmail/src/api/handler/bot/mod.rs | 4 +- rustmail/src/api/handler/bot/restart.rs | 41 ++++--- rustmail/src/api/handler/bot/status.rs | 22 ++-- rustmail/src/api/handler/user/avatar.rs | 12 +- rustmail/src/api/middleware/auth.rs | 9 +- rustmail/src/api/routes/bot.rs | 8 +- .../src/api/utils/get_user_id_from_session.rs | 11 +- rustmail/src/api/utils/mod.rs | 2 +- rustmail/src/api/utils/ping_internal.rs | 7 +- rustmail/src/bot.rs | 10 +- rustmail/src/commands/add_reminder/common.rs | 8 +- .../add_staff/text_command/add_staff.rs | 2 +- rustmail/src/commands/alert/common.rs | 2 +- .../src/commands/close/slash_command/close.rs | 83 +++++++++---- .../src/commands/close/text_command/close.rs | 86 +++++++++---- rustmail/src/commands/delete/common.rs | 2 +- rustmail/src/commands/edit/common.rs | 2 +- .../src/commands/edit/text_command/edit.rs | 4 +- .../src/commands/help/text_command/help.rs | 2 +- rustmail/src/commands/id/text_command/id.rs | 2 +- rustmail/src/commands/move_thread/common.rs | 2 +- rustmail/src/commands/new_thread/common.rs | 20 +++- .../commands/recover/text_command/recover.rs | 4 +- .../text_command/remove_reminder.rs | 2 +- .../remove_staff/text_command/remove_staff.rs | 2 +- .../src/commands/reply/text_command/reply.rs | 2 +- rustmail/src/db/operations/messages.rs | 12 +- rustmail/src/db/operations/mod.rs | 5 +- rustmail/src/db/operations/scheduled.rs | 26 ++-- rustmail/src/db/operations/threads.rs | 15 ++- rustmail/src/handlers/guild_handler.rs | 19 ++- .../src/handlers/guild_members_handler.rs | 33 ++--- .../src/handlers/guild_messages_handler.rs | 4 +- .../src/handlers/guild_moderation_handler.rs | 4 +- rustmail/src/modules/message_recovery.rs | 4 +- rustmail/src/modules/scheduled_closures.rs | 24 +++- rustmail/src/modules/threads.rs | 8 +- rustmail/src/utils/command/category.rs | 38 +++--- rustmail/src/utils/command/mod.rs | 2 +- rustmail/src/utils/message/category.rs | 36 +++--- rustmail/src/utils/message/message_builder.rs | 113 ++++++++++++++---- rustmail/src/utils/message/mod.rs | 2 +- rustmail/src/utils/thread/category.rs | 36 +++--- rustmail/src/utils/thread/mod.rs | 2 +- rustmail/src/utils/thread/send_to_thread.rs | 13 +- rustmail_panel/src/components/navbar.rs | 2 +- 48 files changed, 473 insertions(+), 282 deletions(-) diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index b1de68f..3ff26f8 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -1,9 +1,9 @@ -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; use crate::BotState; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; use axum::extract::State; use axum::response::Redirect; use axum_extra::extract::CookieJar; -use sqlx::{query, Row}; +use sqlx::{Row, query}; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/auth/logout.rs b/rustmail/src/api/handler/auth/logout.rs index aff8936..8d73c17 100644 --- a/rustmail/src/api/handler/auth/logout.rs +++ b/rustmail/src/api/handler/auth/logout.rs @@ -1,10 +1,10 @@ use crate::BotState; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; use axum::extract::State; use axum::response::Redirect; use axum_extra::extract::CookieJar; use std::sync::Arc; use tokio::sync::Mutex; -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; pub async fn handle_logout( jar: CookieJar, diff --git a/rustmail/src/api/handler/bot/mod.rs b/rustmail/src/api/handler/bot/mod.rs index 88ba233..d85da0f 100644 --- a/rustmail/src/api/handler/bot/mod.rs +++ b/rustmail/src/api/handler/bot/mod.rs @@ -1,5 +1,5 @@ +pub mod restart; pub mod start; -pub mod stop; pub mod status; -pub mod restart; +pub mod stop; pub mod tickets; diff --git a/rustmail/src/api/handler/bot/restart.rs b/rustmail/src/api/handler/bot/restart.rs index 2337fd7..a91c787 100644 --- a/rustmail/src/api/handler/bot/restart.rs +++ b/rustmail/src/api/handler/bot/restart.rs @@ -1,12 +1,12 @@ -use std::sync::Arc; -use std::time::Duration; +use crate::api::utils::ping_internal::ping_internal; +use crate::{BotState, BotStatus}; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use axum::Json; use sqlx::__rt::sleep; +use std::sync::Arc; +use std::time::Duration; use tokio::sync::Mutex; -use crate::{BotState, BotStatus}; -use crate::api::utils::ping_internal::ping_internal; pub async fn handle_restart_bot( State(bot_state): State>>, @@ -22,9 +22,12 @@ pub async fn handle_restart_bot( BotStatus::Stopped => { drop(state_lock); - let result = ping_internal("/api/bot/start", &token) - .await - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json("Failed to restart bot"))); + let result = ping_internal("/api/bot/start", &token).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json("Failed to restart bot"), + ) + }); println!("Restart result: {:?}", result); @@ -33,17 +36,23 @@ pub async fn handle_restart_bot( BotStatus::Running { .. } => { drop(state_lock); - let _ = ping_internal("/api/bot/stop", &token) - .await - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json("Failed to stop bot"))); + let _ = ping_internal("/api/bot/stop", &token).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json("Failed to stop bot"), + ) + }); sleep(Duration::from_secs(2)).await; - let _ = ping_internal("/api/bot/start", &token) - .await - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json("Failed to stop bot"))); + let _ = ping_internal("/api/bot/start", &token).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json("Failed to stop bot"), + ) + }); (StatusCode::OK, Json("Bot is restarting")) - }, + } } -} \ No newline at end of file +} diff --git a/rustmail/src/api/handler/bot/status.rs b/rustmail/src/api/handler/bot/status.rs index 9421d16..3868b58 100644 --- a/rustmail/src/api/handler/bot/status.rs +++ b/rustmail/src/api/handler/bot/status.rs @@ -1,18 +1,22 @@ -use std::sync::Arc; +use crate::BotState; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use axum::Json; use axum::response::IntoResponse; +use std::sync::Arc; use tokio::sync::Mutex; -use crate::BotState; -pub async fn handle_status_bot( - State(bot_state): State>>, -) -> impl IntoResponse { +pub async fn handle_status_bot(State(bot_state): State>>) -> impl IntoResponse { let state_lock = bot_state.lock().await; match state_lock.status { - crate::BotStatus::Running { .. } => (StatusCode::OK, Json(serde_json::json!({"status": "running"}))), - crate::BotStatus::Stopped => (StatusCode::OK, Json(serde_json::json!({"status": "stopped"}))), + crate::BotStatus::Running { .. } => ( + StatusCode::OK, + Json(serde_json::json!({"status": "running"})), + ), + crate::BotStatus::Stopped => ( + StatusCode::OK, + Json(serde_json::json!({"status": "stopped"})), + ), } -} \ No newline at end of file +} diff --git a/rustmail/src/api/handler/user/avatar.rs b/rustmail/src/api/handler/user/avatar.rs index b350042..e0df027 100644 --- a/rustmail/src/api/handler/user/avatar.rs +++ b/rustmail/src/api/handler/user/avatar.rs @@ -1,11 +1,11 @@ use crate::BotState; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; use axum::extract::State; use axum::response::IntoResponse; -use std::sync::Arc; use axum_extra::extract::CookieJar; use sqlx::Row; +use std::sync::Arc; use tokio::sync::Mutex; -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct UserAvatar { @@ -16,7 +16,6 @@ pub async fn handle_get_user_avatar( State(bot_state): State>>, jar: CookieJar, ) -> impl IntoResponse { - let pool = { let state_lock = bot_state.lock().await; if let Some(pool) = &state_lock.db_pool { @@ -42,11 +41,10 @@ pub async fn handle_get_user_avatar( } }; - let user_avatar = match sqlx::query( - "SELECT avatar_hash FROM sessions_panel WHERE user_id = ?") + let user_avatar = match sqlx::query("SELECT avatar_hash FROM sessions_panel WHERE user_id = ?") .bind(user_id_str.clone()) - .fetch_one(&pool) - .await + .fetch_one(&pool) + .await { Ok(record) => { let avatar_hash: String = record.get::("avatar_hash"); diff --git a/rustmail/src/api/middleware/auth.rs b/rustmail/src/api/middleware/auth.rs index 6b5cb74..995aea8 100644 --- a/rustmail/src/api/middleware/auth.rs +++ b/rustmail/src/api/middleware/auth.rs @@ -1,8 +1,7 @@ -use subtle::ConstantTimeEq; -use std::net::SocketAddr; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; use crate::{BotCommand, BotState, BotStatus}; -use axum::extract::{ConnectInfo, Request}; use axum::extract::State; +use axum::extract::{ConnectInfo, Request}; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; use axum_extra::extract::CookieJar; @@ -10,9 +9,10 @@ use chrono::Utc; use hyper::StatusCode; use serenity::all::{GuildId, UserId}; use sqlx::{Row, query}; +use std::net::SocketAddr; use std::sync::Arc; +use subtle::ConstantTimeEq; use tokio::sync::Mutex; -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; async fn check_user_with_bot(bot_state: Arc>, user_id: &str) -> bool { let user_id_num = match user_id.parse::() { @@ -89,7 +89,6 @@ pub async fn auth_middleware( req: Request, next: Next, ) -> Response { - if addr.ip().is_loopback() { if let Some(h) = req.headers().get("x-internal-call") { if let Ok(s) = h.to_str() { diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index ff977a8..e982c89 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -1,14 +1,14 @@ use crate::BotState; +use crate::api::handler::bot::restart::handle_restart_bot; use crate::api::handler::bot::start::handle_start_bot; +use crate::api::handler::bot::status::handle_status_bot; use crate::api::handler::bot::stop::handle_stop_bot; +use crate::api::handler::bot::tickets::handle_tickets_bot; +use crate::api::middleware::auth::auth_middleware; use axum::Router; use axum::routing::{get, post}; use std::sync::Arc; use tokio::sync::Mutex; -use crate::api::handler::bot::restart::handle_restart_bot; -use crate::api::handler::bot::status::handle_status_bot; -use crate::api::handler::bot::tickets::{handle_tickets_bot}; -use crate::api::middleware::auth::auth_middleware; pub fn create_bot_router(bot_state: Arc>) -> Router>> { let bot_router = Router::new() diff --git a/rustmail/src/api/utils/get_user_id_from_session.rs b/rustmail/src/api/utils/get_user_id_from_session.rs index 9feaa85..4b4af40 100644 --- a/rustmail/src/api/utils/get_user_id_from_session.rs +++ b/rustmail/src/api/utils/get_user_id_from_session.rs @@ -1,7 +1,6 @@ -use sqlx::{query, Row, SqlitePool}; - -pub async fn get_user_id_from_session(session_id: &str, db_pool: &SqlitePool) -> String{ +use sqlx::{Row, SqlitePool, query}; +pub async fn get_user_id_from_session(session_id: &str, db_pool: &SqlitePool) -> String { let result = query("SELECT user_id FROM sessions_panel WHERE session_id = ?") .bind(session_id) .fetch_one(db_pool) @@ -11,9 +10,7 @@ pub async fn get_user_id_from_session(session_id: &str, db_pool: &SqlitePool) -> Ok(row) => { let user_id: String = row.get::("user_id"); user_id - }, - Err(_) => { - "".to_string() } + Err(_) => "".to_string(), } -} \ No newline at end of file +} diff --git a/rustmail/src/api/utils/mod.rs b/rustmail/src/api/utils/mod.rs index 4981a94..8f5fd4d 100644 --- a/rustmail/src/api/utils/mod.rs +++ b/rustmail/src/api/utils/mod.rs @@ -1,2 +1,2 @@ pub mod get_user_id_from_session; -pub mod ping_internal; \ No newline at end of file +pub mod ping_internal; diff --git a/rustmail/src/api/utils/ping_internal.rs b/rustmail/src/api/utils/ping_internal.rs index c48bc38..6fd5c6f 100644 --- a/rustmail/src/api/utils/ping_internal.rs +++ b/rustmail/src/api/utils/ping_internal.rs @@ -1,6 +1,9 @@ use reqwest::Client; -pub async fn ping_internal(endpoint: &str, token: &str) -> Result { +pub async fn ping_internal( + endpoint: &str, + token: &str, +) -> Result { let client = Client::new(); let url = format!("http://127.0.0.1:3002{}", endpoint); client @@ -8,4 +11,4 @@ pub async fn ping_internal(endpoint: &str, token: &str) -> Result = None; - let mut silent: Option = None; + let mut silent: bool = false; let mut cancel: Option = None; let mut duration: Option = None; @@ -124,7 +130,7 @@ impl RegistrableCommand for CloseCommand { } "silent" => { if let CommandDataOptionValue::Boolean(val) = &option.value { - silent.replace(*val); + silent = *val; } } "cancel" => { @@ -201,9 +207,7 @@ impl RegistrableCommand for CloseCommand { let mut params = HashMap::new(); params.insert("time".to_string(), human); - let _ = if let Some(silent) = silent - && silent - { + let _ = if silent { let response = MessageBuilder::system_message(&ctx, &config) .translated_content( "close.silent_closing", @@ -235,15 +239,24 @@ impl RegistrableCommand for CloseCommand { let thread_id = thread.id.clone(); let close_at = Utc::now().timestamp() + delay.as_secs() as i64; - let silent = silent.unwrap_or(false); let closed_by = command.user.id.to_string(); let category_id = get_category_id_from_command(&ctx, &command).await; let category_name = get_category_name_from_command(&ctx, &command).await; - let required_permissions = get_required_permissions_channel_from_command(&ctx, &command).await; - - if let Err(e) = - upsert_scheduled_closure(&thread_id, close_at, silent, &closed_by, &category_id, &category_name, &required_permissions.to_string(), db_pool).await + let required_permissions = + get_required_permissions_channel_from_command(&ctx, &command).await; + + if let Err(e) = upsert_scheduled_closure( + &thread_id, + close_at, + silent, + &closed_by, + &category_id, + &category_name, + &required_permissions.to_string(), + db_pool, + ) + .await { eprintln!("Failed to persist scheduled closure: {e:?}"); } @@ -260,7 +273,15 @@ impl RegistrableCommand for CloseCommand { get_scheduled_closure(&thread_id_for_task, pool).await { if record.close_at <= Utc::now().timestamp() { - let _ = close_thread(&thread_id_for_task, &record.closed_by, &record.category_id, &record.category_name, record.required_permissions.parse::().unwrap_or(0), pool).await; + let _ = close_thread( + &thread_id_for_task, + &record.closed_by, + &record.category_id, + &record.category_name, + record.required_permissions.parse::().unwrap_or(0), + pool, + ) + .await; let _ = delete_scheduled_closure(&thread_id_for_task, pool).await; let community_guild_id = @@ -276,7 +297,7 @@ impl RegistrableCommand for CloseCommand { MessageBuilder::system_message(&ctx_clone, &config_clone) .content(&config_clone.bot.close_message) .to_user(user_id_clone) - .send() + .send(false) .await; } let _ = channel_id.delete(&ctx_clone.http).await; @@ -294,7 +315,17 @@ impl RegistrableCommand for CloseCommand { get_scheduled_closure(&thread_id_again, pool2).await { if r2.close_at <= Utc::now().timestamp() { - let _ = close_thread(&thread_id_again, &r2.closed_by, &r2.category_id, &r2.category_name, r2.required_permissions.parse::().unwrap_or(0), pool2).await; + let _ = close_thread( + &thread_id_again, + &r2.closed_by, + &r2.category_id, + &r2.category_name, + r2.required_permissions + .parse::() + .unwrap_or(0), + pool2, + ) + .await; let _ = delete_scheduled_closure( &thread_id_again, pool2, @@ -314,7 +345,7 @@ impl RegistrableCommand for CloseCommand { ) .content(&config_clone2.bot.close_message) .to_user(user_id_clone) - .send() + .send(false) .await; } let _ = channel_id.delete(&ctx_clone2.http).await; @@ -334,16 +365,14 @@ impl RegistrableCommand for CloseCommand { let closed_by = command.user.id.to_string(); let category_id = get_category_id_from_command(&ctx, &command).await; let category_name = get_category_name_from_command(&ctx, &command).await; - let required_permissions = get_required_permissions_channel_from_command(&ctx, &command).await; + let required_permissions = + get_required_permissions_channel_from_command(&ctx, &command).await; - if user_still_member - && let Some(silent) = silent - && !silent - { + if user_still_member && !silent { let _ = MessageBuilder::system_message(&ctx, &config) .content(&config.bot.close_message) .to_user(user_id) - .send() + .send(false) .await; } else if !user_still_member { let mut params = HashMap::new(); @@ -364,7 +393,15 @@ impl RegistrableCommand for CloseCommand { let _ = command.create_followup(&ctx.http, response).await; } - close_thread(&thread.id, &closed_by, &category_id, &category_name, required_permissions, db_pool).await?; + close_thread( + &thread.id, + &closed_by, + &category_id, + &category_name, + required_permissions, + db_pool, + ) + .await?; let _ = delete_scheduled_closure(&thread.id, db_pool).await; let _ = command.channel_id.delete(&ctx.http).await?; diff --git a/rustmail/src/commands/close/text_command/close.rs b/rustmail/src/commands/close/text_command/close.rs index b1ceb36..d3aa713 100644 --- a/rustmail/src/commands/close/text_command/close.rs +++ b/rustmail/src/commands/close/text_command/close.rs @@ -4,6 +4,10 @@ use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::utils::message::category::{ + get_category_id_from_message, get_category_name_from_message, + get_required_permissions_channel_from_message, +}; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::fetch_thread::fetch_thread; use chrono::Utc; @@ -13,7 +17,6 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::watch::Receiver; use tokio::time::sleep; -use crate::utils::message::category::{get_category_id_from_message, get_category_name_from_message, get_required_permissions_channel_from_message}; pub async fn close( ctx: &Context, @@ -88,7 +91,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } else { let _ = MessageBuilder::system_message(ctx, config) @@ -100,7 +103,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } return Ok(()); @@ -123,7 +126,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; return Ok(()); } @@ -154,7 +157,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } else { let _ = MessageBuilder::system_message(ctx, config) @@ -166,27 +169,23 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; }; let closed_by = msg.author.id.to_string(); let category_id = match msg.channel_id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.category() { - Some(category) => category.id.to_string(), - None => String::new(), - } - } + Ok(channel) => match channel.category() { + Some(category) => category.id.to_string(), + None => String::new(), + }, _ => String::new(), }; let category_name = match msg.channel_id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.category() { - Some(category) => category.name.clone(), - None => String::new(), - } - } + Ok(channel) => match channel.category() { + Some(category) => category.name.clone(), + None => String::new(), + }, _ => String::new(), }; @@ -216,7 +215,18 @@ pub async fn close( let thread_id = thread.id.clone(); let close_at = Utc::now().timestamp() + delay.as_secs() as i64; - if let Err(e) = upsert_scheduled_closure(&thread_id, close_at, silent, &closed_by, &category_id, &category_name, &required_permissions.to_string(), db_pool).await { + if let Err(e) = upsert_scheduled_closure( + &thread_id, + close_at, + silent, + &closed_by, + &category_id, + &category_name, + &required_permissions.to_string(), + db_pool, + ) + .await + { eprintln!("Failed to persist scheduled closure: {e:?}"); } let channel_id = msg.channel_id; @@ -230,7 +240,15 @@ pub async fn close( if let Some(pool) = config_clone.db_pool.as_ref() { if let Ok(Some(record)) = get_scheduled_closure(&thread_id_for_task, pool).await { if record.close_at <= Utc::now().timestamp() { - let _ = close_thread(&thread_id_for_task, &record.closed_by, &record.category_id, &category_name, record.required_permissions.parse::().unwrap_or(0), pool).await; + let _ = close_thread( + &thread_id_for_task, + &record.closed_by, + &record.category_id, + &category_name, + record.required_permissions.parse::().unwrap_or(0), + pool, + ) + .await; let _ = delete_scheduled_closure(&thread_id_for_task, pool).await; let community_guild_id = @@ -245,7 +263,7 @@ pub async fn close( let _ = MessageBuilder::system_message(&ctx_clone, &config_clone) .content(&config_clone.bot.close_message) .to_user(user_id_clone) - .send() + .send(false) .await; } let _ = channel_id.delete(&ctx_clone.http).await; @@ -262,7 +280,15 @@ pub async fn close( get_scheduled_closure(&thread_id_again, pool2).await { if r2.close_at <= Utc::now().timestamp() { - let _ = close_thread(&thread_id_again, &r2.closed_by, &r2.category_id, &r2.category_id, r2.required_permissions.parse::().unwrap_or(0), pool2).await; + let _ = close_thread( + &thread_id_again, + &r2.closed_by, + &r2.category_id, + &r2.category_id, + r2.required_permissions.parse::().unwrap_or(0), + pool2, + ) + .await; let _ = delete_scheduled_closure(&thread_id_again, pool2).await; let community_guild_id = GuildId::new( @@ -279,7 +305,7 @@ pub async fn close( ) .content(&config_clone2.bot.close_message) .to_user(user_id_clone) - .send() + .send(false) .await; } let _ = channel_id.delete(&ctx_clone2.http).await; @@ -305,7 +331,7 @@ pub async fn close( let _ = MessageBuilder::system_message(ctx, config) .content(&config.bot.close_message) .to_user(user_id) - .send() + .send(false) .await; } else if !user_still_member { let mut params = HashMap::new(); @@ -320,11 +346,19 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } - close_thread(&thread.id, &closed_by, &category_id, &category_name, required_permissions, db_pool).await?; + close_thread( + &thread.id, + &closed_by, + &category_id, + &category_name, + required_permissions, + db_pool, + ) + .await?; let _ = delete_scheduled_closure(&thread.id, db_pool).await; let _ = msg.channel_id.delete(&ctx.http).await?; diff --git a/rustmail/src/commands/delete/common.rs b/rustmail/src/commands/delete/common.rs index 3b07a28..cc102a8 100644 --- a/rustmail/src/commands/delete/common.rs +++ b/rustmail/src/commands/delete/common.rs @@ -166,6 +166,6 @@ pub async fn send_delete_message( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } diff --git a/rustmail/src/commands/edit/common.rs b/rustmail/src/commands/edit/common.rs index a475dbc..29bbaaf 100644 --- a/rustmail/src/commands/edit/common.rs +++ b/rustmail/src/commands/edit/common.rs @@ -1,6 +1,6 @@ use crate::config::Config; -use crate::errors::common::invalid_command; use crate::errors::ModmailResult; +use crate::errors::common::invalid_command; use crate::utils::command::extract_reply_content::extract_reply_content; use serenity::all::Message; diff --git a/rustmail/src/commands/edit/text_command/edit.rs b/rustmail/src/commands/edit/text_command/edit.rs index 7a6a560..57c79c2 100644 --- a/rustmail/src/commands/edit/text_command/edit.rs +++ b/rustmail/src/commands/edit/text_command/edit.rs @@ -118,7 +118,7 @@ pub async fn edit( .await .color(hex_string_to_int(&config.thread.system_message_color) as u32) .to_channel(msg.channel_id) - .send() + .send(false) .await; }; @@ -154,7 +154,7 @@ pub async fn edit( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } diff --git a/rustmail/src/commands/help/text_command/help.rs b/rustmail/src/commands/help/text_command/help.rs index 82c4980..fad8403 100644 --- a/rustmail/src/commands/help/text_command/help.rs +++ b/rustmail/src/commands/help/text_command/help.rs @@ -29,7 +29,7 @@ pub async fn help( MessageBuilder::system_message(ctx, config) .content(help_message) .to_channel(msg.channel_id) - .send() + .send(false) .await?; Ok(()) diff --git a/rustmail/src/commands/id/text_command/id.rs b/rustmail/src/commands/id/text_command/id.rs index 4ba8d81..f1ea5d4 100644 --- a/rustmail/src/commands/id/text_command/id.rs +++ b/rustmail/src/commands/id/text_command/id.rs @@ -37,7 +37,7 @@ pub async fn id( .translated_content("id.show_id", Some(¶ms), None, None) .await .to_channel(msg.channel_id) - .send() + .send(false) .await?; Ok(()) diff --git a/rustmail/src/commands/move_thread/common.rs b/rustmail/src/commands/move_thread/common.rs index 6c2b9dd..841b597 100644 --- a/rustmail/src/commands/move_thread/common.rs +++ b/rustmail/src/commands/move_thread/common.rs @@ -89,7 +89,7 @@ pub async fn send_success_message( let _ = MessageBuilder::system_message(ctx, config) .content(confirmation_msg) .to_channel(msg.channel_id) - .send() + .send(false) .await; let _ = msg.delete(&ctx.http).await; diff --git a/rustmail/src/commands/new_thread/common.rs b/rustmail/src/commands/new_thread/common.rs index 6fe0dba..5021b84 100644 --- a/rustmail/src/commands/new_thread/common.rs +++ b/rustmail/src/commands/new_thread/common.rs @@ -60,7 +60,19 @@ pub async fn send_welcome_message( ) .await .to_channel(channel.id) - .send() + .send(false) + .await; + + let _ = MessageBuilder::system_message(ctx, config) + .translated_content( + "new_thread.welcome_message", + Some(¶ms), + None, + Some(channel.guild_id.get()), + ) + .await + .to_user(user.id) + .send(true) .await; } @@ -69,7 +81,7 @@ pub async fn send_dm_to_user(ctx: &Context, user: &User, config: &Config) -> Mod .translated_content("new_thread.dm_notification", None, Some(user.id), None) .await .to_user(user.id) - .send() + .send(false) .await; Ok(()) @@ -91,7 +103,7 @@ pub async fn send_error_message( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } @@ -123,6 +135,6 @@ pub async fn send_success_message( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } diff --git a/rustmail/src/commands/recover/text_command/recover.rs b/rustmail/src/commands/recover/text_command/recover.rs index 7cd800c..d174b8a 100644 --- a/rustmail/src/commands/recover/text_command/recover.rs +++ b/rustmail/src/commands/recover/text_command/recover.rs @@ -35,7 +35,7 @@ pub async fn recover( let _ = MessageBuilder::system_message(ctx, config) .content(confirmation_message) .to_channel(msg.channel_id) - .send() + .send(false) .await; let ctx_clone = ctx.clone(); @@ -67,7 +67,7 @@ pub async fn recover( let _ = MessageBuilder::system_message(&ctx_clone, &config_clone) .content(summary_message) .to_channel(channel_id) - .send() + .send(false) .await; }); diff --git a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs index 087b184..f8be6cf 100644 --- a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -64,7 +64,7 @@ pub async fn remove_reminder( .translated_content("remove_reminder.confirmation", Some(¶ms), None, None) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } Err(e) => { diff --git a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs index e383d83..128cbd2 100644 --- a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs @@ -42,7 +42,7 @@ pub async fn remove_staff( .translated_content("add_staff.remove_success", Some(¶ms), None, None) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; Ok(()) diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index ac0201b..61c473e 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -91,7 +91,7 @@ pub async fn reply( ) .await .to_channel(msg.channel_id) - .send() + .send(false) .await; } diff --git a/rustmail/src/db/operations/messages.rs b/rustmail/src/db/operations/messages.rs index fd9bd6c..2a8c35b 100644 --- a/rustmail/src/db/operations/messages.rs +++ b/rustmail/src/db/operations/messages.rs @@ -1,8 +1,9 @@ use crate::config::Config; use crate::db::operations::threads::get_user_name_from_thread_id; -use crate::errors::common::message_not_found; use crate::errors::ModmailResult; +use crate::errors::common::message_not_found; use serenity::all::{Message, MessageId, UserId}; +use serenity::client::Context; use sqlx::{Error, SqlitePool}; #[derive(Debug, Clone)] @@ -12,6 +13,7 @@ pub struct MessageIds { } pub async fn insert_staff_message( + ctx: &Context, inbox_msg: &Message, dm_msg_id: Option, thread_id: &str, @@ -19,14 +21,16 @@ pub async fn insert_staff_message( is_anonymous: bool, pool: &SqlitePool, config: &Config, - message_number: u64, + message_number: i64, ) -> Result<(), Error> { let inbox_message_id = inbox_msg.id.to_string(); let user_id = staff_user_id.get() as i64; - let user_name = get_user_name_from_thread_id(thread_id, pool) + let user_name = staff_user_id + .to_user(&ctx.http) .await - .unwrap_or_else(|| "Unknown".to_string()); + .map(|user| user.name) + .unwrap_or_else(|_| "Unknown".to_string()); let content = extract_message_content(inbox_msg, config); let message_number_i64 = message_number as i64; diff --git a/rustmail/src/db/operations/mod.rs b/rustmail/src/db/operations/mod.rs index bdbe213..3d88641 100644 --- a/rustmail/src/db/operations/mod.rs +++ b/rustmail/src/db/operations/mod.rs @@ -9,9 +9,8 @@ pub use features::{get_feature_message, upsert_feature_message}; pub use init::{get_last_recovery_timestamp, init_database, update_last_recovery_timestamp}; pub use messages::{ delete_message, get_latest_thread_message, get_message_ids_by_message_id, - get_message_ids_by_number, get_thread_message_by_inbox_message_id, - insert_staff_message, insert_user_message_with_ids, update_message_content, - update_message_numbers_after_deletion, + get_message_ids_by_number, get_thread_message_by_inbox_message_id, insert_staff_message, + insert_user_message_with_ids, update_message_content, update_message_numbers_after_deletion, }; pub use scheduled::{ delete_scheduled_closure, get_all_scheduled_closures, get_scheduled_closure, diff --git a/rustmail/src/db/operations/scheduled.rs b/rustmail/src/db/operations/scheduled.rs index 94705d0..6fa7575 100644 --- a/rustmail/src/db/operations/scheduled.rs +++ b/rustmail/src/db/operations/scheduled.rs @@ -43,19 +43,19 @@ pub async fn upsert_scheduled_closure( required_permissions = excluded.required_permissions "#, ) - .bind(thread_id) - .bind(close_at) - .bind(silent) - .bind(closed_by) - .bind(category_id) - .bind(category_name) - .bind(required_permissions) - .execute(pool) - .await - .map_err(|e| { - eprintln!("Failed to upsert scheduled closure: {e:?}"); - common::validation_failed("Failed to upsert scheduled closure") - })?; + .bind(thread_id) + .bind(close_at) + .bind(silent) + .bind(closed_by) + .bind(category_id) + .bind(category_name) + .bind(required_permissions) + .execute(pool) + .await + .map_err(|e| { + eprintln!("Failed to upsert scheduled closure: {e:?}"); + common::validation_failed("Failed to upsert scheduled closure") + })?; Ok(()) } diff --git a/rustmail/src/db/operations/threads.rs b/rustmail/src/db/operations/threads.rs index a4430cf..fee528c 100644 --- a/rustmail/src/db/operations/threads.rs +++ b/rustmail/src/db/operations/threads.rs @@ -1,6 +1,6 @@ -use chrono::Utc; use crate::db::repr::Thread; use crate::errors::ModmailResult; +use chrono::Utc; use serenity::all::{ChannelId, GuildChannel, UserId}; use sqlx::{Error, SqlitePool}; use uuid::Uuid; @@ -145,7 +145,14 @@ pub async fn create_thread_for_user( } } -pub async fn close_thread(thread_id: &str, closed_by: &str, category_id: &str, category_name: &str, required_permissions: u64, pool: &SqlitePool) -> ModmailResult<()> { +pub async fn close_thread( + thread_id: &str, + closed_by: &str, + category_id: &str, + category_name: &str, + required_permissions: u64, + pool: &SqlitePool, +) -> ModmailResult<()> { let closed_at = Utc::now().timestamp(); let required_permissions = required_permissions.clone().to_string(); @@ -168,8 +175,8 @@ pub async fn close_thread(thread_id: &str, closed_by: &str, category_id: &str, c required_permissions, thread_id ) - .execute(pool) - .await?; + .execute(pool) + .await?; Ok(()) } diff --git a/rustmail/src/handlers/guild_handler.rs b/rustmail/src/handlers/guild_handler.rs index 5a2cb9c..3d84fe3 100644 --- a/rustmail/src/handlers/guild_handler.rs +++ b/rustmail/src/handlers/guild_handler.rs @@ -2,10 +2,13 @@ use crate::config::Config; use crate::db::close_thread; use crate::db::operations::get_thread_by_channel_id; use crate::db::threads::is_an_opened_ticket_channel; +use crate::utils::thread::category::{ + get_category_id_from_guild_channel, get_category_name_from_guild_channel, + get_required_permissions_channel_from_guild_channel, +}; use async_trait::async_trait; use serenity::all::{Context, GuildChannel, Message}; use serenity::client::EventHandler; -use crate::utils::thread::category::{get_category_id_from_guild_channel, get_category_name_from_guild_channel, get_required_permissions_channel_from_guild_channel}; pub struct GuildHandler { pub config: Config, @@ -46,9 +49,19 @@ impl EventHandler for GuildHandler { let closed_by = "deleted_by_client".to_string(); let category_id = get_category_id_from_guild_channel(&ctx, &channel).await; let category_name = get_category_name_from_guild_channel(&ctx, &channel).await; - let required_permissions = get_required_permissions_channel_from_guild_channel(&ctx, &channel).await; + let required_permissions = + get_required_permissions_channel_from_guild_channel(&ctx, &channel).await; - match close_thread(&thread.id, &closed_by, &category_id, &category_name, required_permissions, pool).await { + match close_thread( + &thread.id, + &closed_by, + &category_id, + &category_name, + required_permissions, + pool, + ) + .await + { Ok(_) => { println!("Close thread successfully by deleted channel!"); } diff --git a/rustmail/src/handlers/guild_members_handler.rs b/rustmail/src/handlers/guild_members_handler.rs index cc2492d..2630c44 100644 --- a/rustmail/src/handlers/guild_members_handler.rs +++ b/rustmail/src/handlers/guild_members_handler.rs @@ -11,7 +11,6 @@ use serenity::{ async_trait, }; use std::collections::HashMap; -use crate::utils::thread::category::get_required_permissions_channel_from_guild_channel; pub struct GuildMembersHandler { pub config: Config, @@ -95,7 +94,7 @@ impl EventHandler for GuildMembersHandler { .await .to_channel(channel_id) .components(close_buttons) - .send() + .send(false) .await; if let Err(e) = update_thread_user_left(&thread.channel_id, pool).await { @@ -104,21 +103,17 @@ impl EventHandler for GuildMembersHandler { let closed_by = "user_left_server".to_string(); let category_id = match channel_id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.category() { - Some(category) => category.id.to_string(), - None => String::new(), - } - } + Ok(channel) => match channel.category() { + Some(category) => category.id.to_string(), + None => String::new(), + }, _ => String::new(), }; let category_name = match channel_id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.category() { - Some(category) => category.name.clone(), - None => String::new(), - } - } + Ok(channel) => match channel.category() { + Some(category) => category.name.clone(), + None => String::new(), + }, _ => String::new(), }; @@ -146,6 +141,14 @@ impl EventHandler for GuildMembersHandler { _ => 0u64, }; - let _ = close_thread(&thread.id, &closed_by, &category_id, &category_name, required_permissions, pool).await; + let _ = close_thread( + &thread.id, + &closed_by, + &category_id, + &category_name, + required_permissions, + pool, + ) + .await; } } diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 3cc0f9c..5118416 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -268,7 +268,7 @@ impl EventHandler for GuildMessagesHandler { .to_channel(ChannelId::new( thread.channel_id.parse::().unwrap_or(0), )) - .send() + .send(false) .await; } @@ -396,7 +396,7 @@ impl EventHandler for GuildMessagesHandler { ) .await .to_channel(channel_id_parse) - .send() + .send(false) .await; } diff --git a/rustmail/src/handlers/guild_moderation_handler.rs b/rustmail/src/handlers/guild_moderation_handler.rs index 2bcdfb3..4eef1c5 100644 --- a/rustmail/src/handlers/guild_moderation_handler.rs +++ b/rustmail/src/handlers/guild_moderation_handler.rs @@ -246,7 +246,7 @@ async fn manage_creating_ticket_via_opening_thread( .await .components(res_button) .to_channel(channel_id_inner) - .send() + .send(false) .await; } @@ -289,7 +289,7 @@ impl EventHandler for GuildModerationHandler { let _ = MessageBuilder::user_message(&ctx, &self.config, user.id, user.name) .to_channel(ChannelId::new(logs_channel_id)) .content(format_audit_log(&entry)) - .send() + .send(false) .await; } } diff --git a/rustmail/src/modules/message_recovery.rs b/rustmail/src/modules/message_recovery.rs index 4312f45..b5db7d5 100644 --- a/rustmail/src/modules/message_recovery.rs +++ b/rustmail/src/modules/message_recovery.rs @@ -86,6 +86,8 @@ async fn recover_messages_for_thread( } }; + println!("Recovering messages for thread {:?}", latest_message); + let last_dm_message_id = match &latest_message.dm_message_id { Some(id) => id, None => { @@ -193,7 +195,7 @@ async fn recover_messages_for_thread( ) .await .to_channel(channel_id) - .send() + .send(false) .await; } diff --git a/rustmail/src/modules/scheduled_closures.rs b/rustmail/src/modules/scheduled_closures.rs index 5af25e3..55a82cf 100644 --- a/rustmail/src/modules/scheduled_closures.rs +++ b/rustmail/src/modules/scheduled_closures.rs @@ -26,13 +26,21 @@ fn schedule_one(ctx: &Context, config: &Config, thread_id: String, close_at: i64 ChannelId::new(thread.channel_id.parse::().unwrap_or(0)); let user_id = UserId::new(thread.user_id as u64); - let _ = close_thread(&thread_id, ¤t.closed_by, ¤t.category_id, ¤t.category_name, current.required_permissions.parse::().unwrap_or(0), pool).await; + let _ = close_thread( + &thread_id, + ¤t.closed_by, + ¤t.category_id, + ¤t.category_name, + current.required_permissions.parse::().unwrap_or(0), + pool, + ) + .await; let _ = delete_scheduled_closure(&thread_id, pool).await; if !current.silent { let _ = MessageBuilder::system_message(&ctx_clone, &config_clone) .content(&config_clone.bot.close_message) .to_user(user_id) - .send() + .send(false) .await; } let _ = channel_id.delete(&ctx_clone.http).await; @@ -68,13 +76,21 @@ pub async fn hydrate_scheduled_closures(ctx: &Context, config: &Config) { if sc.close_at <= Utc::now().timestamp() { let channel_id = ChannelId::new(thread.channel_id.parse::().unwrap_or(0)); let user_id = UserId::new(thread.user_id as u64); - let _ = close_thread(&thread.id, &sc.closed_by, &sc.category_id, &sc.category_name, sc.required_permissions.parse::().unwrap_or(0), pool).await; + let _ = close_thread( + &thread.id, + &sc.closed_by, + &sc.category_id, + &sc.category_name, + sc.required_permissions.parse::().unwrap_or(0), + pool, + ) + .await; let _ = delete_scheduled_closure(&thread.id, pool).await; if !sc.silent { let _ = MessageBuilder::system_message(ctx, config) .content(&config.bot.close_message) .to_user(user_id) - .send() + .send(false) .await; } let _ = channel_id.delete(&ctx.http).await; diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index 8cb1fbc..b9a49ce 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -85,13 +85,13 @@ async fn create_or_get_thread_for_user( let _ = MessageBuilder::system_message(ctx, config) .to_channel(target_channel_id) .content(open_thread_message) - .send() + .send(false) .await; let _ = MessageBuilder::system_message(ctx, config) .content(&config.bot.welcome_message) .to_user(user_id) - .send() + .send(false) .await; println!("Thread created successfully"); @@ -436,14 +436,14 @@ pub async fn handle_thread_modal_interaction( ) .await .to_channel(interaction.channel_id) - .send() + .send(false) .await; let _ = MessageBuilder::system_message(ctx, config) .translated_content("new_thread.dm_notification", None, Some(user_id), None) .await .to_user(user_id) - .send() + .send(false) .await; let mut params = HashMap::new(); diff --git a/rustmail/src/utils/command/category.rs b/rustmail/src/utils/command/category.rs index 2780432..95ad01d 100644 --- a/rustmail/src/utils/command/category.rs +++ b/rustmail/src/utils/command/category.rs @@ -1,37 +1,31 @@ -use serenity::all::{Channel, Context, PermissionOverwriteType, RoleId}; use serenity::all::CommandInteraction; +use serenity::all::{Channel, Context, PermissionOverwriteType, RoleId}; pub async fn get_category_id_from_command(ctx: &Context, command: &CommandInteraction) -> String { match command.channel_id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.guild() { - Some(guild_channel) => match guild_channel.parent_id { - Some(category_id) => category_id.to_string(), - None => String::new(), - }, + Ok(channel) => match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => category_id.to_string(), None => String::new(), - } - } + }, + None => String::new(), + }, _ => String::new(), } } pub async fn get_category_name_from_command(ctx: &Context, command: &CommandInteraction) -> String { match command.channel_id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.guild() { - Some(guild_channel) => match guild_channel.parent_id { - Some(category_id) => { - match category_id.name(&ctx.http).await { - Ok(category_name) => category_name.clone(), - _ => String::new(), - } - } - None => String::new(), + Ok(channel) => match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => match category_id.name(&ctx.http).await { + Ok(category_name) => category_name.clone(), + _ => String::new(), }, None => String::new(), - } - } + }, + None => String::new(), + }, _ => String::new(), } } @@ -63,4 +57,4 @@ pub async fn get_required_permissions_channel_from_command( } _ => 0u64, } -} \ No newline at end of file +} diff --git a/rustmail/src/utils/command/mod.rs b/rustmail/src/utils/command/mod.rs index 85a1c21..64f622f 100644 --- a/rustmail/src/utils/command/mod.rs +++ b/rustmail/src/utils/command/mod.rs @@ -1,4 +1,4 @@ +pub mod category; pub mod defer_response; pub mod extract_reply_content; pub mod wrap_command; -pub mod category; diff --git a/rustmail/src/utils/message/category.rs b/rustmail/src/utils/message/category.rs index 8b11e1a..151d29c 100644 --- a/rustmail/src/utils/message/category.rs +++ b/rustmail/src/utils/message/category.rs @@ -2,35 +2,29 @@ use serenity::all::{Channel, Context, Message, PermissionOverwriteType, RoleId}; pub async fn get_category_id_from_message(ctx: &Context, message: &Message) -> String { match message.channel_id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.guild() { - Some(guild_channel) => match guild_channel.parent_id { - Some(category_id) => category_id.to_string(), - None => String::new(), - }, + Ok(channel) => match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => category_id.to_string(), None => String::new(), - } - } + }, + None => String::new(), + }, _ => String::new(), } } pub async fn get_category_name_from_message(ctx: &Context, message: &Message) -> String { match message.channel_id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.guild() { - Some(guild_channel) => match guild_channel.parent_id { - Some(category_id) => { - match category_id.name(&ctx.http).await { - Ok(category_name) => category_name.clone(), - _ => String::new(), - } - } - None => String::new(), + Ok(channel) => match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => match category_id.name(&ctx.http).await { + Ok(category_name) => category_name.clone(), + _ => String::new(), }, None => String::new(), - } - } + }, + None => String::new(), + }, _ => String::new(), } } @@ -62,4 +56,4 @@ pub async fn get_required_permissions_channel_from_message( } _ => 0u64, } -} \ No newline at end of file +} diff --git a/rustmail/src/utils/message/message_builder.rs b/rustmail/src/utils/message/message_builder.rs index 7383875..2124ceb 100644 --- a/rustmail/src/utils/message/message_builder.rs +++ b/rustmail/src/utils/message/message_builder.rs @@ -1,5 +1,8 @@ use crate::config::Config; +use crate::db::get_thread_by_channel_id; use crate::db::operations::{insert_staff_message, insert_user_message_with_ids}; +use crate::db::threads::get_thread_by_user_id; +use crate::errors::{DatabaseError, ModmailError}; use crate::i18n::get_translated_message; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use serenity::all::{ @@ -394,7 +397,14 @@ impl<'a> MessageBuilder<'a> { } } - pub async fn send(self) -> Result { + pub async fn send(self, to_be_recorded: bool) -> Result { + let pool = match &self.config.db_pool { + Some(pool) => pool, + None => { + return Err(ModmailError::Database(DatabaseError::ConnectionFailed)); + } + }; + let target = self .target .clone() @@ -402,38 +412,97 @@ impl<'a> MessageBuilder<'a> { let message = self.build_create_message().await; - match target { + let thread_id: Option; + let mut dm_message_id: Option = None; + + let message = match target { MessageTarget::Channel(channel_id) => { + let db_thread_id = + match get_thread_by_channel_id(&channel_id.to_string(), &pool).await { + Some(thread) => Some(thread.id.clone()), + None => None, + }; + + thread_id = db_thread_id; + let message = match channel_id.send_message(&self.ctx.http, message).await { Ok(message) => message, - Err(err) => return Err(err), + Err(err) => return Err(ModmailError::from(err)), }; Ok(message) } MessageTarget::User(user_id) => { + let db_thread_id = match get_thread_by_user_id(user_id, &pool).await { + Some(thread) => Some(thread.id.clone()), + None => None, + }; + + thread_id = db_thread_id; + let dm_channel = user_id.create_dm_channel(&self.ctx.http).await?; let message = match dm_channel.send_message(&self.ctx.http, message).await { Ok(message) => message, - Err(err) => return Err(err), + Err(err) => return Err(ModmailError::from(err)), }; + dm_message_id = Some(message.id.to_string()); + Ok(message) } MessageTarget::Reply(original_message) => { + let db_thread_id = + match get_thread_by_channel_id(&original_message.channel_id.to_string(), &pool) + .await + { + Some(thread) => Some(thread.id.clone()), + None => None, + }; + + thread_id = db_thread_id; + let message = match original_message .channel_id .send_message(&self.ctx.http, message) .await { Ok(message) => message, - Err(err) => return Err(err), + Err(err) => return Err(ModmailError::from(err)), }; Ok(message) } + }; + + let message = match message { + Ok(msg) => msg, + Err(e) => return Err(e), + }; + + if to_be_recorded { + match insert_staff_message( + &self.ctx, + &message, + dm_message_id, + &thread_id.unwrap_or_default(), + self.bot_user_id, + false, + &pool, + self.config, + -1, + ) + .await + { + Ok(_) => (), + Err(e) => { + println!("Failed to record sent message: {}", e); + return Err(ModmailError::from(e)); + } + }; } + + Ok(message) } pub async fn build_create_message(&self) -> CreateMessage { @@ -526,7 +595,7 @@ impl<'a> MessageBuilder<'a> { } pub async fn send_and_forget(self) { - if let Err(e) = self.send().await { + if let Err(e) = self.send(false).await { eprintln!("Failed to send message: {}", e); } } @@ -566,11 +635,11 @@ impl<'a> MessageBuilder<'a> { config: &'a Config, channel_id: ChannelId, content: String, - ) -> Result { + ) -> Result { Self::system_message(ctx, config) .content(content) .to_channel(channel_id) - .send() + .send(false) .await } @@ -579,11 +648,11 @@ impl<'a> MessageBuilder<'a> { config: &'a Config, user_id: UserId, content: String, - ) -> Result { + ) -> Result { Self::system_message(ctx, config) .content(content) .to_user(user_id) - .send() + .send(false) .await } @@ -592,11 +661,11 @@ impl<'a> MessageBuilder<'a> { config: &'a Config, message: Message, content: String, - ) -> Result { + ) -> Result { Self::system_message(ctx, config) .content(content) .reply_to(message) - .send() + .send(false) .await } } @@ -713,7 +782,7 @@ impl<'a> StaffReply<'a> { .color(hex_string_to_int(&self.config.thread.staff_message_color) as u32) .to_user(dm_user); - match dm_builder.send().await { + match dm_builder.send(false).await { Ok(m) => Some(m), Err(e) => { eprintln!("Failed to send DM to user: {}", e); @@ -728,7 +797,7 @@ impl<'a> StaffReply<'a> { pub async fn send_msg_and_record( self, pool: &SqlitePool, - ) -> Result<(Message, Option), serenity::Error> { + ) -> Result<(Message, Option), ModmailError> { let thread_channel = self .thread_channel .ok_or_else(|| serenity::Error::Other("No thread channel for StaffReply"))?; @@ -757,12 +826,13 @@ impl<'a> StaffReply<'a> { .add_attachments(self.attachments.clone()) .to_channel(thread_channel); - let thread_msg = thread_builder.send().await?; + let thread_msg = thread_builder.send(false).await?; let dm_msg_opt: Option = self.build_and_send_message(top_role_name).await; let dm_id_opt = dm_msg_opt.as_ref().map(|m| m.id.to_string()); if let Err(e) = insert_staff_message( + &self.ctx, &thread_msg, dm_id_opt, &self.thread_id, @@ -770,7 +840,7 @@ impl<'a> StaffReply<'a> { self.is_anonymous, pool, self.config, - self.message_number, + self.message_number as i64, ) .await { @@ -784,7 +854,7 @@ impl<'a> StaffReply<'a> { self, command: &CommandInteraction, pool: &SqlitePool, - ) -> Result<(Message, Option), serenity::Error> { + ) -> Result<(Message, Option), ModmailError> { let thread_channel = self .thread_channel .ok_or_else(|| serenity::Error::Other("No thread channel for StaffReply"))?; @@ -822,7 +892,7 @@ impl<'a> StaffReply<'a> { Ok(m) => m, Err(e) => { println!("Failed to send follow-up message: {}", e); - return Err(e); + return Err(ModmailError::from(e)); } }; @@ -830,6 +900,7 @@ impl<'a> StaffReply<'a> { let dm_id_opt = dm_msg_opt.as_ref().map(|m| m.id.to_string()); if let Err(e) = insert_staff_message( + &self.ctx, &thread_msg, dm_id_opt, &self.thread_id, @@ -837,7 +908,7 @@ impl<'a> StaffReply<'a> { self.is_anonymous, pool, self.config, - self.message_number, + self.message_number as i64, ) .await { @@ -894,7 +965,7 @@ impl<'a> UserIncoming<'a> { self } - pub async fn send_and_record(self, pool: &SqlitePool) -> Result { + pub async fn send_and_record(self, pool: &SqlitePool) -> Result { let thread_channel = self .thread_channel .ok_or_else(|| serenity::Error::Other("No thread channel for UserIncoming"))?; @@ -907,7 +978,7 @@ impl<'a> UserIncoming<'a> { .content(self.content) .add_attachments(self.attachments) .to_channel(thread_channel); - let sent = builder.send().await?; + let sent = builder.send(false).await?; if let Err(e) = insert_user_message_with_ids( self.dm_msg, &sent, diff --git a/rustmail/src/utils/message/mod.rs b/rustmail/src/utils/message/mod.rs index f4e7124..2cd920b 100644 --- a/rustmail/src/utils/message/mod.rs +++ b/rustmail/src/utils/message/mod.rs @@ -1,8 +1,8 @@ pub mod message_builder; pub mod reply_intent; -pub mod ui_components; pub mod category; +pub mod ui_components; pub mod ui { use super::{ui_components::ButtonsBuilder, ui_components::ModalBuilder}; diff --git a/rustmail/src/utils/thread/category.rs b/rustmail/src/utils/thread/category.rs index f03d94d..eb7c044 100644 --- a/rustmail/src/utils/thread/category.rs +++ b/rustmail/src/utils/thread/category.rs @@ -2,35 +2,29 @@ use serenity::all::{Channel, Context, GuildChannel, PermissionOverwriteType, Rol pub async fn get_category_id_from_guild_channel(ctx: &Context, channel: &GuildChannel) -> String { match channel.id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.guild() { - Some(guild_channel) => match guild_channel.parent_id { - Some(category_id) => category_id.to_string(), - None => String::new(), - }, + Ok(channel) => match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => category_id.to_string(), None => String::new(), - } - } + }, + None => String::new(), + }, _ => String::new(), } } pub async fn get_category_name_from_guild_channel(ctx: &Context, channel: &GuildChannel) -> String { match channel.id.to_channel(&ctx.http).await { - Ok(channel) => { - match channel.guild() { - Some(guild_channel) => match guild_channel.parent_id { - Some(category_id) => { - match category_id.name(&ctx.http).await { - Ok(category_name) => category_name.clone(), - _ => String::new(), - } - } - None => String::new(), + Ok(channel) => match channel.guild() { + Some(guild_channel) => match guild_channel.parent_id { + Some(category_id) => match category_id.name(&ctx.http).await { + Ok(category_name) => category_name.clone(), + _ => String::new(), }, None => String::new(), - } - } + }, + None => String::new(), + }, _ => String::new(), } } @@ -62,4 +56,4 @@ pub async fn get_required_permissions_channel_from_guild_channel( } _ => 0u64, } -} \ No newline at end of file +} diff --git a/rustmail/src/utils/thread/mod.rs b/rustmail/src/utils/thread/mod.rs index 7523d79..c237e83 100644 --- a/rustmail/src/utils/thread/mod.rs +++ b/rustmail/src/utils/thread/mod.rs @@ -1,4 +1,4 @@ +pub mod category; pub mod fetch_thread; pub mod get_thread_lock; pub mod send_to_thread; -pub mod category; diff --git a/rustmail/src/utils/thread/send_to_thread.rs b/rustmail/src/utils/thread/send_to_thread.rs index df3cf29..713ef98 100644 --- a/rustmail/src/utils/thread/send_to_thread.rs +++ b/rustmail/src/utils/thread/send_to_thread.rs @@ -2,6 +2,7 @@ use crate::config::Config; use crate::db::operations::{ get_staff_alerts_for_user, get_thread_id_by_user_id, is_user_left, mark_alert_as_used, }; +use crate::errors::{DatabaseError, ModmailError, ModmailResult, ThreadError}; use crate::i18n::get_translated_message; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ChannelId, Context, CreateAttachment, GuildId, Message, UserId}; @@ -69,12 +70,12 @@ pub async fn send_to_thread( msg: &Message, config: &Config, is_anonymous: bool, -) -> serenity::Result { +) -> ModmailResult { let pool = match &config.db_pool { Some(pool) => pool, None => { eprintln!("Database pool is not set in config."); - return Err(serenity::Error::Other("Database pool not available")); + return Err(ModmailError::Database(DatabaseError::ConnectionFailed)); } }; @@ -88,7 +89,7 @@ pub async fn send_to_thread( .translated_content("user.left_server", Some(¶ms), Some(msg.author.id), None) .await .to_channel(channel_id) - .send() + .send(false) .await; } @@ -106,7 +107,7 @@ pub async fn send_to_thread( ) .await .to_channel(channel_id) - .send() + .send(false) .await; } @@ -123,7 +124,7 @@ pub async fn send_to_thread( Some(thread_id) => thread_id, None => { eprintln!("Failed to get thread ID"); - return Err(serenity::Error::Other("Failed to get thread ID")); + return Err(ModmailError::Thread(ThreadError::ThreadNotFound)); } }; @@ -170,7 +171,7 @@ pub async fn send_to_thread( .content(full_content) .mention(mentions) .to_channel(channel_id) - .send() + .send(false) .await; for staff_id in &alerts { diff --git a/rustmail_panel/src/components/navbar.rs b/rustmail_panel/src/components/navbar.rs index 27a6f42..26c2fcd 100644 --- a/rustmail_panel/src/components/navbar.rs +++ b/rustmail_panel/src/components/navbar.rs @@ -1,6 +1,6 @@ use crate::components::language_switcher::LanguageSwitcher; use crate::pages::panel::PanelRoute; -use yew::{classes, function_component, html, use_state, Callback, Html, Properties}; +use yew::{Callback, Html, Properties, classes, function_component, html, use_state}; use yew_router::hooks::{use_location, use_navigator}; #[derive(Properties, PartialEq, Clone)] From 2f9deccb136b551a780cc32d2820418a3fe7b644 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 21 Oct 2025 17:24:17 +0200 Subject: [PATCH 037/100] refactor(interation): add pagination arguments to textuals commands --- rustmail/src/bot.rs | 17 ++++++++++---- .../add_reminder/text_command/add_reminder.rs | 6 +++-- .../add_staff/text_command/add_staff.rs | 4 +++- .../src/commands/alert/text_command/alert.rs | 4 +++- .../anonreply/text_command/anonreply.rs | 6 +++-- .../src/commands/close/text_command/close.rs | 4 +++- .../commands/delete/text_command/delete.rs | 4 +++- .../src/commands/edit/text_command/edit.rs | 6 +++-- .../force_close/text_command/force_close.rs | 4 +++- .../src/commands/help/text_command/help.rs | 2 ++ rustmail/src/commands/id/text_command/id.rs | 4 +++- rustmail/src/commands/mod.rs | 1 + .../move_thread/text_command/move_thread.rs | 4 +++- .../new_thread/text_command/new_thread.rs | 4 +++- .../commands/recover/text_command/recover.rs | 4 +++- .../text_command/remove_reminder.rs | 4 +++- .../remove_staff/text_command/remove_staff.rs | 4 +++- .../src/commands/reply/text_command/reply.rs | 6 +++-- rustmail/src/main.rs | 1 + rustmail/src/types/logs.rs | 23 +++++++++++++++++++ rustmail/src/types/mod.rs | 1 + 21 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 rustmail/src/types/logs.rs create mode 100644 rustmail/src/types/mod.rs diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 4e847f3..407c946 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -1,4 +1,3 @@ -use crate::commands::CommandRegistry; use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; use crate::commands::alert::slash_command::alert::AlertCommand; @@ -14,9 +13,10 @@ use crate::commands::recover::slash_command::recover::RecoverCommand; use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; use crate::commands::reply::slash_command::reply::ReplyCommand; +use crate::commands::CommandRegistry; use crate::config::load_config; -use crate::errors::ModmailError; use crate::errors::types::ConfigError; +use crate::errors::ModmailError; use crate::handlers::guild_handler::GuildHandler; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::handlers::guild_members_handler::GuildMembersHandler; @@ -26,11 +26,13 @@ use crate::handlers::guild_moderation_handler::GuildModerationHandler; use crate::handlers::ready_handler::ReadyHandler; use crate::handlers::typing_proxy_handler::TypingProxyHandler; use crate::panel_commands::user::is_member::is_member; -use crate::{BotCommand, BotState, BotStatus, db}; +use crate::types::logs::PaginationContext; +use crate::{db, BotCommand, BotState, BotStatus}; use base64::Engine; use rand::RngCore; use serenity::all::{ClientBuilder, GatewayIntents}; use serenity::cache::Settings as CacheSettings; +use std::collections::HashMap; use std::process; use std::sync::Arc; use std::time::Duration; @@ -120,6 +122,8 @@ pub async fn run_bot( state_lock.config.clone().expect("Config not set") }; + let pagination = Arc::new(Mutex::new(HashMap::::new())); + config.db_pool = Some(pool.clone()); let intents = GatewayIntents::GUILDS @@ -164,7 +168,11 @@ pub async fn run_bot( registry.clone(), Arc::new(shutdown_rx.clone()), )) - .event_handler(GuildMessagesHandler::new(&config, shutdown_rx.clone())) + .event_handler(GuildMessagesHandler::new( + &config, + shutdown_rx.clone(), + pagination.clone(), + )) .event_handler(TypingProxyHandler::new(&config)) .event_handler(GuildMembersHandler::new(&config)) .event_handler(GuildMessageReactionsHandler::new(&config)) @@ -173,6 +181,7 @@ pub async fn run_bot( &config, registry.clone(), shutdown_rx, + pagination, )) .event_handler(GuildHandler::new(&config)) .await diff --git a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs index 9d6278b..af12758 100644 --- a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs @@ -2,11 +2,12 @@ use crate::commands::add_reminder::common::{ send_register_confirmation_from_message, spawn_reminder, }; use crate::config::Config; -use crate::db::reminders::{Reminder, insert_reminder}; +use crate::db::reminders::{insert_reminder, Reminder}; use crate::db::threads::get_thread_by_user_id; use crate::errors::{ - CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, common, + common, CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, }; +use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use chrono::{Local, NaiveTime}; use regex::Regex; @@ -19,6 +20,7 @@ pub async fn add_reminder( msg: &Message, config: &Config, shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/add_staff/text_command/add_staff.rs b/rustmail/src/commands/add_staff/text_command/add_staff.rs index 2d49b51..18ef50d 100644 --- a/rustmail/src/commands/add_staff/text_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/text_command/add_staff.rs @@ -4,7 +4,8 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; +use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; use std::collections::HashMap; @@ -16,6 +17,7 @@ pub async fn add_staff( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/alert/text_command/alert.rs b/rustmail/src/commands/alert/text_command/alert.rs index e21ffd1..26c5c5c 100644 --- a/rustmail/src/commands/alert/text_command/alert.rs +++ b/rustmail/src/commands/alert/text_command/alert.rs @@ -3,7 +3,8 @@ use crate::commands::alert::common::{ handle_set_alert_from_msg, }; use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; +use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; use tokio::sync::watch::Receiver; @@ -13,6 +14,7 @@ pub async fn alert( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/anonreply/text_command/anonreply.rs b/rustmail/src/commands/anonreply/text_command/anonreply.rs index e8f430b..8bbccaa 100644 --- a/rustmail/src/commands/anonreply/text_command/anonreply.rs +++ b/rustmail/src/commands/anonreply/text_command/anonreply.rs @@ -1,9 +1,10 @@ use crate::config::Config; use crate::db::operations::allocate_next_message_number; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; +use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; @@ -15,6 +16,7 @@ pub async fn anonreply( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let db_pool = config .db_pool diff --git a/rustmail/src/commands/close/text_command/close.rs b/rustmail/src/commands/close/text_command/close.rs index d3aa713..08e71cc 100644 --- a/rustmail/src/commands/close/text_command/close.rs +++ b/rustmail/src/commands/close/text_command/close.rs @@ -3,7 +3,8 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::types::logs::PaginationStore; use crate::utils::message::category::{ get_category_id_from_message, get_category_name_from_message, get_required_permissions_channel_from_message, @@ -23,6 +24,7 @@ pub async fn close( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let db_pool = config .db_pool diff --git a/rustmail/src/commands/delete/text_command/delete.rs b/rustmail/src/commands/delete/text_command/delete.rs index b7ba5db..8f7f2ff 100644 --- a/rustmail/src/commands/delete/text_command/delete.rs +++ b/rustmail/src/commands/delete/text_command/delete.rs @@ -3,7 +3,8 @@ use crate::commands::delete::common::{ get_thread_info, send_delete_message, update_message_numbers, }; use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; +use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; use tokio::sync::watch::Receiver; @@ -13,6 +14,7 @@ pub async fn delete( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/edit/text_command/edit.rs b/rustmail/src/commands/edit/text_command/edit.rs index 57c79c2..0df6e77 100644 --- a/rustmail/src/commands/edit/text_command/edit.rs +++ b/rustmail/src/commands/edit/text_command/edit.rs @@ -3,13 +3,14 @@ use crate::commands::edit::message_ops::{ cleanup_command_message, edit_messages, format_new_message, get_message_ids, }; use crate::commands::edit::validation::{ - EditCommandInput, parse_edit_command, validate_edit_permissions, + parse_edit_command, validate_edit_permissions, EditCommandInput, }; use crate::config::Config; use crate::db::get_thread_message_by_inbox_message_id; use crate::db::update_message_content; use crate::errors::common::message_not_found; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; +use crate::types::logs::PaginationStore; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; @@ -22,6 +23,7 @@ pub async fn edit( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/force_close/text_command/force_close.rs b/rustmail/src/commands/force_close/text_command/force_close.rs index cc841f0..c664cb6 100644 --- a/rustmail/src/commands/force_close/text_command/force_close.rs +++ b/rustmail/src/commands/force_close/text_command/force_close.rs @@ -3,7 +3,8 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; +use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; use tokio::sync::watch::Receiver; @@ -13,6 +14,7 @@ pub async fn force_close( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let db_pool = config .db_pool diff --git a/rustmail/src/commands/help/text_command/help.rs b/rustmail/src/commands/help/text_command/help.rs index fad8403..4e36e0e 100644 --- a/rustmail/src/commands/help/text_command/help.rs +++ b/rustmail/src/commands/help/text_command/help.rs @@ -1,5 +1,6 @@ use crate::config::Config; use crate::errors::ModmailResult; +use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::sync::Arc; @@ -10,6 +11,7 @@ pub async fn help( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let help_message = "# Available commands:\n\n\ **!add_staff ** - Add a staff to an hidden ticket\n\ diff --git a/rustmail/src/commands/id/text_command/id.rs b/rustmail/src/commands/id/text_command/id.rs index f1ea5d4..b437fab 100644 --- a/rustmail/src/commands/id/text_command/id.rs +++ b/rustmail/src/commands/id/text_command/id.rs @@ -1,9 +1,10 @@ use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::common::{database_connection_failed, thread_not_found}; +use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::{ModmailError, ModmailResult}; +use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::sync::Arc; @@ -14,6 +15,7 @@ pub async fn id( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let db_pool = config .db_pool diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index ab552ba..f4dc5e1 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -16,6 +16,7 @@ pub mod edit; pub mod force_close; pub mod help; pub mod id; +pub mod logs; pub mod move_thread; pub mod new_thread; pub mod recover; diff --git a/rustmail/src/commands/move_thread/text_command/move_thread.rs b/rustmail/src/commands/move_thread/text_command/move_thread.rs index bc692f3..ef28f27 100644 --- a/rustmail/src/commands/move_thread/text_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/text_command/move_thread.rs @@ -5,7 +5,8 @@ use crate::commands::move_thread::common::{ use crate::config::Config; use crate::errors::CommandError::NotInThread; use crate::errors::ThreadError::CategoryNotFound; -use crate::errors::{CommandError, DiscordError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DiscordError, ModmailError, ModmailResult}; +use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; use tokio::sync::watch::Receiver; @@ -15,6 +16,7 @@ pub async fn move_thread( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/new_thread/text_command/new_thread.rs b/rustmail/src/commands/new_thread/text_command/new_thread.rs index c5e3ab0..9aee4aa 100644 --- a/rustmail/src/commands/new_thread/text_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/text_command/new_thread.rs @@ -4,7 +4,8 @@ use crate::commands::new_thread::common::{ }; use crate::config::Config; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; -use crate::errors::{DiscordError, ModmailError, ModmailResult, common}; +use crate::errors::{common, DiscordError, ModmailError, ModmailResult}; +use crate::types::logs::PaginationStore; use serenity::all::{ChannelId, Context, GuildId, Message}; use std::collections::HashMap; use std::sync::Arc; @@ -15,6 +16,7 @@ pub async fn new_thread( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/recover/text_command/recover.rs b/rustmail/src/commands/recover/text_command/recover.rs index d174b8a..0bbef6d 100644 --- a/rustmail/src/commands/recover/text_command/recover.rs +++ b/rustmail/src/commands/recover/text_command/recover.rs @@ -1,7 +1,8 @@ use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; +use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::collections::HashMap; @@ -13,6 +14,7 @@ pub async fn recover( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let _ = config .db_pool diff --git a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs index f8be6cf..05bed5d 100644 --- a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -1,6 +1,7 @@ use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; +use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; @@ -13,6 +14,7 @@ pub async fn remove_reminder( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs index 128cbd2..84fc629 100644 --- a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs @@ -3,7 +3,8 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; +use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; use std::collections::HashMap; @@ -15,6 +16,7 @@ pub async fn remove_staff( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index 61c473e..544b621 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -1,10 +1,11 @@ use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; @@ -16,6 +17,7 @@ pub async fn reply( msg: &Message, config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> ModmailResult<()> { let db_pool = config .db_pool diff --git a/rustmail/src/main.rs b/rustmail/src/main.rs index 5d5f382..d0f5b63 100644 --- a/rustmail/src/main.rs +++ b/rustmail/src/main.rs @@ -23,6 +23,7 @@ mod handlers; mod i18n; mod modules; mod panel_commands; +mod types; mod utils; pub struct Database; diff --git a/rustmail/src/types/logs.rs b/rustmail/src/types/logs.rs new file mode 100644 index 0000000..7196fa5 --- /dev/null +++ b/rustmail/src/types/logs.rs @@ -0,0 +1,23 @@ +use serenity::all::{ChannelId, MessageId}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Clone, Debug)] +pub struct TicketLog { + pub id: i64, + pub ticket_id: String, + pub user_id: String, + pub created_at: String, +} + +#[derive(Clone)] +pub struct PaginationContext { + pub user_id: String, + pub logs: Vec, + pub current_page: usize, + pub message_id: MessageId, + pub channel_id: ChannelId, +} + +pub type PaginationStore = Arc>>; diff --git a/rustmail/src/types/mod.rs b/rustmail/src/types/mod.rs new file mode 100644 index 0000000..af2c2c3 --- /dev/null +++ b/rustmail/src/types/mod.rs @@ -0,0 +1 @@ +pub mod logs; From 6df2ed65e436e4903aa8432300d4ef6b2c8c4fd0 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 21 Oct 2025 17:25:31 +0200 Subject: [PATCH 038/100] fix(poll): reformat code --- rustmail/src/features/mod.rs | 7 ++++--- rustmail/src/features/poll.rs | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rustmail/src/features/mod.rs b/rustmail/src/features/mod.rs index 774cc532..5654ade 100644 --- a/rustmail/src/features/mod.rs +++ b/rustmail/src/features/mod.rs @@ -27,13 +27,14 @@ pub fn registry<'a>() -> Vec>> { vec![Arc::new(PollFeature)] } -pub fn make_buttons(pairs: &[(&str, &str, ButtonStyle)]) -> Vec { +pub fn make_buttons(pairs: &[(&str, &str, ButtonStyle, bool)]) -> Vec { let mut row = CreateActionRow::Buttons(vec![]); let mut buttons: Vec = Vec::new(); - for (label, custom_id, style) in pairs { + for (label, custom_id, style, disable) in pairs { let b = CreateButton::new(custom_id.to_string()) .label(label.to_string()) - .style(*style); + .style(*style) + .disabled(*disable); buttons.push(b); } diff --git a/rustmail/src/features/poll.rs b/rustmail/src/features/poll.rs index 7b14872..490121a 100644 --- a/rustmail/src/features/poll.rs +++ b/rustmail/src/features/poll.rs @@ -1,4 +1,4 @@ -use super::{Feature, make_buttons}; +use super::{make_buttons, Feature}; use crate::config::Config; use crate::utils::message::message_builder::MessageBuilder; use async_trait::async_trait; @@ -23,8 +23,9 @@ impl<'a> Feature<'a> for PollFeature { "Créer un sondage", "feature:poll:create", ButtonStyle::Success, + false, ), - ("Test", "feature:poll:delete", ButtonStyle::Danger), + ("Test", "feature:poll:delete", ButtonStyle::Danger, false), ]); MessageBuilder::system_message(ctx, config) From 2e9e8e24de4db9e9dacde0e8b1eda45a870dc458 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 21 Oct 2025 17:26:48 +0200 Subject: [PATCH 039/100] feat(db): add functions to fetch all user's logs --- rustmail/src/db/operations/logs.rs | 35 ++++++++++++++++++++++++++++++ rustmail/src/db/operations/mod.rs | 1 + 2 files changed, 36 insertions(+) create mode 100644 rustmail/src/db/operations/logs.rs diff --git a/rustmail/src/db/operations/logs.rs b/rustmail/src/db/operations/logs.rs new file mode 100644 index 0000000..412b4c3 --- /dev/null +++ b/rustmail/src/db/operations/logs.rs @@ -0,0 +1,35 @@ +use crate::errors::{DatabaseError, ModmailError, ModmailResult}; +use crate::types::logs::TicketLog; +use sqlx::SqlitePool; + +pub async fn get_logs_from_user_id( + user_id: &str, + pool: &SqlitePool, +) -> ModmailResult> { + let logs = sqlx::query_as!( + TicketLog, + r#" + SELECT + (COUNT(*) OVER ()) + - ROW_NUMBER() OVER (ORDER BY created_at DESC) + 1 AS id, + id AS ticket_id, + user_id AS "user_id: String", + created_at AS "created_at: String" + FROM threads + WHERE user_id = ? AND status = 0 + ORDER BY created_at DESC + "#, + user_id + ) + .fetch_all(pool) + .await + .map_err(|e| { + eprintln!( + "Database error getting logs for user ID {}: {:?}", + user_id, e + ); + ModmailError::Database(DatabaseError::QueryFailed(e.to_string())) + })?; + + Ok(logs) +} diff --git a/rustmail/src/db/operations/mod.rs b/rustmail/src/db/operations/mod.rs index 3d88641..ee64212 100644 --- a/rustmail/src/db/operations/mod.rs +++ b/rustmail/src/db/operations/mod.rs @@ -1,5 +1,6 @@ pub mod features; pub mod init; +pub mod logs; pub mod messages; pub mod reminders; pub mod scheduled; From 10d96b06e28779efb2c11c852b9173dfcf27d1f1 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 21 Oct 2025 17:28:34 +0200 Subject: [PATCH 040/100] refactor(button_builder): add paramater to set a button disable --- rustmail/src/handlers/guild_members_handler.rs | 2 ++ rustmail/src/handlers/guild_messages_handler.rs | 11 +++++++++-- rustmail/src/handlers/guild_moderation_handler.rs | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/rustmail/src/handlers/guild_members_handler.rs b/rustmail/src/handlers/guild_members_handler.rs index 2630c44..3db3941 100644 --- a/rustmail/src/handlers/guild_members_handler.rs +++ b/rustmail/src/handlers/guild_members_handler.rs @@ -74,6 +74,7 @@ impl EventHandler for GuildMembersHandler { .as_ref(), "ticket:keep", ButtonStyle::Success, + false, ), ( get_translated_message(&self.config, "thread.ask_to_close", None, None, None, None) @@ -81,6 +82,7 @@ impl EventHandler for GuildMembersHandler { .as_ref(), "ticket:delete", ButtonStyle::Danger, + false, ), ]); diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 5118416..0ad57cf 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -9,6 +9,7 @@ use crate::commands::edit::text_command::edit::edit; use crate::commands::force_close::text_command::force_close::force_close; use crate::commands::help::text_command::help::help; use crate::commands::id::text_command::id::id; +use crate::commands::logs::text_command::logs::logs; use crate::commands::move_thread::text_command::move_thread::move_thread; use crate::commands::new_thread::text_command::new_thread::new_thread; use crate::commands::recover::text_command::recover::recover; @@ -23,8 +24,9 @@ use crate::db::operations::{ }; use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::get_thread_lock::get_thread_lock; use crate::utils::thread::send_to_thread::send_to_thread; @@ -48,6 +50,7 @@ type StaticCommandFunc = dyn Fn( Message, Config, Receiver, + PaginationStore, ) -> Pin> + Send>> + Send + Sync @@ -57,13 +60,15 @@ pub struct GuildMessagesHandler { pub config: Config, pub commands: HashMap, pub shutdown: Receiver, + pub pagination: PaginationStore, } impl GuildMessagesHandler { - pub fn new(config: &Config, shutdown: Receiver) -> Self { + pub fn new(config: &Config, shutdown: Receiver, pagination: PaginationStore) -> Self { let mut h = Self { config: config.clone(), commands: HashMap::new(), + pagination, shutdown, }; wrap_command!(h.commands, ["reply", "r"], reply); @@ -82,6 +87,7 @@ impl GuildMessagesHandler { wrap_command!(h.commands, "help", help); wrap_command!(h.commands, ["add_reminder", "add_rap"], add_reminder); wrap_command!(h.commands, ["remove_reminder", "rr"], remove_reminder); + wrap_command!(h.commands, "logs", logs); h } } @@ -190,6 +196,7 @@ impl EventHandler for GuildMessagesHandler { msg.clone(), self.config.clone(), self.shutdown.clone(), + self.pagination.clone(), ) .await { diff --git a/rustmail/src/handlers/guild_moderation_handler.rs b/rustmail/src/handlers/guild_moderation_handler.rs index 4eef1c5..7c245ba 100644 --- a/rustmail/src/handlers/guild_moderation_handler.rs +++ b/rustmail/src/handlers/guild_moderation_handler.rs @@ -237,8 +237,18 @@ async fn manage_creating_ticket_via_opening_thread( let yes = get_translated_message(&config, "general.yes", None, None, None, None).await; let no = get_translated_message(&config, "general.no", None, None, None, None).await; let res_button = make_buttons(&[ - (yes.as_ref(), "ticket:wants_to_create", ButtonStyle::Success), - (no.as_ref(), "ticket:dont_create", ButtonStyle::Danger), + ( + yes.as_ref(), + "ticket:wants_to_create", + ButtonStyle::Success, + false, + ), + ( + no.as_ref(), + "ticket:dont_create", + ButtonStyle::Danger, + false, + ), ]); let _ = MessageBuilder::system_message(&ctx, &config) From dfd6bb34a2e2ccfe9fe3bed4e250e1fbe8c77d70 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 21 Oct 2025 17:28:57 +0200 Subject: [PATCH 041/100] feat(commands): add logs textual command --- rustmail/src/commands/logs/common.rs | 54 ++++++ rustmail/src/commands/logs/mod.rs | 3 + .../src/commands/logs/slash_command/logs.rs | 0 .../src/commands/logs/slash_command/mod.rs | 1 + .../src/commands/logs/text_command/logs.rs | 155 ++++++++++++++++++ .../src/commands/logs/text_command/mod.rs | 1 + .../src/handlers/guild_interaction_handler.rs | 21 ++- rustmail/src/i18n/language/en.rs | 8 + rustmail/src/i18n/language/fr.rs | 8 + rustmail/src/modules/commands.rs | 108 ++++++++++++ rustmail/src/modules/mod.rs | 1 + rustmail/src/modules/threads.rs | 23 +-- rustmail/src/utils/command/wrap_command.rs | 12 +- rustmail/src/utils/message/message_builder.rs | 4 + 14 files changed, 374 insertions(+), 25 deletions(-) create mode 100644 rustmail/src/commands/logs/common.rs create mode 100644 rustmail/src/commands/logs/mod.rs create mode 100644 rustmail/src/commands/logs/slash_command/logs.rs create mode 100644 rustmail/src/commands/logs/slash_command/mod.rs create mode 100644 rustmail/src/commands/logs/text_command/logs.rs create mode 100644 rustmail/src/commands/logs/text_command/mod.rs create mode 100644 rustmail/src/modules/commands.rs diff --git a/rustmail/src/commands/logs/common.rs b/rustmail/src/commands/logs/common.rs new file mode 100644 index 0000000..5957361 --- /dev/null +++ b/rustmail/src/commands/logs/common.rs @@ -0,0 +1,54 @@ +use crate::config::Config; +use crate::types::logs::TicketLog; +use serenity::all::Message; + +pub fn extract_user_id(msg: &Message, config: &Config) -> String { + let content = msg.content.trim(); + let prefix = &config.command.prefix; + let command_name = "logs"; + + if content.starts_with(&format!("{}{}", prefix, command_name)) { + println!("Logging in..."); + let start = prefix.len() + command_name.len(); + content[start..].trim().to_string() + } else { + String::new() + } +} + +pub fn render_logs_page(logs: &[TicketLog], page: usize, per_page: usize) -> String { + let total_pages = (logs.len() + per_page - 1) / per_page; + let start = page * per_page; + let end = usize::min(start + per_page, logs.len()); + + let mut desc = String::new(); + + if start >= logs.len() { + return "_Aucun log trouvé pour cet utilisateur._".into(); + } + + for (_, log) in logs[start..end].iter().enumerate() { + use std::fmt::Write; + let _ = writeln!( + desc, + "**#{}** | [`Ticket {}`]({}) | Fermé le {} {}", + log.id, + log.ticket_id, + format!("http://localhost:3002/panel/tickets/{}", log.ticket_id), + log.created_at, + "\n".to_string(), + ); + } + + if desc.is_empty() { + desc = "_Aucun log trouvé pour cet utilisateur._".into(); + } + + format!( + "{}\n_Page {}/{} ({} logs totaux)_", + desc, + page + 1, + total_pages.max(1), + logs.len() + ) +} diff --git a/rustmail/src/commands/logs/mod.rs b/rustmail/src/commands/logs/mod.rs new file mode 100644 index 0000000..de6da7a --- /dev/null +++ b/rustmail/src/commands/logs/mod.rs @@ -0,0 +1,3 @@ +pub mod common; +pub mod slash_command; +pub mod text_command; diff --git a/rustmail/src/commands/logs/slash_command/logs.rs b/rustmail/src/commands/logs/slash_command/logs.rs new file mode 100644 index 0000000..e69de29 diff --git a/rustmail/src/commands/logs/slash_command/mod.rs b/rustmail/src/commands/logs/slash_command/mod.rs new file mode 100644 index 0000000..af2c2c3 --- /dev/null +++ b/rustmail/src/commands/logs/slash_command/mod.rs @@ -0,0 +1 @@ +pub mod logs; diff --git a/rustmail/src/commands/logs/text_command/logs.rs b/rustmail/src/commands/logs/text_command/logs.rs new file mode 100644 index 0000000..15f4c0f --- /dev/null +++ b/rustmail/src/commands/logs/text_command/logs.rs @@ -0,0 +1,155 @@ +use crate::commands::logs::common::extract_user_id; +use crate::config::Config; +use crate::db::get_thread_by_channel_id; +use crate::db::logs::get_logs_from_user_id; +use crate::errors::{DatabaseError, ModmailError, ModmailResult, ThreadError}; +use crate::features::make_buttons; +use crate::i18n::get_translated_message; +use crate::modules::commands::LOGS_PAGE_SIZE; +use crate::types::logs::{PaginationContext, PaginationStore, TicketLog}; +use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{ButtonStyle, Context, Message}; +use sqlx::SqlitePool; +use std::sync::Arc; +use tokio::sync::watch::Receiver; +use uuid::Uuid; + +fn render_logs_page(logs: &[TicketLog], page: usize, per_page: usize) -> String { + let total_pages = (logs.len() + per_page - 1) / per_page; + let start = page * per_page; + let end = usize::min(start + per_page, logs.len()); + + let mut desc = String::new(); + + for (_, log) in logs[start..end].iter().enumerate() { + use std::fmt::Write; + let _ = writeln!( + desc, + "**#{}** | [`Ticket {}`]({}) | Fermé le {} {}", + log.id, + log.ticket_id, + format!("http://localhost:3002/panel/tickets/{}", log.ticket_id), + log.created_at, + "\n".to_string(), + ); + } + + if desc.is_empty() { + desc = "_Aucun log trouvé pour cet utilisateur._".into(); + } + + format!( + "{}\n_Page {}/{} ({} logs totaux)_", + desc, + page + 1, + total_pages.max(1), + logs.len() + ) +} + +pub async fn handle_logs_in_thread( + ctx: &Context, + msg: &Message, + config: &Config, + pool: &SqlitePool, + pagination: PaginationStore, +) -> ModmailResult<()> { + let thread = match get_thread_by_channel_id(&msg.channel_id.to_string(), &pool).await { + Some(thread) => thread, + None => return Err(ModmailError::Thread(ThreadError::ThreadNotFound)), + }; + + handle_logs_from_user_id( + &ctx, + &msg, + &config, + &pool, + &thread.user_id.to_string(), + pagination, + ) + .await +} + +pub async fn handle_logs_from_user_id( + ctx: &Context, + msg: &Message, + config: &Config, + pool: &SqlitePool, + user_id: &str, + pagination_store: PaginationStore, +) -> ModmailResult<()> { + let logs = match get_logs_from_user_id(&user_id, &pool).await { + Ok(logs) => logs, + Err(e) => { + eprintln!("Error retrieving logs for user ID {}: {:?}", user_id, e); + return Err(ModmailError::Database(DatabaseError::QueryFailed( + "Failed to retrieve logs.".to_string(), + ))); + } + }; + + let page = 0; + let content = render_logs_page(&logs, page, LOGS_PAGE_SIZE); + let session_id = Uuid::new_v4().to_string(); + + let next_button = + get_translated_message(&config, "logs_command.next", None, None, None, None).await; + let prev_button = + get_translated_message(&config, "logs_command.prev", None, None, None, None).await; + + let components = make_buttons(&[ + ( + &prev_button.to_string(), + &format!("command:logs_prev:{}", session_id), + ButtonStyle::Primary, + true, + ), + ( + &next_button.to_string(), + &format!("command:logs_next:{}", session_id), + ButtonStyle::Primary, + false, + ), + ]); + + let response = MessageBuilder::system_message(&ctx, &config) + .content(content) + .components(components) + .to_channel(msg.channel_id) + .send(false) + .await?; + + pagination_store.lock().await.insert( + session_id.clone(), + PaginationContext { + user_id: user_id.to_string(), + logs, + current_page: page, + message_id: response.id, + channel_id: response.channel_id, + }, + ); + + Ok(()) +} + +pub async fn logs( + ctx: &Context, + msg: &Message, + config: &Config, + _shutdown: Arc>, + pagination: PaginationStore, +) -> ModmailResult<()> { + let pool = match config.db_pool.clone() { + Some(pool) => pool.clone(), + None => return Err(ModmailError::Database(DatabaseError::ConnectionFailed)), + }; + + let user_id = extract_user_id(&msg, &config); + + if user_id.is_empty() { + handle_logs_in_thread(&ctx, &msg, &config, &pool, pagination).await + } else { + handle_logs_from_user_id(&ctx, &msg, &config, &pool, &user_id, pagination).await + } +} diff --git a/rustmail/src/commands/logs/text_command/mod.rs b/rustmail/src/commands/logs/text_command/mod.rs new file mode 100644 index 0000000..af2c2c3 --- /dev/null +++ b/rustmail/src/commands/logs/text_command/mod.rs @@ -0,0 +1 @@ +pub mod logs; diff --git a/rustmail/src/handlers/guild_interaction_handler.rs b/rustmail/src/handlers/guild_interaction_handler.rs index 1dada24..2a14c1a 100644 --- a/rustmail/src/handlers/guild_interaction_handler.rs +++ b/rustmail/src/handlers/guild_interaction_handler.rs @@ -1,9 +1,11 @@ use crate::commands::CommandRegistry; use crate::config::Config; use crate::features::handle_feature_component_interaction; +use crate::modules::commands::handle_command_component_interaction; use crate::modules::threads::{ handle_thread_component_interaction, handle_thread_modal_interaction, }; +use crate::types::logs::PaginationStore; use serenity::all::{Context, EventHandler, Interaction}; use std::sync::Arc; use tokio::sync::watch::Receiver; @@ -13,14 +15,21 @@ pub struct InteractionHandler { pub config: Config, pub registry: Arc, pub shutdown: Arc>, + pub pagination: PaginationStore, } impl InteractionHandler { - pub fn new(config: &Config, register: Arc, shutdown: Receiver) -> Self { + pub fn new( + config: &Config, + register: Arc, + shutdown: Receiver, + pagination: PaginationStore, + ) -> Self { Self { config: config.clone(), registry: register, shutdown: Arc::new(shutdown), + pagination, } } } @@ -40,6 +49,16 @@ impl EventHandler for InteractionHandler { { return; } + if let Err(..) = handle_command_component_interaction( + &ctx, + &self.config, + &mut comp, + self.pagination.clone(), + ) + .await + { + return; + } } Interaction::Modal(mut modal) => { if let Err(..) = diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 6d59c7f..45217ad 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -768,4 +768,12 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "slash_command.remove_reminder_id_argument".to_string(), DictionaryMessage::new("The ID of the reminder to remove"), ); + dict.messages.insert( + "logs_command.next".to_string(), + DictionaryMessage::new("Next"), + ); + dict.messages.insert( + "logs_command.prev".to_string(), + DictionaryMessage::new("Previous"), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index c6f90ec..f9a0332 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -792,4 +792,12 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "slash_command.remove_reminder_id_argument".to_string(), DictionaryMessage::new("L'ID du rappel à supprimer."), ); + dict.messages.insert( + "logs_command.next".to_string(), + DictionaryMessage::new("Suivant"), + ); + dict.messages.insert( + "logs_command.prev".to_string(), + DictionaryMessage::new("Précédent"), + ); } diff --git a/rustmail/src/modules/commands.rs b/rustmail/src/modules/commands.rs new file mode 100644 index 0000000..38fd02f --- /dev/null +++ b/rustmail/src/modules/commands.rs @@ -0,0 +1,108 @@ +use crate::commands::logs::common::render_logs_page; +use crate::config::Config; +use crate::features::make_buttons; +use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; +use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{ButtonStyle, ComponentInteraction, Context}; +use serenity::builder::CreateInteractionResponse; + +pub const LOGS_PAGE_SIZE: usize = 10; + +fn parse_command_interaction(custom_id: &str) -> Option { + custom_id.strip_prefix("command:").map(|s| s.to_string()) +} + +async fn handle_logs_action( + session_id: &str, + page: usize, + ctx: &Context, + config: &Config, + pagination: PaginationStore, +) -> Result<(), Box> { + let mut store = pagination.lock().await; + + let next_button = + get_translated_message(&config, "logs_command.next", None, None, None, None).await; + let prev_button = + get_translated_message(&config, "logs_command.prev", None, None, None, None).await; + + if let Some(ctx_data) = store.get_mut(session_id) { + if page == 0 && ctx_data.current_page > 0 { + ctx_data.current_page -= 1; + } else if page == 1 && ctx_data.current_page < LOGS_PAGE_SIZE { + ctx_data.current_page += 1; + } else { + return Ok(()); + } + + let new_content = render_logs_page(&ctx_data.logs, ctx_data.current_page, LOGS_PAGE_SIZE); + + if new_content.is_empty() { + return Ok(()); + } + + let components = make_buttons(&[ + ( + &prev_button.to_string(), + &format!("command:logs_prev:{}", session_id), + ButtonStyle::Primary, + ctx_data.current_page == 0, + ), + ( + &next_button.to_string(), + &format!("command:logs_next:{}", session_id), + ButtonStyle::Primary, + (ctx_data.current_page + 1) * 10 >= ctx_data.logs.len(), + ), + ]); + + let response = MessageBuilder::system_message(&ctx, &config) + .content(new_content) + .components(components) + .to_channel(ctx_data.channel_id) + .build_edit_message() + .await; + + ctx_data + .channel_id + .edit_message(&ctx.http, ctx_data.message_id, response) + .await?; + } + + Ok(()) +} + +pub async fn handle_command_component_interaction( + ctx: &Context, + config: &Config, + interaction: &mut ComponentInteraction, + pagination: PaginationStore, +) -> Result<(), Box> { + let parts = match parse_command_interaction(&interaction.data.custom_id) { + Some(parts) => parts, + None => return Ok(()), + }; + + if parts.starts_with("logs_next:") { + let session_id = parts.strip_prefix("logs_next:").unwrap(); + + interaction + .create_response(&ctx.http, CreateInteractionResponse::Acknowledge) + .await?; + + handle_logs_action(session_id, 1, ctx, config, pagination.clone()).await?; + } + + if parts.starts_with("logs_prev:") { + let session_id = parts.strip_prefix("logs_prev:").unwrap(); + + interaction + .create_response(&ctx.http, CreateInteractionResponse::Acknowledge) + .await?; + + handle_logs_action(session_id, 0, ctx, config, pagination.clone()).await?; + } + + Ok(()) +} diff --git a/rustmail/src/modules/mod.rs b/rustmail/src/modules/mod.rs index e3ce2a6..ad3b788 100644 --- a/rustmail/src/modules/mod.rs +++ b/rustmail/src/modules/mod.rs @@ -1,3 +1,4 @@ +pub mod commands; pub mod message_recovery; pub mod reminders; pub mod scheduled_closures; diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index b9a49ce..9617f81 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -150,6 +150,10 @@ pub async fn handle_thread_modal_interaction( config: &Config, interaction: &mut ModalInteraction, ) -> Result<(), Box> { + let _ = interaction + .create_response(&ctx.http, CreateInteractionResponse::Acknowledge) + .await; + let parts = match parse_thread_interaction(&interaction.data.custom_id) { Some(parts) => parts, None => { @@ -504,24 +508,7 @@ pub async fn handle_thread_component_interaction( ) -> Result<(), Box> { let parts = match parse_thread_interaction(&interaction.data.custom_id) { Some(parts) => parts, - None => { - let response = CreateInteractionResponse::Message( - MessageBuilder::system_message(&ctx, &config) - .translated_content( - "feature.not_implemented", - None, - Some(interaction.user.id), - interaction.guild_id.map(|g| g.get()), - ) - .await - .to_channel(interaction.channel_id) - .build_interaction_message() - .await - .ephemeral(true), - ); - let _ = interaction.create_response(&ctx.http, response).await; - return Ok(()); - } + None => return Ok(()), }; let key = interaction.channel_id.get(); diff --git a/rustmail/src/utils/command/wrap_command.rs b/rustmail/src/utils/command/wrap_command.rs index 5820b0b..186b1be 100644 --- a/rustmail/src/utils/command/wrap_command.rs +++ b/rustmail/src/utils/command/wrap_command.rs @@ -1,10 +1,10 @@ #[macro_export] macro_rules! wrap_command { ($map:expr, [$($name:expr),+], $func:expr) => {{ - let command: std::sync::Arc) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = - std::sync::Arc::new(|ctx, msg, config, shutdown| { + let command: std::sync::Arc, PaginationStore) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = + std::sync::Arc::new(|ctx, msg, config, shutdown, pagination| { Box::pin(async move { - $func(&ctx, &msg, &config, Arc::new(shutdown)).await + $func(&ctx, &msg, &config, Arc::new(shutdown), pagination).await }) }); $( @@ -12,10 +12,10 @@ macro_rules! wrap_command { )+ }}; ($map:expr, $name:expr, $func:expr) => {{ - let command: std::sync::Arc) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = - std::sync::Arc::new(|ctx, msg, config, shutdown| { + let command: std::sync::Arc, PaginationStore) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = + std::sync::Arc::new(|ctx, msg, config, shutdown, pagination| { Box::pin(async move { - $func(&ctx, &msg, &config, Arc::new(shutdown)).await + $func(&ctx, &msg, &config, Arc::new(shutdown), pagination).await }) }); $map.insert($name.to_string(), command); diff --git a/rustmail/src/utils/message/message_builder.rs b/rustmail/src/utils/message/message_builder.rs index 2124ceb..a0b49de 100644 --- a/rustmail/src/utils/message/message_builder.rs +++ b/rustmail/src/utils/message/message_builder.rs @@ -547,6 +547,10 @@ impl<'a> MessageBuilder<'a> { } } + if let Some(components) = &self.components { + message = message.components(components.clone()); + } + message } From 29bd7b6e64c2ba465be33ffeab4ab2ba847e7301 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 21 Oct 2025 17:29:27 +0200 Subject: [PATCH 042/100] refactor(code): reformat code --- rustmail/src/api/handler/bot/tickets.rs | 2 +- rustmail/src/bot.rs | 6 +++--- .../src/commands/add_reminder/text_command/add_reminder.rs | 4 ++-- rustmail/src/commands/add_staff/text_command/add_staff.rs | 2 +- rustmail/src/commands/alert/text_command/alert.rs | 2 +- rustmail/src/commands/anonreply/text_command/anonreply.rs | 4 ++-- rustmail/src/commands/close/text_command/close.rs | 2 +- rustmail/src/commands/delete/text_command/delete.rs | 2 +- rustmail/src/commands/edit/text_command/edit.rs | 4 ++-- .../src/commands/force_close/text_command/force_close.rs | 2 +- rustmail/src/commands/id/text_command/id.rs | 2 +- rustmail/src/commands/logs/slash_command/logs.rs | 1 + .../src/commands/move_thread/text_command/move_thread.rs | 2 +- rustmail/src/commands/new_thread/text_command/new_thread.rs | 2 +- rustmail/src/commands/recover/text_command/recover.rs | 2 +- .../remove_reminder/text_command/remove_reminder.rs | 2 +- .../src/commands/remove_staff/text_command/remove_staff.rs | 2 +- rustmail/src/commands/reply/text_command/reply.rs | 4 ++-- rustmail/src/features/poll.rs | 2 +- rustmail/src/handlers/guild_messages_handler.rs | 2 +- 20 files changed, 26 insertions(+), 25 deletions(-) diff --git a/rustmail/src/api/handler/bot/tickets.rs b/rustmail/src/api/handler/bot/tickets.rs index 0ac78c5..713845a 100644 --- a/rustmail/src/api/handler/bot/tickets.rs +++ b/rustmail/src/api/handler/bot/tickets.rs @@ -1,8 +1,8 @@ use crate::BotState; use axum::{ + Json, extract::{Query, State}, http::StatusCode, - Json, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 407c946..051a6dd 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -1,3 +1,4 @@ +use crate::commands::CommandRegistry; use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; use crate::commands::alert::slash_command::alert::AlertCommand; @@ -13,10 +14,9 @@ use crate::commands::recover::slash_command::recover::RecoverCommand; use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; use crate::commands::reply::slash_command::reply::ReplyCommand; -use crate::commands::CommandRegistry; use crate::config::load_config; -use crate::errors::types::ConfigError; use crate::errors::ModmailError; +use crate::errors::types::ConfigError; use crate::handlers::guild_handler::GuildHandler; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::handlers::guild_members_handler::GuildMembersHandler; @@ -27,7 +27,7 @@ use crate::handlers::ready_handler::ReadyHandler; use crate::handlers::typing_proxy_handler::TypingProxyHandler; use crate::panel_commands::user::is_member::is_member; use crate::types::logs::PaginationContext; -use crate::{db, BotCommand, BotState, BotStatus}; +use crate::{BotCommand, BotState, BotStatus, db}; use base64::Engine; use rand::RngCore; use serenity::all::{ClientBuilder, GatewayIntents}; diff --git a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs index af12758..a418539 100644 --- a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs @@ -2,10 +2,10 @@ use crate::commands::add_reminder::common::{ send_register_confirmation_from_message, spawn_reminder, }; use crate::config::Config; -use crate::db::reminders::{insert_reminder, Reminder}; +use crate::db::reminders::{Reminder, insert_reminder}; use crate::db::threads::get_thread_by_user_id; use crate::errors::{ - common, CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, + CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, common, }; use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; diff --git a/rustmail/src/commands/add_staff/text_command/add_staff.rs b/rustmail/src/commands/add_staff/text_command/add_staff.rs index 18ef50d..db10a5c 100644 --- a/rustmail/src/commands/add_staff/text_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/text_command/add_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; diff --git a/rustmail/src/commands/alert/text_command/alert.rs b/rustmail/src/commands/alert/text_command/alert.rs index 26c5c5c..6ea4fd8 100644 --- a/rustmail/src/commands/alert/text_command/alert.rs +++ b/rustmail/src/commands/alert/text_command/alert.rs @@ -3,7 +3,7 @@ use crate::commands::alert::common::{ handle_set_alert_from_msg, }; use crate::config::Config; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/anonreply/text_command/anonreply.rs b/rustmail/src/commands/anonreply/text_command/anonreply.rs index 8bbccaa..7f63df5 100644 --- a/rustmail/src/commands/anonreply/text_command/anonreply.rs +++ b/rustmail/src/commands/anonreply/text_command/anonreply.rs @@ -1,10 +1,10 @@ use crate::config::Config; use crate::db::operations::allocate_next_message_number; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; +use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; diff --git a/rustmail/src/commands/close/text_command/close.rs b/rustmail/src/commands/close/text_command/close.rs index 08e71cc..994f339 100644 --- a/rustmail/src/commands/close/text_command/close.rs +++ b/rustmail/src/commands/close/text_command/close.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::types::logs::PaginationStore; use crate::utils::message::category::{ get_category_id_from_message, get_category_name_from_message, diff --git a/rustmail/src/commands/delete/text_command/delete.rs b/rustmail/src/commands/delete/text_command/delete.rs index 8f7f2ff..42e79a2 100644 --- a/rustmail/src/commands/delete/text_command/delete.rs +++ b/rustmail/src/commands/delete/text_command/delete.rs @@ -3,7 +3,7 @@ use crate::commands::delete::common::{ get_thread_info, send_delete_message, update_message_numbers, }; use crate::config::Config; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/edit/text_command/edit.rs b/rustmail/src/commands/edit/text_command/edit.rs index 0df6e77..1848e47 100644 --- a/rustmail/src/commands/edit/text_command/edit.rs +++ b/rustmail/src/commands/edit/text_command/edit.rs @@ -3,13 +3,13 @@ use crate::commands::edit::message_ops::{ cleanup_command_message, edit_messages, format_new_message, get_message_ids, }; use crate::commands::edit::validation::{ - parse_edit_command, validate_edit_permissions, EditCommandInput, + EditCommandInput, parse_edit_command, validate_edit_permissions, }; use crate::config::Config; use crate::db::get_thread_message_by_inbox_message_id; use crate::db::update_message_content; use crate::errors::common::message_not_found; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::types::logs::PaginationStore; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; diff --git a/rustmail/src/commands/force_close/text_command/force_close.rs b/rustmail/src/commands/force_close/text_command/force_close.rs index c664cb6..b9ee1ff 100644 --- a/rustmail/src/commands/force_close/text_command/force_close.rs +++ b/rustmail/src/commands/force_close/text_command/force_close.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/id/text_command/id.rs b/rustmail/src/commands/id/text_command/id.rs index b437fab..7dd61d6 100644 --- a/rustmail/src/commands/id/text_command/id.rs +++ b/rustmail/src/commands/id/text_command/id.rs @@ -1,8 +1,8 @@ use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; -use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::{ModmailError, ModmailResult}; use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; diff --git a/rustmail/src/commands/logs/slash_command/logs.rs b/rustmail/src/commands/logs/slash_command/logs.rs index e69de29..8b13789 100644 --- a/rustmail/src/commands/logs/slash_command/logs.rs +++ b/rustmail/src/commands/logs/slash_command/logs.rs @@ -0,0 +1 @@ + diff --git a/rustmail/src/commands/move_thread/text_command/move_thread.rs b/rustmail/src/commands/move_thread/text_command/move_thread.rs index ef28f27..e4fdd71 100644 --- a/rustmail/src/commands/move_thread/text_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/text_command/move_thread.rs @@ -5,7 +5,7 @@ use crate::commands::move_thread::common::{ use crate::config::Config; use crate::errors::CommandError::NotInThread; use crate::errors::ThreadError::CategoryNotFound; -use crate::errors::{common, CommandError, DiscordError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, DiscordError, ModmailError, ModmailResult, common}; use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/new_thread/text_command/new_thread.rs b/rustmail/src/commands/new_thread/text_command/new_thread.rs index 9aee4aa..c027b12 100644 --- a/rustmail/src/commands/new_thread/text_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/text_command/new_thread.rs @@ -4,7 +4,7 @@ use crate::commands::new_thread::common::{ }; use crate::config::Config; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; -use crate::errors::{common, DiscordError, ModmailError, ModmailResult}; +use crate::errors::{DiscordError, ModmailError, ModmailResult, common}; use crate::types::logs::PaginationStore; use serenity::all::{ChannelId, Context, GuildId, Message}; use std::collections::HashMap; diff --git a/rustmail/src/commands/recover/text_command/recover.rs b/rustmail/src/commands/recover/text_command/recover.rs index 0bbef6d..e3e1bd9 100644 --- a/rustmail/src/commands/recover/text_command/recover.rs +++ b/rustmail/src/commands/recover/text_command/recover.rs @@ -1,5 +1,5 @@ use crate::config::Config; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; use crate::types::logs::PaginationStore; diff --git a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs index 05bed5d..0640d26 100644 --- a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; diff --git a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs index 84fc629..54528d0 100644 --- a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index 544b621..d181160 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -1,11 +1,11 @@ use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; +use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; diff --git a/rustmail/src/features/poll.rs b/rustmail/src/features/poll.rs index 490121a..4842a12 100644 --- a/rustmail/src/features/poll.rs +++ b/rustmail/src/features/poll.rs @@ -1,4 +1,4 @@ -use super::{make_buttons, Feature}; +use super::{Feature, make_buttons}; use crate::config::Config; use crate::utils::message::message_builder::MessageBuilder; use async_trait::async_trait; diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 0ad57cf..52aef7a 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -24,7 +24,7 @@ use crate::db::operations::{ }; use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; From 5ebb5d562145fcc8b13a5795595e050038ba18f2 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Wed, 22 Oct 2025 14:02:02 +0200 Subject: [PATCH 043/100] refactor(panel): add missing sqlx files --- ...847582ff4fc54c9b6d26ff758cf04bba1d647.json | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .sqlx/query-62f2f9b44d00169468b3596c352847582ff4fc54c9b6d26ff758cf04bba1d647.json diff --git a/.sqlx/query-62f2f9b44d00169468b3596c352847582ff4fc54c9b6d26ff758cf04bba1d647.json b/.sqlx/query-62f2f9b44d00169468b3596c352847582ff4fc54c9b6d26ff758cf04bba1d647.json new file mode 100644 index 0000000..7eae63e --- /dev/null +++ b/.sqlx/query-62f2f9b44d00169468b3596c352847582ff4fc54c9b6d26ff758cf04bba1d647.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n (COUNT(*) OVER ())\n - ROW_NUMBER() OVER (ORDER BY created_at DESC) + 1 AS id,\n id AS ticket_id,\n user_id AS \"user_id: String\",\n created_at AS \"created_at: String\"\n FROM threads\n WHERE user_id = ? AND status = 0\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "ticket_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "user_id: String", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "created_at: String", + "ordinal": 3, + "type_info": "Datetime" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "62f2f9b44d00169468b3596c352847582ff4fc54c9b6d26ff758cf04bba1d647" +} From ed25d89df4975d7c8f3a92e8e79e68c1705ad567 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Wed, 22 Oct 2025 15:01:16 +0200 Subject: [PATCH 044/100] refactor(panel): add disable option to logs command's buttons --- rustmail/src/commands/logs/common.rs | 1 - .../src/commands/logs/text_command/logs.rs | 41 ++----------------- rustmail/src/db/operations/messages.rs | 2 - rustmail/src/modules/message_recovery.rs | 2 - rustmail/src/modules/threads.rs | 4 +- 5 files changed, 5 insertions(+), 45 deletions(-) diff --git a/rustmail/src/commands/logs/common.rs b/rustmail/src/commands/logs/common.rs index 5957361..8f5c3d1 100644 --- a/rustmail/src/commands/logs/common.rs +++ b/rustmail/src/commands/logs/common.rs @@ -8,7 +8,6 @@ pub fn extract_user_id(msg: &Message, config: &Config) -> String { let command_name = "logs"; if content.starts_with(&format!("{}{}", prefix, command_name)) { - println!("Logging in..."); let start = prefix.len() + command_name.len(); content[start..].trim().to_string() } else { diff --git a/rustmail/src/commands/logs/text_command/logs.rs b/rustmail/src/commands/logs/text_command/logs.rs index 15f4c0f..63ff479 100644 --- a/rustmail/src/commands/logs/text_command/logs.rs +++ b/rustmail/src/commands/logs/text_command/logs.rs @@ -1,4 +1,4 @@ -use crate::commands::logs::common::extract_user_id; +use crate::commands::logs::common::{extract_user_id, render_logs_page}; use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::logs::get_logs_from_user_id; @@ -6,7 +6,7 @@ use crate::errors::{DatabaseError, ModmailError, ModmailResult, ThreadError}; use crate::features::make_buttons; use crate::i18n::get_translated_message; use crate::modules::commands::LOGS_PAGE_SIZE; -use crate::types::logs::{PaginationContext, PaginationStore, TicketLog}; +use crate::types::logs::{PaginationContext, PaginationStore}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ButtonStyle, Context, Message}; use sqlx::SqlitePool; @@ -14,39 +14,6 @@ use std::sync::Arc; use tokio::sync::watch::Receiver; use uuid::Uuid; -fn render_logs_page(logs: &[TicketLog], page: usize, per_page: usize) -> String { - let total_pages = (logs.len() + per_page - 1) / per_page; - let start = page * per_page; - let end = usize::min(start + per_page, logs.len()); - - let mut desc = String::new(); - - for (_, log) in logs[start..end].iter().enumerate() { - use std::fmt::Write; - let _ = writeln!( - desc, - "**#{}** | [`Ticket {}`]({}) | Fermé le {} {}", - log.id, - log.ticket_id, - format!("http://localhost:3002/panel/tickets/{}", log.ticket_id), - log.created_at, - "\n".to_string(), - ); - } - - if desc.is_empty() { - desc = "_Aucun log trouvé pour cet utilisateur._".into(); - } - - format!( - "{}\n_Page {}/{} ({} logs totaux)_", - desc, - page + 1, - total_pages.max(1), - logs.len() - ) -} - pub async fn handle_logs_in_thread( ctx: &Context, msg: &Message, @@ -102,13 +69,13 @@ pub async fn handle_logs_from_user_id( &prev_button.to_string(), &format!("command:logs_prev:{}", session_id), ButtonStyle::Primary, - true, + page == 0, ), ( &next_button.to_string(), &format!("command:logs_next:{}", session_id), ButtonStyle::Primary, - false, + (page + 1) * 10 >= logs.len(), ), ]); diff --git a/rustmail/src/db/operations/messages.rs b/rustmail/src/db/operations/messages.rs index 2a8c35b..3192ef7 100644 --- a/rustmail/src/db/operations/messages.rs +++ b/rustmail/src/db/operations/messages.rs @@ -179,8 +179,6 @@ pub async fn get_latest_thread_message( .fetch_optional(pool) .await?; - println!("Debug: Fetched latest thread message row: {:?}", row); - let latest = row.map(|row| ThreadMessage { id: row.id, thread_id: row.thread_id, diff --git a/rustmail/src/modules/message_recovery.rs b/rustmail/src/modules/message_recovery.rs index b5db7d5..0019435 100644 --- a/rustmail/src/modules/message_recovery.rs +++ b/rustmail/src/modules/message_recovery.rs @@ -86,8 +86,6 @@ async fn recover_messages_for_thread( } }; - println!("Recovering messages for thread {:?}", latest_message); - let last_dm_message_id = match &latest_message.dm_message_id { Some(id) => id, None => { diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index 9617f81..cc37347 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -150,9 +150,7 @@ pub async fn handle_thread_modal_interaction( config: &Config, interaction: &mut ModalInteraction, ) -> Result<(), Box> { - let _ = interaction - .create_response(&ctx.http, CreateInteractionResponse::Acknowledge) - .await; + let parts = match parse_thread_interaction(&interaction.data.custom_id) { Some(parts) => parts, From 21ce91b511dc7e049593222f4013070ee1297031 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Wed, 22 Oct 2025 16:37:19 +0200 Subject: [PATCH 045/100] refactor(commands): add pagination to slash commands --- .../src/commands/add_reminder/slash_command/add_reminder.rs | 2 ++ rustmail/src/commands/add_staff/slash_command/add_staff.rs | 2 ++ rustmail/src/commands/alert/slash_command/alert.rs | 2 ++ rustmail/src/commands/close/slash_command/close.rs | 2 ++ rustmail/src/commands/delete/slash_command/delete.rs | 2 ++ rustmail/src/commands/edit/slash_command/edit.rs | 2 ++ .../src/commands/force_close/slash_command/force_close.rs | 2 ++ rustmail/src/commands/help/slash_command/help.rs | 2 ++ rustmail/src/commands/id/slash_command/id.rs | 2 ++ rustmail/src/commands/mod.rs | 6 +++++- .../src/commands/move_thread/slash_command/move_thread.rs | 2 ++ .../src/commands/new_thread/slash_command/new_thread.rs | 2 ++ rustmail/src/commands/recover/slash_command/recover.rs | 2 ++ .../remove_reminder/slash_command/remove_reminder.rs | 2 ++ .../src/commands/remove_staff/slash_command/remove_staff.rs | 2 ++ rustmail/src/commands/reply/slash_command/reply.rs | 2 ++ 16 files changed, 35 insertions(+), 1 deletion(-) diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index b7221d8..ee9423c 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -18,6 +18,7 @@ use serenity::all::{ }; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct AddReminderCommand; @@ -86,6 +87,7 @@ impl RegistrableCommand for AddReminderCommand { _options: &[ResolvedOption<'_>], config: &Config, shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index 0749a63..ae1c662 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -15,6 +15,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct AddStaffCommand; @@ -66,6 +67,7 @@ impl RegistrableCommand for AddStaffCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/alert/slash_command/alert.rs b/rustmail/src/commands/alert/slash_command/alert.rs index 1491cb6..9e30bbc 100644 --- a/rustmail/src/commands/alert/slash_command/alert.rs +++ b/rustmail/src/commands/alert/slash_command/alert.rs @@ -13,6 +13,7 @@ use serenity::all::{ }; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct AlertCommand; @@ -63,6 +64,7 @@ impl RegistrableCommand for AlertCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index 4038674..88a0500 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -23,6 +23,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::watch::Receiver; use tokio::time::sleep; +use crate::types::logs::PaginationStore; pub struct CloseCommand; @@ -103,6 +104,7 @@ impl RegistrableCommand for CloseCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/delete/slash_command/delete.rs b/rustmail/src/commands/delete/slash_command/delete.rs index 6793c4d..4be45ee 100644 --- a/rustmail/src/commands/delete/slash_command/delete.rs +++ b/rustmail/src/commands/delete/slash_command/delete.rs @@ -16,6 +16,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct DeleteCommand; @@ -71,6 +72,7 @@ impl RegistrableCommand for DeleteCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/edit/slash_command/edit.rs b/rustmail/src/commands/edit/slash_command/edit.rs index 6b01b29..ecd757f 100644 --- a/rustmail/src/commands/edit/slash_command/edit.rs +++ b/rustmail/src/commands/edit/slash_command/edit.rs @@ -16,6 +16,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct EditCommand; @@ -87,6 +88,7 @@ impl RegistrableCommand for EditCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/force_close/slash_command/force_close.rs b/rustmail/src/commands/force_close/slash_command/force_close.rs index e0f69c7..654a535 100644 --- a/rustmail/src/commands/force_close/slash_command/force_close.rs +++ b/rustmail/src/commands/force_close/slash_command/force_close.rs @@ -9,6 +9,7 @@ use crate::i18n::get_translated_message; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct ForceCloseCommand; @@ -43,6 +44,7 @@ impl RegistrableCommand for ForceCloseCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/help/slash_command/help.rs b/rustmail/src/commands/help/slash_command/help.rs index 6a1f71c..6bb448b 100644 --- a/rustmail/src/commands/help/slash_command/help.rs +++ b/rustmail/src/commands/help/slash_command/help.rs @@ -7,6 +7,7 @@ use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct HelpCommand; @@ -41,6 +42,7 @@ impl RegistrableCommand for HelpCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/id/slash_command/id.rs b/rustmail/src/commands/id/slash_command/id.rs index e3a3854..6ef1bb7 100644 --- a/rustmail/src/commands/id/slash_command/id.rs +++ b/rustmail/src/commands/id/slash_command/id.rs @@ -11,6 +11,7 @@ use serenity::all::{CommandInteraction, Context, ResolvedOption}; use serenity::builder::CreateCommand; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct IdCommand; @@ -45,6 +46,7 @@ impl RegistrableCommand for IdCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index f4dc5e1..854db3a 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub mod add_reminder; pub mod add_staff; @@ -38,19 +39,22 @@ pub trait RegistrableCommand: Send + Sync { options: &[ResolvedOption<'_>], config: &Config, shutdown: Arc>, + pagination: PaginationStore, ) -> BoxFuture>; } pub struct CommandRegistry { commands: HashMap<&'static str, Arc>, _shutdown: Arc>, + pagination: PaginationStore, } impl CommandRegistry { - pub fn new(shutdown: Receiver) -> Self { + pub fn new(shutdown: Receiver, pagination: PaginationStore) -> Self { Self { commands: HashMap::new(), _shutdown: Arc::new(shutdown), + pagination, } } diff --git a/rustmail/src/commands/move_thread/slash_command/move_thread.rs b/rustmail/src/commands/move_thread/slash_command/move_thread.rs index d42a9ca..cb2b7a3 100644 --- a/rustmail/src/commands/move_thread/slash_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/slash_command/move_thread.rs @@ -17,6 +17,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct MoveCommand; @@ -69,6 +70,7 @@ impl RegistrableCommand for MoveCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index 01478f8..85e7439 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -15,6 +15,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct NewThreadCommand; @@ -67,6 +68,7 @@ impl RegistrableCommand for NewThreadCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/recover/slash_command/recover.rs b/rustmail/src/commands/recover/slash_command/recover.rs index 0e42036..fa47621 100644 --- a/rustmail/src/commands/recover/slash_command/recover.rs +++ b/rustmail/src/commands/recover/slash_command/recover.rs @@ -9,6 +9,7 @@ use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct RecoverCommand; @@ -43,6 +44,7 @@ impl RegistrableCommand for RecoverCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index fbcbd33..32cb3db 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -12,6 +12,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct RemoveReminderCommand; @@ -57,6 +58,7 @@ impl RegistrableCommand for RemoveReminderCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index 11758d5..fa35eb6 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -15,6 +15,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct RemoveStaffCommand; @@ -67,6 +68,7 @@ impl RegistrableCommand for RemoveStaffCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index 5b53af8..65682d2 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -15,6 +15,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; +use crate::types::logs::PaginationStore; pub struct ReplyCommand; @@ -100,6 +101,7 @@ impl RegistrableCommand for ReplyCommand { _options: &[ResolvedOption<'_>], config: &Config, _shutdown: Arc>, + _pagination: PaginationStore, ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); From 19e4e0a4ad52f65abd22534ef0e8d64fb6f82fdf Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Wed, 22 Oct 2025 16:47:27 +0200 Subject: [PATCH 046/100] feat(dict): add trads for logs command --- rustmail/src/i18n/language/en.rs | 8 ++++++++ rustmail/src/i18n/language/fr.rs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 45217ad..352acb2 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -776,4 +776,12 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "logs_command.prev".to_string(), DictionaryMessage::new("Previous"), ); + dict.messages.insert( + "slash_commands.logs_command_description".to_string(), + DictionaryMessage::new("View the logs of a specific user"), + ); + dict.messages.insert( + "slash_commands.logs_id_argument_description".to_string(), + DictionaryMessage::new("The ID of the user to view logs for"), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index f9a0332..c6bfaf7 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -800,4 +800,12 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "logs_command.prev".to_string(), DictionaryMessage::new("Précédent"), ); + dict.messages.insert( + "slash_commands.logs_command_description".to_string(), + DictionaryMessage::new("Afficher les logs d'un utilisateur"), + ); + dict.messages.insert( + "slash_commands.logs_id_argument_description".to_string(), + DictionaryMessage::new("L'ID de l'utilisateur dont vous souhaitez voir les logs"), + ); } From 1c611e2e5d186a3de089a32990c46f82586fb37d Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Wed, 22 Oct 2025 16:48:49 +0200 Subject: [PATCH 047/100] feat(commands): add logs slash command --- rustmail/src/bot.rs | 4 +- rustmail/src/commands/logs/common.rs | 32 ++++++- .../src/commands/logs/slash_command/logs.rs | 86 +++++++++++++++++++ .../src/commands/logs/text_command/logs.rs | 29 +++---- .../src/handlers/guild_interaction_handler.rs | 3 +- rustmail/src/utils/message/message_builder.rs | 8 ++ 6 files changed, 143 insertions(+), 19 deletions(-) diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 051a6dd..a8e1a48 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -38,6 +38,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; use tokio::{select, spawn}; +use crate::commands::logs::slash_command::logs::LogsCommand; pub async fn init_bot_state() -> Arc> { let pool = db::operations::init_database() @@ -142,7 +143,7 @@ pub async fn run_bot( cache_settings.max_messages = 10_000; cache_settings.time_to_live = Duration::from_secs(6 * 60 * 60); - let mut registry = CommandRegistry::new(shutdown_rx_command); + let mut registry = CommandRegistry::new(shutdown_rx_command, pagination.clone()); registry.register_command(AddStaffCommand); registry.register_command(AlertCommand); registry.register_command(CloseCommand); @@ -158,6 +159,7 @@ pub async fn run_bot( registry.register_command(ReplyCommand); registry.register_command(AddReminderCommand); registry.register_command(RemoveReminderCommand); + registry.register_command(LogsCommand); let registry = Arc::new(registry); diff --git a/rustmail/src/commands/logs/common.rs b/rustmail/src/commands/logs/common.rs index 8f5c3d1..6bb6bd9 100644 --- a/rustmail/src/commands/logs/common.rs +++ b/rustmail/src/commands/logs/common.rs @@ -1,6 +1,10 @@ use crate::config::Config; use crate::types::logs::TicketLog; -use serenity::all::Message; +use serenity::all::{ChannelId, CommandInteraction, Message}; +use serenity::builder::CreateActionRow; +use serenity::client::Context; +use crate::errors::ModmailError; +use crate::utils::message::message_builder::MessageBuilder; pub fn extract_user_id(msg: &Message, config: &Config) -> String { let content = msg.content.trim(); @@ -51,3 +55,29 @@ pub fn render_logs_page(logs: &[TicketLog], page: usize, per_page: usize) -> Str logs.len() ) } + +pub async fn get_response(ctx: Context, config: Config, content: &str, components: Vec, channel_id: ChannelId, command: Option) -> Result { + + if !command.is_none() { + let command = command.unwrap(); + + let response = MessageBuilder::system_message(&ctx.clone(), &config.clone()) + .content(content) + .components(components) + .to_channel(channel_id) + .build_interaction_message_followup() + .await; + + let tkt = command.create_followup(&ctx.http, response).await; + + Ok(tkt?) + + } else { + MessageBuilder::system_message(&ctx, &config) + .content(content) + .components(components) + .to_channel(channel_id) + .send(false) + .await + } +} diff --git a/rustmail/src/commands/logs/slash_command/logs.rs b/rustmail/src/commands/logs/slash_command/logs.rs index 8b13789..850e5f8 100644 --- a/rustmail/src/commands/logs/slash_command/logs.rs +++ b/rustmail/src/commands/logs/slash_command/logs.rs @@ -1 +1,87 @@ +use std::sync::Arc; +use serenity::all::{CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, UserId}; +use tokio::sync::watch::Receiver; +use crate::commands::{BoxFuture, RegistrableCommand}; +use crate::commands::logs::text_command::logs::{handle_logs_from_user_id, handle_logs_in_thread}; +use crate::config::Config; +use crate::errors::{DatabaseError, ModmailError, ModmailResult}; +use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; +use crate::utils::command::defer_response::defer_response; +pub struct LogsCommand; + +impl RegistrableCommand for LogsCommand { + fn name(&self) -> &'static str { + "logs" + } + + fn register(&self, config: &Config) -> BoxFuture> { + let config = config.clone(); + + Box::pin(async move { + let cmd_desc = get_translated_message( + &config, + "slash_commands.logs_command_description", + None, + None, + None, + None, + ) + .await; + let id_desc = get_translated_message( + &config, + "slash_commands.logs_id_argument_description", + None, + None, + None, + None, + ) + .await; + + vec![ + CreateCommand::new("logs").description(cmd_desc) + .add_option( + CreateCommandOption::new( + CommandOptionType::User, "id", id_desc).required(false) + ) + ] + }) + } + + fn run(&self, ctx: &Context, command: &CommandInteraction, options: &[ResolvedOption<'_>], config: &Config, shutdown: Arc>, pagination: PaginationStore) -> BoxFuture> { + let ctx = ctx.clone(); + let command = command.clone(); + let config = config.clone(); + + Box::pin(async move { + + let pool = match config.db_pool.clone() { + Some(pool) => pool.clone(), + None => return Err(ModmailError::Database(DatabaseError::ConnectionFailed)), + }; + + defer_response(&ctx, &command).await?; + + let mut user_id: Option = None; + + for option in &command.data.options { + match option.name.as_str() { + "id" => { + if let CommandDataOptionValue::User(val) = &option.value { + user_id.replace(val.clone()); + } + } + _ => {} + } + } + + if !user_id.is_some() { + handle_logs_in_thread(&ctx, &command.clone().channel_id, Some(command.clone()), &config, &pool, pagination).await + } else { + handle_logs_from_user_id(&ctx, &command.clone().channel_id, Some(command), &config, &pool, &user_id.unwrap().to_string(), pagination).await + } + + }) + } +} diff --git a/rustmail/src/commands/logs/text_command/logs.rs b/rustmail/src/commands/logs/text_command/logs.rs index 63ff479..862f323 100644 --- a/rustmail/src/commands/logs/text_command/logs.rs +++ b/rustmail/src/commands/logs/text_command/logs.rs @@ -1,4 +1,4 @@ -use crate::commands::logs::common::{extract_user_id, render_logs_page}; +use crate::commands::logs::common::{extract_user_id, get_response, render_logs_page}; use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::logs::get_logs_from_user_id; @@ -7,8 +7,7 @@ use crate::features::make_buttons; use crate::i18n::get_translated_message; use crate::modules::commands::LOGS_PAGE_SIZE; use crate::types::logs::{PaginationContext, PaginationStore}; -use crate::utils::message::message_builder::MessageBuilder; -use serenity::all::{ButtonStyle, Context, Message}; +use serenity::all::{ButtonStyle, ChannelId, CommandInteraction, Context, Message}; use sqlx::SqlitePool; use std::sync::Arc; use tokio::sync::watch::Receiver; @@ -16,30 +15,33 @@ use uuid::Uuid; pub async fn handle_logs_in_thread( ctx: &Context, - msg: &Message, + channel_id: &ChannelId, + command: Option, config: &Config, pool: &SqlitePool, pagination: PaginationStore, ) -> ModmailResult<()> { - let thread = match get_thread_by_channel_id(&msg.channel_id.to_string(), &pool).await { + let thread = match get_thread_by_channel_id(&channel_id.to_string(), &pool).await { Some(thread) => thread, None => return Err(ModmailError::Thread(ThreadError::ThreadNotFound)), }; handle_logs_from_user_id( &ctx, - &msg, + channel_id, + command, &config, &pool, &thread.user_id.to_string(), - pagination, + pagination ) .await } pub async fn handle_logs_from_user_id( ctx: &Context, - msg: &Message, + channel_id: &ChannelId, + command: Option, config: &Config, pool: &SqlitePool, user_id: &str, @@ -79,12 +81,7 @@ pub async fn handle_logs_from_user_id( ), ]); - let response = MessageBuilder::system_message(&ctx, &config) - .content(content) - .components(components) - .to_channel(msg.channel_id) - .send(false) - .await?; + let response = get_response(ctx.clone(), config.clone(), &content, components, *channel_id, command).await?; pagination_store.lock().await.insert( session_id.clone(), @@ -115,8 +112,8 @@ pub async fn logs( let user_id = extract_user_id(&msg, &config); if user_id.is_empty() { - handle_logs_in_thread(&ctx, &msg, &config, &pool, pagination).await + handle_logs_in_thread(&ctx, &msg.channel_id, None, &config, &pool, pagination).await } else { - handle_logs_from_user_id(&ctx, &msg, &config, &pool, &user_id, pagination).await + handle_logs_from_user_id(&ctx, &msg.channel_id, None, config, &pool, &user_id.to_string(), pagination).await } } diff --git a/rustmail/src/handlers/guild_interaction_handler.rs b/rustmail/src/handlers/guild_interaction_handler.rs index 2a14c1a..5e8acdd 100644 --- a/rustmail/src/handlers/guild_interaction_handler.rs +++ b/rustmail/src/handlers/guild_interaction_handler.rs @@ -72,10 +72,11 @@ impl EventHandler for InteractionHandler { let command = command.clone(); let options = command.data.options().clone(); let config = self.config.clone(); + let pagination = self.pagination.clone(); if let Some(handler) = self.registry.get(command.data.name.as_str()) { let result = handler - .run(&ctx, &command, &options, &config, self.shutdown.clone()) + .run(&ctx, &command, &options, &config, self.shutdown.clone(), pagination) .await; if let Err(e) = result { diff --git a/rustmail/src/utils/message/message_builder.rs b/rustmail/src/utils/message/message_builder.rs index a0b49de..f858109 100644 --- a/rustmail/src/utils/message/message_builder.rs +++ b/rustmail/src/utils/message/message_builder.rs @@ -566,6 +566,10 @@ impl<'a> MessageBuilder<'a> { } } + if let Some(components) = &self.components { + message = message.components(components.clone()); + } + message = message.ephemeral(self.ephemeral); message @@ -589,6 +593,10 @@ impl<'a> MessageBuilder<'a> { } } + if let Some(components) = &self.components { + message = message.components(components.clone()); + } + message = message.ephemeral(self.ephemeral); message From f6d774501309fb5220d43f3e0788c0733029c4b6 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 08:28:56 +0200 Subject: [PATCH 048/100] refactor(code): reformat code --- rustmail/src/bot.rs | 2 +- .../slash_command/add_reminder.rs | 2 +- .../add_staff/slash_command/add_staff.rs | 2 +- .../src/commands/alert/slash_command/alert.rs | 2 +- .../src/commands/close/slash_command/close.rs | 2 +- .../commands/delete/slash_command/delete.rs | 2 +- .../src/commands/edit/slash_command/edit.rs | 2 +- .../force_close/slash_command/force_close.rs | 2 +- .../src/commands/help/slash_command/help.rs | 2 +- rustmail/src/commands/id/slash_command/id.rs | 2 +- rustmail/src/commands/logs/common.rs | 15 +++-- .../src/commands/logs/slash_command/logs.rs | 58 +++++++++++++------ .../src/commands/logs/text_command/logs.rs | 23 +++++++- rustmail/src/commands/mod.rs | 2 +- .../move_thread/slash_command/move_thread.rs | 2 +- .../new_thread/slash_command/new_thread.rs | 2 +- .../commands/recover/slash_command/recover.rs | 2 +- .../slash_command/remove_reminder.rs | 2 +- .../slash_command/remove_staff.rs | 2 +- .../src/commands/reply/slash_command/reply.rs | 2 +- .../src/handlers/guild_interaction_handler.rs | 9 ++- rustmail/src/modules/threads.rs | 2 - 22 files changed, 95 insertions(+), 46 deletions(-) diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index a8e1a48..9e69a9c 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -8,6 +8,7 @@ use crate::commands::edit::slash_command::edit::EditCommand; use crate::commands::force_close::slash_command::force_close::ForceCloseCommand; use crate::commands::help::slash_command::help::HelpCommand; use crate::commands::id::slash_command::id::IdCommand; +use crate::commands::logs::slash_command::logs::LogsCommand; use crate::commands::move_thread::slash_command::move_thread::MoveCommand; use crate::commands::new_thread::slash_command::new_thread::NewThreadCommand; use crate::commands::recover::slash_command::recover::RecoverCommand; @@ -38,7 +39,6 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; use tokio::{select, spawn}; -use crate::commands::logs::slash_command::logs::LogsCommand; pub async fn init_bot_state() -> Arc> { let pool = db::operations::init_database() diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index ee9423c..67228a7 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -9,6 +9,7 @@ use crate::errors::{ CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, common, }; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use chrono::{Local, NaiveTime}; use regex::Regex; @@ -18,7 +19,6 @@ use serenity::all::{ }; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct AddReminderCommand; diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index ae1c662..2560b5f 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -6,6 +6,7 @@ use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -15,7 +16,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct AddStaffCommand; diff --git a/rustmail/src/commands/alert/slash_command/alert.rs b/rustmail/src/commands/alert/slash_command/alert.rs index 9e30bbc..f4a0536 100644 --- a/rustmail/src/commands/alert/slash_command/alert.rs +++ b/rustmail/src/commands/alert/slash_command/alert.rs @@ -6,6 +6,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, @@ -13,7 +14,6 @@ use serenity::all::{ }; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct AlertCommand; diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index 88a0500..25eaa93 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -6,6 +6,7 @@ use crate::db::{ }; use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::category::{ get_category_id_from_command, get_category_name_from_command, get_required_permissions_channel_from_command, @@ -23,7 +24,6 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::watch::Receiver; use tokio::time::sleep; -use crate::types::logs::PaginationStore; pub struct CloseCommand; diff --git a/rustmail/src/commands/delete/slash_command/delete.rs b/rustmail/src/commands/delete/slash_command/delete.rs index 4be45ee..5b4bf26 100644 --- a/rustmail/src/commands/delete/slash_command/delete.rs +++ b/rustmail/src/commands/delete/slash_command/delete.rs @@ -7,6 +7,7 @@ use crate::config::Config; use crate::db::messages::get_thread_message_by_message_id; use crate::errors::{MessageError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response_ephemeral; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -16,7 +17,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct DeleteCommand; diff --git a/rustmail/src/commands/edit/slash_command/edit.rs b/rustmail/src/commands/edit/slash_command/edit.rs index ecd757f..1195206 100644 --- a/rustmail/src/commands/edit/slash_command/edit.rs +++ b/rustmail/src/commands/edit/slash_command/edit.rs @@ -6,6 +6,7 @@ use crate::db::{get_thread_message_by_inbox_message_id, update_message_content}; use crate::errors::common::message_not_found; use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; @@ -16,7 +17,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct EditCommand; diff --git a/rustmail/src/commands/force_close/slash_command/force_close.rs b/rustmail/src/commands/force_close/slash_command/force_close.rs index 654a535..c3cb460 100644 --- a/rustmail/src/commands/force_close/slash_command/force_close.rs +++ b/rustmail/src/commands/force_close/slash_command/force_close.rs @@ -6,10 +6,10 @@ use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; use crate::errors::{ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct ForceCloseCommand; diff --git a/rustmail/src/commands/help/slash_command/help.rs b/rustmail/src/commands/help/slash_command/help.rs index 6bb448b..ea6c784 100644 --- a/rustmail/src/commands/help/slash_command/help.rs +++ b/rustmail/src/commands/help/slash_command/help.rs @@ -2,12 +2,12 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::errors::ModmailResult; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct HelpCommand; diff --git a/rustmail/src/commands/id/slash_command/id.rs b/rustmail/src/commands/id/slash_command/id.rs index 6ef1bb7..6dff7e5 100644 --- a/rustmail/src/commands/id/slash_command/id.rs +++ b/rustmail/src/commands/id/slash_command/id.rs @@ -5,13 +5,13 @@ use crate::db::threads::is_a_ticket_channel; use crate::errors::ThreadError::{NotAThreadChannel, ThreadNotFound}; use crate::errors::{DatabaseError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, ResolvedOption}; use serenity::builder::CreateCommand; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct IdCommand; diff --git a/rustmail/src/commands/logs/common.rs b/rustmail/src/commands/logs/common.rs index 6bb6bd9..edd1d87 100644 --- a/rustmail/src/commands/logs/common.rs +++ b/rustmail/src/commands/logs/common.rs @@ -1,10 +1,10 @@ use crate::config::Config; +use crate::errors::ModmailError; use crate::types::logs::TicketLog; +use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ChannelId, CommandInteraction, Message}; use serenity::builder::CreateActionRow; use serenity::client::Context; -use crate::errors::ModmailError; -use crate::utils::message::message_builder::MessageBuilder; pub fn extract_user_id(msg: &Message, config: &Config) -> String { let content = msg.content.trim(); @@ -56,8 +56,14 @@ pub fn render_logs_page(logs: &[TicketLog], page: usize, per_page: usize) -> Str ) } -pub async fn get_response(ctx: Context, config: Config, content: &str, components: Vec, channel_id: ChannelId, command: Option) -> Result { - +pub async fn get_response( + ctx: Context, + config: Config, + content: &str, + components: Vec, + channel_id: ChannelId, + command: Option, +) -> Result { if !command.is_none() { let command = command.unwrap(); @@ -71,7 +77,6 @@ pub async fn get_response(ctx: Context, config: Config, content: &str, component let tkt = command.create_followup(&ctx.http, response).await; Ok(tkt?) - } else { MessageBuilder::system_message(&ctx, &config) .content(content) diff --git a/rustmail/src/commands/logs/slash_command/logs.rs b/rustmail/src/commands/logs/slash_command/logs.rs index 850e5f8..e61e185 100644 --- a/rustmail/src/commands/logs/slash_command/logs.rs +++ b/rustmail/src/commands/logs/slash_command/logs.rs @@ -1,13 +1,16 @@ -use std::sync::Arc; -use serenity::all::{CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, UserId}; -use tokio::sync::watch::Receiver; -use crate::commands::{BoxFuture, RegistrableCommand}; use crate::commands::logs::text_command::logs::{handle_logs_from_user_id, handle_logs_in_thread}; +use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::errors::{DatabaseError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; +use serenity::all::{ + CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, + CreateCommandOption, ResolvedOption, UserId, +}; +use std::sync::Arc; +use tokio::sync::watch::Receiver; pub struct LogsCommand; @@ -28,7 +31,7 @@ impl RegistrableCommand for LogsCommand { None, None, ) - .await; + .await; let id_desc = get_translated_message( &config, "slash_commands.logs_id_argument_description", @@ -37,25 +40,28 @@ impl RegistrableCommand for LogsCommand { None, None, ) - .await; + .await; - vec![ - CreateCommand::new("logs").description(cmd_desc) - .add_option( - CreateCommandOption::new( - CommandOptionType::User, "id", id_desc).required(false) - ) - ] + vec![CreateCommand::new("logs").description(cmd_desc).add_option( + CreateCommandOption::new(CommandOptionType::User, "id", id_desc).required(false), + )] }) } - fn run(&self, ctx: &Context, command: &CommandInteraction, options: &[ResolvedOption<'_>], config: &Config, shutdown: Arc>, pagination: PaginationStore) -> BoxFuture> { + fn run( + &self, + ctx: &Context, + command: &CommandInteraction, + _options: &[ResolvedOption<'_>], + config: &Config, + _shutdown: Arc>, + pagination: PaginationStore, + ) -> BoxFuture> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); Box::pin(async move { - let pool = match config.db_pool.clone() { Some(pool) => pool.clone(), None => return Err(ModmailError::Database(DatabaseError::ConnectionFailed)), @@ -77,11 +83,27 @@ impl RegistrableCommand for LogsCommand { } if !user_id.is_some() { - handle_logs_in_thread(&ctx, &command.clone().channel_id, Some(command.clone()), &config, &pool, pagination).await + handle_logs_in_thread( + &ctx, + &command.clone().channel_id, + Some(command.clone()), + &config, + &pool, + pagination, + ) + .await } else { - handle_logs_from_user_id(&ctx, &command.clone().channel_id, Some(command), &config, &pool, &user_id.unwrap().to_string(), pagination).await + handle_logs_from_user_id( + &ctx, + &command.clone().channel_id, + Some(command), + &config, + &pool, + &user_id.unwrap().to_string(), + pagination, + ) + .await } - }) } } diff --git a/rustmail/src/commands/logs/text_command/logs.rs b/rustmail/src/commands/logs/text_command/logs.rs index 862f323..eb2a8b4 100644 --- a/rustmail/src/commands/logs/text_command/logs.rs +++ b/rustmail/src/commands/logs/text_command/logs.rs @@ -33,7 +33,7 @@ pub async fn handle_logs_in_thread( &config, &pool, &thread.user_id.to_string(), - pagination + pagination, ) .await } @@ -81,7 +81,15 @@ pub async fn handle_logs_from_user_id( ), ]); - let response = get_response(ctx.clone(), config.clone(), &content, components, *channel_id, command).await?; + let response = get_response( + ctx.clone(), + config.clone(), + &content, + components, + *channel_id, + command, + ) + .await?; pagination_store.lock().await.insert( session_id.clone(), @@ -114,6 +122,15 @@ pub async fn logs( if user_id.is_empty() { handle_logs_in_thread(&ctx, &msg.channel_id, None, &config, &pool, pagination).await } else { - handle_logs_from_user_id(&ctx, &msg.channel_id, None, config, &pool, &user_id.to_string(), pagination).await + handle_logs_from_user_id( + &ctx, + &msg.channel_id, + None, + config, + &pool, + &user_id.to_string(), + pagination, + ) + .await } } diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index 854db3a..c954794 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -1,11 +1,11 @@ use crate::config::Config; use crate::errors::ModmailResult; +use crate::types::logs::PaginationStore; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub mod add_reminder; pub mod add_staff; diff --git a/rustmail/src/commands/move_thread/slash_command/move_thread.rs b/rustmail/src/commands/move_thread/slash_command/move_thread.rs index cb2b7a3..321326e 100644 --- a/rustmail/src/commands/move_thread/slash_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/slash_command/move_thread.rs @@ -8,6 +8,7 @@ use crate::errors::{ CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, ThreadError, }; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -17,7 +18,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct MoveCommand; diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index 85e7439..67c7e68 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -6,6 +6,7 @@ use crate::errors::{ CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, common, }; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -15,7 +16,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct NewThreadCommand; diff --git a/rustmail/src/commands/recover/slash_command/recover.rs b/rustmail/src/commands/recover/slash_command/recover.rs index fa47621..52beb71 100644 --- a/rustmail/src/commands/recover/slash_command/recover.rs +++ b/rustmail/src/commands/recover/slash_command/recover.rs @@ -3,13 +3,13 @@ use crate::config::Config; use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct RecoverCommand; diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index 32cb3db..c74afca 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -3,6 +3,7 @@ use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -12,7 +13,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct RemoveReminderCommand; diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index fa35eb6..4458417 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -6,6 +6,7 @@ use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -15,7 +16,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct RemoveStaffCommand; diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index 65682d2..9277056 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -4,6 +4,7 @@ use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::i18n::get_translated_message; +use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; @@ -15,7 +16,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::watch::Receiver; -use crate::types::logs::PaginationStore; pub struct ReplyCommand; diff --git a/rustmail/src/handlers/guild_interaction_handler.rs b/rustmail/src/handlers/guild_interaction_handler.rs index 5e8acdd..1320dc5 100644 --- a/rustmail/src/handlers/guild_interaction_handler.rs +++ b/rustmail/src/handlers/guild_interaction_handler.rs @@ -76,7 +76,14 @@ impl EventHandler for InteractionHandler { if let Some(handler) = self.registry.get(command.data.name.as_str()) { let result = handler - .run(&ctx, &command, &options, &config, self.shutdown.clone(), pagination) + .run( + &ctx, + &command, + &options, + &config, + self.shutdown.clone(), + pagination, + ) .await; if let Err(e) = result { diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index cc37347..6bb94bb 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -150,8 +150,6 @@ pub async fn handle_thread_modal_interaction( config: &Config, interaction: &mut ModalInteraction, ) -> Result<(), Box> { - - let parts = match parse_thread_interaction(&interaction.data.custom_id) { Some(parts) => parts, None => { From 7611cd909aeb396324861da64f8ed214ab209867 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 08:42:16 +0200 Subject: [PATCH 049/100] refactor(commands): add missing trads for logs slash command --- rustmail/src/commands/logs/common.rs | 23 +++++++++++++++---- .../src/commands/logs/text_command/logs.rs | 2 +- rustmail/src/i18n/language/en.rs | 4 ++++ rustmail/src/i18n/language/fr.rs | 4 ++++ rustmail/src/modules/commands.rs | 8 ++++++- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/rustmail/src/commands/logs/common.rs b/rustmail/src/commands/logs/common.rs index edd1d87..1f21b06 100644 --- a/rustmail/src/commands/logs/common.rs +++ b/rustmail/src/commands/logs/common.rs @@ -1,5 +1,6 @@ use crate::config::Config; use crate::errors::ModmailError; +use crate::i18n::get_translated_message; use crate::types::logs::TicketLog; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ChannelId, CommandInteraction, Message}; @@ -19,22 +20,36 @@ pub fn extract_user_id(msg: &Message, config: &Config) -> String { } } -pub fn render_logs_page(logs: &[TicketLog], page: usize, per_page: usize) -> String { +pub async fn render_logs_page( + config: &Config, + logs: &[TicketLog], + page: usize, + per_page: usize, +) -> String { let total_pages = (logs.len() + per_page - 1) / per_page; let start = page * per_page; let end = usize::min(start + per_page, logs.len()); + let no_logs = get_translated_message( + config, + "slash_commands.no_logs_found", + None, + None, + None, + None, + ) + .await; let mut desc = String::new(); if start >= logs.len() { - return "_Aucun log trouvé pour cet utilisateur._".into(); + return no_logs; } for (_, log) in logs[start..end].iter().enumerate() { use std::fmt::Write; let _ = writeln!( desc, - "**#{}** | [`Ticket {}`]({}) | Fermé le {} {}", + "**#{}** | [`🎫 {}`]({}) | 🔒 {} {}", log.id, log.ticket_id, format!("http://localhost:3002/panel/tickets/{}", log.ticket_id), @@ -48,7 +63,7 @@ pub fn render_logs_page(logs: &[TicketLog], page: usize, per_page: usize) -> Str } format!( - "{}\n_Page {}/{} ({} logs totaux)_", + "{}\n_Page {}/{} ( 🧾 {} )_", desc, page + 1, total_pages.max(1), diff --git a/rustmail/src/commands/logs/text_command/logs.rs b/rustmail/src/commands/logs/text_command/logs.rs index eb2a8b4..aa84214 100644 --- a/rustmail/src/commands/logs/text_command/logs.rs +++ b/rustmail/src/commands/logs/text_command/logs.rs @@ -58,7 +58,7 @@ pub async fn handle_logs_from_user_id( }; let page = 0; - let content = render_logs_page(&logs, page, LOGS_PAGE_SIZE); + let content = render_logs_page(config, &logs, page, LOGS_PAGE_SIZE).await; let session_id = Uuid::new_v4().to_string(); let next_button = diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 352acb2..8d9789a 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -784,4 +784,8 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "slash_commands.logs_id_argument_description".to_string(), DictionaryMessage::new("The ID of the user to view logs for"), ); + dict.messages.insert( + "slash_commands.no_logs_found".to_string(), + DictionaryMessage::new("No logs found for this user."), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index c6bfaf7..04c8690 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -808,4 +808,8 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "slash_commands.logs_id_argument_description".to_string(), DictionaryMessage::new("L'ID de l'utilisateur dont vous souhaitez voir les logs"), ); + dict.messages.insert( + "slash_commands.no_logs_found".to_string(), + DictionaryMessage::new("Aucun log trouvé pour cet utilisateur."), + ); } diff --git a/rustmail/src/modules/commands.rs b/rustmail/src/modules/commands.rs index 38fd02f..216d4fc 100644 --- a/rustmail/src/modules/commands.rs +++ b/rustmail/src/modules/commands.rs @@ -36,7 +36,13 @@ async fn handle_logs_action( return Ok(()); } - let new_content = render_logs_page(&ctx_data.logs, ctx_data.current_page, LOGS_PAGE_SIZE); + let new_content = render_logs_page( + config, + &ctx_data.logs, + ctx_data.current_page, + LOGS_PAGE_SIZE, + ) + .await; if new_content.is_empty() { return Ok(()); From 2755c9c161fc01a2fa4a1542e0481c86087f13fd Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 10:21:12 +0200 Subject: [PATCH 050/100] refactor(config): add option to auto-detect server ip at startup --- rustmail/src/commands/edit/common.rs | 3 ++- rustmail/src/commands/logs/common.rs | 6 +++++- rustmail/src/config.rs | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/rustmail/src/commands/edit/common.rs b/rustmail/src/commands/edit/common.rs index 29bbaaf..c089cd8 100644 --- a/rustmail/src/commands/edit/common.rs +++ b/rustmail/src/commands/edit/common.rs @@ -1,6 +1,6 @@ use crate::config::Config; -use crate::errors::ModmailResult; use crate::errors::common::invalid_command; +use crate::errors::ModmailResult; use crate::utils::command::extract_reply_content::extract_reply_content; use serenity::all::Message; @@ -39,6 +39,7 @@ mod tests { client_id: 123456789012345678, client_secret: "secret".to_string(), redirect_url: "http://localhost:3002/api/auth/callback".to_string(), + ip: Option::from("0.0.0.0".to_string()), }, command: CommandConfig { prefix: "!".to_string(), diff --git a/rustmail/src/commands/logs/common.rs b/rustmail/src/commands/logs/common.rs index 1f21b06..e7ee541 100644 --- a/rustmail/src/commands/logs/common.rs +++ b/rustmail/src/commands/logs/common.rs @@ -52,7 +52,11 @@ pub async fn render_logs_page( "**#{}** | [`🎫 {}`]({}) | 🔒 {} {}", log.id, log.ticket_id, - format!("http://localhost:3002/panel/tickets/{}", log.ticket_id), + format!( + "http://{}:3002/panel/tickets/{}", + &config.bot.ip.clone().unwrap(), + log.ticket_id + ), log.created_at, "\n".to_string(), ); diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index 25b4dbc..062e350 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -5,6 +5,7 @@ use serenity::all::GuildId; use serenity::http::Http; use sqlx::SqlitePool; use std::fs; +use std::net::UdpSocket; use std::sync::Arc; #[derive(Debug, Deserialize, Clone)] @@ -46,6 +47,8 @@ pub struct BotConfig { pub logs_channel_id: Option, #[serde(default)] pub features_channel_id: Option, + #[serde(default)] + pub ip: Option, } #[derive(Debug, Deserialize, Clone)] @@ -114,6 +117,13 @@ pub struct ErrorHandlingConfig { pub error_message_ttl: Option, } +fn get_local_ip() -> Option { + let socket = UdpSocket::bind("0.0.0.0:0").ok()?; + + socket.connect("1.1.1.1:80").ok()?; + Some(socket.local_addr().ok()?.ip().to_string()) +} + pub fn load_config(path: &str) -> Option { let content = match fs::read_to_string(path) { Ok(c) => c, @@ -128,6 +138,10 @@ pub fn load_config(path: &str) -> Option { } }; + if config.bot.ip.is_none() { + config.bot.ip = get_local_ip(); + } + match u64::from_str_radix(&config.thread.user_message_color, 16) { Ok(_) => {} Err(_) => { From 8954019092b3f5b5ba06480010ca370d3fb2f353 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 10:58:27 +0200 Subject: [PATCH 051/100] refactor(thread): add complete user_recap in user and staff ticket --- rustmail/src/commands/edit/common.rs | 2 +- .../new_thread/slash_command/new_thread.rs | 39 ++++++++++++++++++ .../new_thread/text_command/new_thread.rs | 41 +++++++++++++++++++ rustmail/src/i18n/language/en.rs | 6 +++ rustmail/src/i18n/language/fr.rs | 4 ++ rustmail/src/modules/threads.rs | 34 ++++++++++++--- rustmail/src/utils/thread/mod.rs | 1 + rustmail/src/utils/thread/user_recap.rs | 18 ++++++++ 8 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 rustmail/src/utils/thread/user_recap.rs diff --git a/rustmail/src/commands/edit/common.rs b/rustmail/src/commands/edit/common.rs index c089cd8..d5166bd 100644 --- a/rustmail/src/commands/edit/common.rs +++ b/rustmail/src/commands/edit/common.rs @@ -1,6 +1,6 @@ use crate::config::Config; -use crate::errors::common::invalid_command; use crate::errors::ModmailResult; +use crate::errors::common::invalid_command; use crate::utils::command::extract_reply_content::extract_reply_content; use serenity::all::Message; diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index 67c7e68..d8e161c 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -1,6 +1,7 @@ use crate::commands::new_thread::common::send_welcome_message; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; +use crate::db::logs::get_logs_from_user_id; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; use crate::errors::{ CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, common, @@ -9,6 +10,8 @@ use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; +use crate::utils::thread::user_recap::get_user_recap; +use crate::utils::time::get_member_join_date::get_member_join_date_for_user; use serenity::all::{ ChannelId, CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Context, CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup, GuildId, ResolvedOption, @@ -168,6 +171,42 @@ impl RegistrableCommand for NewThreadCommand { } }; + let community_guild_id = GuildId::new(config.bot.get_community_guild_id()); + + let member_join_date = get_member_join_date_for_user(&ctx, user_id, community_guild_id) + .await + .unwrap_or_else(|| "Unknown".to_string()); + + let logs_count = match get_logs_from_user_id(&user_id.clone().to_string(), pool).await { + Ok(logs) => logs.len(), + Err(_) => 0, + }; + + let params = { + let mut p = HashMap::new(); + p.insert("logs_count".to_string(), logs_count.to_string()); + p.insert("prefix".to_string(), config.command.prefix.clone()); + p + }; + + let logs_info = get_translated_message( + &config, + "new_thread.show_logs", + Some(¶ms), + None, + None, + None, + ) + .await; + + let recap = get_user_recap(user_id, &user.name, &member_join_date, &logs_info); + + let _ = MessageBuilder::system_message(&ctx, &config) + .content(recap) + .to_channel(guild_channel.id) + .send(true) + .await; + send_welcome_message(&ctx, &guild_channel, &config, &user).await; let mut params = HashMap::new(); diff --git a/rustmail/src/commands/new_thread/text_command/new_thread.rs b/rustmail/src/commands/new_thread/text_command/new_thread.rs index c027b12..166e1b8 100644 --- a/rustmail/src/commands/new_thread/text_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/text_command/new_thread.rs @@ -3,9 +3,14 @@ use crate::commands::new_thread::common::{ send_welcome_message, }; use crate::config::Config; +use crate::db::logs::get_logs_from_user_id; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; use crate::errors::{DiscordError, ModmailError, ModmailResult, common}; +use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; +use crate::utils::message::message_builder::MessageBuilder; +use crate::utils::thread::user_recap::get_user_recap; +use crate::utils::time::get_member_join_date::get_member_join_date_for_user; use serenity::all::{ChannelId, Context, GuildId, Message}; use std::collections::HashMap; use std::sync::Arc; @@ -80,6 +85,42 @@ pub async fn new_thread( } }; + let community_guild_id = GuildId::new(config.bot.get_community_guild_id()); + + let member_join_date = get_member_join_date_for_user(&ctx, user_id, community_guild_id) + .await + .unwrap_or_else(|| "Unknown".to_string()); + + let logs_count = match get_logs_from_user_id(&user_id.clone().to_string(), pool).await { + Ok(logs) => logs.len(), + Err(_) => 0, + }; + + let params = { + let mut p = HashMap::new(); + p.insert("logs_count".to_string(), logs_count.to_string()); + p.insert("prefix".to_string(), config.command.prefix.clone()); + p + }; + + let logs_info = get_translated_message( + &config, + "new_thread.show_logs", + Some(¶ms), + None, + None, + None, + ) + .await; + + let recap = get_user_recap(user_id, &user.name, &member_join_date, &logs_info); + + let _ = MessageBuilder::system_message(&ctx, &config) + .content(recap) + .to_channel(guild_channel.id) + .send(true) + .await; + let _ = match create_thread_for_user(&guild_channel, user_id.get() as i64, &user.name, pool) .await { diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 8d9789a..e040011 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -788,4 +788,10 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "slash_commands.no_logs_found".to_string(), DictionaryMessage::new("No logs found for this user."), ); + dict.messages.insert( + "new_thread.show_logs".to_string(), + DictionaryMessage::new( + "This user has {count} previous rustmail ticket(s). Use `{prefix}logs` to view them.", + ), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 04c8690..9667fb7 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -812,4 +812,8 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "slash_commands.no_logs_found".to_string(), DictionaryMessage::new("Aucun log trouvé pour cet utilisateur."), ); + dict.messages.insert( + "new_thread.show_logs".to_string(), + DictionaryMessage::new("Cet utilisateur a **{logs_count}** ancien(s) ticket(s) rustmail. Utilisez `{prefix}logs` pour les voir."), + ); } diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index 6bb94bb..8489b9c 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -1,4 +1,5 @@ use crate::config::Config; +use crate::db::logs::get_logs_from_user_id; use crate::db::operations::{create_thread_for_user, get_thread_channel_by_user_id}; use crate::errors::common::validation_failed; use crate::i18n::get_translated_message; @@ -6,7 +7,7 @@ use crate::utils::message::message_builder::MessageBuilder; use crate::utils::message::ui; use crate::utils::thread::get_thread_lock::get_thread_lock; use crate::utils::thread::send_to_thread::send_to_thread; -use crate::utils::time::format_duration_since::format_duration_since; +use crate::utils::thread::user_recap::get_user_recap; use crate::utils::time::get_member_join_date::get_member_join_date_for_user; use serenity::all::{ ActionRowComponent, Channel, ChannelId, ComponentInteraction, Context, CreateChannel, GuildId, @@ -74,12 +75,33 @@ async fn create_or_get_thread_for_user( .await .unwrap_or_else(|| "Unknown".to_string()); - let open_thread_message = format!( - "ACCOUNT AGE **{}**, ID **{}**\nNICKNAME **{}**, JOINED **{}**", - format_duration_since(user_id.created_at()), + let logs_count = match get_logs_from_user_id(&user_id.clone().to_string(), pool).await { + Ok(logs) => logs.len(), + Err(_) => 0, + }; + + let params = { + let mut p = HashMap::new(); + p.insert("logs_count".to_string(), logs_count.to_string()); + p.insert("prefix".to_string(), config.command.prefix.clone()); + p + }; + + let logs_info = get_translated_message( + &config, + "new_thread.show_logs", + Some(¶ms), + None, + None, + None, + ) + .await; + + let open_thread_message = get_user_recap( user_id, - username, - member_join_date + &username.to_string(), + &member_join_date, + &logs_info, ); let _ = MessageBuilder::system_message(ctx, config) diff --git a/rustmail/src/utils/thread/mod.rs b/rustmail/src/utils/thread/mod.rs index c237e83..2ec3a7a 100644 --- a/rustmail/src/utils/thread/mod.rs +++ b/rustmail/src/utils/thread/mod.rs @@ -2,3 +2,4 @@ pub mod category; pub mod fetch_thread; pub mod get_thread_lock; pub mod send_to_thread; +pub mod user_recap; diff --git a/rustmail/src/utils/thread/user_recap.rs b/rustmail/src/utils/thread/user_recap.rs new file mode 100644 index 0000000..d2a6dbe --- /dev/null +++ b/rustmail/src/utils/thread/user_recap.rs @@ -0,0 +1,18 @@ +use crate::utils::time::format_duration_since::format_duration_since; +use serenity::all::UserId; + +pub fn get_user_recap( + user_id: UserId, + username: &str, + member_join_date: &str, + logs_info: &str, +) -> String { + format!( + "ACCOUNT AGE **{}**, ID **{}**\nNICKNAME **{}**, JOINED **{}** ago\n\n{}", + format_duration_since(user_id.created_at()), + user_id, + username, + member_join_date, + logs_info + ) +} From edff1159bfedb618d91d3f630493eccd5003125b Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 11:17:41 +0200 Subject: [PATCH 052/100] fix(thread): make message_number optional to avoid sql conflicts --- rustmail/src/db/operations/messages.rs | 7 +++---- rustmail/src/utils/message/message_builder.rs | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/rustmail/src/db/operations/messages.rs b/rustmail/src/db/operations/messages.rs index 3192ef7..df73937 100644 --- a/rustmail/src/db/operations/messages.rs +++ b/rustmail/src/db/operations/messages.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::db::operations::threads::get_user_name_from_thread_id; -use crate::errors::ModmailResult; use crate::errors::common::message_not_found; +use crate::errors::ModmailResult; use serenity::all::{Message, MessageId, UserId}; use serenity::client::Context; use sqlx::{Error, SqlitePool}; @@ -21,7 +21,7 @@ pub async fn insert_staff_message( is_anonymous: bool, pool: &SqlitePool, config: &Config, - message_number: i64, + message_number: Option, ) -> Result<(), Error> { let inbox_message_id = inbox_msg.id.to_string(); let user_id = staff_user_id.get() as i64; @@ -33,7 +33,6 @@ pub async fn insert_staff_message( .unwrap_or_else(|_| "Unknown".to_string()); let content = extract_message_content(inbox_msg, config); - let message_number_i64 = message_number as i64; sqlx::query!( r#" @@ -49,7 +48,7 @@ pub async fn insert_staff_message( is_anonymous, dm_msg_id, inbox_message_id, - message_number_i64, + message_number, content, 1 ) diff --git a/rustmail/src/utils/message/message_builder.rs b/rustmail/src/utils/message/message_builder.rs index f858109..9d9737a 100644 --- a/rustmail/src/utils/message/message_builder.rs +++ b/rustmail/src/utils/message/message_builder.rs @@ -490,7 +490,7 @@ impl<'a> MessageBuilder<'a> { false, &pool, self.config, - -1, + None, ) .await { @@ -852,7 +852,7 @@ impl<'a> StaffReply<'a> { self.is_anonymous, pool, self.config, - self.message_number as i64, + Some(self.message_number as i64), ) .await { @@ -920,7 +920,7 @@ impl<'a> StaffReply<'a> { self.is_anonymous, pool, self.config, - self.message_number as i64, + Some(self.message_number as i64), ) .await { From 00e6690b11a57e56b07f83c1b6c0e26ce13a65ee Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 11:24:09 +0200 Subject: [PATCH 053/100] refactor(thread): welcome message is now registered in db --- rustmail/src/modules/threads.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index 8489b9c..c40c137 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -107,13 +107,13 @@ async fn create_or_get_thread_for_user( let _ = MessageBuilder::system_message(ctx, config) .to_channel(target_channel_id) .content(open_thread_message) - .send(false) + .send(true) .await; let _ = MessageBuilder::system_message(ctx, config) .content(&config.bot.welcome_message) .to_user(user_id) - .send(false) + .send(true) .await; println!("Thread created successfully"); From 117b2c997247e66d45bc97869c467cbde0ca5533 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 11:29:45 +0200 Subject: [PATCH 054/100] =?UTF-8?q?refactor(commands):=20remove=20add=5Fst?= =?UTF-8?q?aff=20user=20context=20since=20it=E2=80=99s=20useless=20when=20?= =?UTF-8?q?the=20target=20staff=20is=20not=20visible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rustmail/src/commands/add_staff/slash_command/add_staff.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index 2560b5f..508a0b3 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -4,13 +4,13 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ - CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Context, + CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; @@ -55,7 +55,6 @@ impl RegistrableCommand for AddStaffCommand { CreateCommandOption::new(CommandOptionType::User, "user_id", user_id_desc) .required(true), ), - CreateCommand::new("add_staff").kind(CommandType::User), ] }) } From 18caef296b5e82374ddd49b2abfdb1d19071ee5c Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 13:30:03 +0200 Subject: [PATCH 055/100] feat(commands): enable new_thread context on both staff and community servers --- .../slash_command/add_reminder.rs | 4 ++-- .../add_staff/slash_command/add_staff.rs | 4 ++-- .../src/commands/alert/slash_command/alert.rs | 2 +- .../src/commands/close/slash_command/close.rs | 2 +- .../commands/delete/slash_command/delete.rs | 2 +- .../src/commands/edit/slash_command/edit.rs | 2 +- .../force_close/slash_command/force_close.rs | 2 +- rustmail/src/commands/mod.rs | 11 +++++++++- .../new_thread/slash_command/new_thread.rs | 14 ++++++++++-- .../commands/recover/slash_command/recover.rs | 2 +- .../slash_command/remove_reminder.rs | 2 +- .../slash_command/remove_staff.rs | 2 +- .../src/commands/reply/slash_command/reply.rs | 4 ++-- rustmail/src/handlers/ready_handler.rs | 22 +++++++++++++++---- 14 files changed, 54 insertions(+), 21 deletions(-) diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index 67228a7..eed594d 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -3,10 +3,10 @@ use crate::commands::add_reminder::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::db::reminders::{Reminder, insert_reminder}; +use crate::db::reminders::{insert_reminder, Reminder}; use crate::db::threads::get_thread_by_user_id; use crate::errors::{ - CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, common, + common, CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, }; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index 508a0b3..dd339bd 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -10,8 +10,8 @@ use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ - CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, - CreateCommand, CreateCommandOption, ResolvedOption, + CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, + CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; diff --git a/rustmail/src/commands/alert/slash_command/alert.rs b/rustmail/src/commands/alert/slash_command/alert.rs index f4a0536..a95f8ba 100644 --- a/rustmail/src/commands/alert/slash_command/alert.rs +++ b/rustmail/src/commands/alert/slash_command/alert.rs @@ -4,7 +4,7 @@ use crate::commands::alert::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index 25eaa93..ab9fa04 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::category::{ diff --git a/rustmail/src/commands/delete/slash_command/delete.rs b/rustmail/src/commands/delete/slash_command/delete.rs index 5b4bf26..764fac8 100644 --- a/rustmail/src/commands/delete/slash_command/delete.rs +++ b/rustmail/src/commands/delete/slash_command/delete.rs @@ -5,7 +5,7 @@ use crate::commands::delete::common::{ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::messages::get_thread_message_by_message_id; -use crate::errors::{MessageError, ModmailError, ModmailResult, common}; +use crate::errors::{common, MessageError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response_ephemeral; diff --git a/rustmail/src/commands/edit/slash_command/edit.rs b/rustmail/src/commands/edit/slash_command/edit.rs index 1195206..8da890b 100644 --- a/rustmail/src/commands/edit/slash_command/edit.rs +++ b/rustmail/src/commands/edit/slash_command/edit.rs @@ -4,7 +4,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::{get_thread_message_by_inbox_message_id, update_message_content}; use crate::errors::common::message_not_found; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/force_close/slash_command/force_close.rs b/rustmail/src/commands/force_close/slash_command/force_close.rs index c3cb460..3708746 100644 --- a/rustmail/src/commands/force_close/slash_command/force_close.rs +++ b/rustmail/src/commands/force_close/slash_command/force_close.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index c954794..8626516 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -2,6 +2,7 @@ use crate::config::Config; use crate::errors::ModmailResult; use crate::types::logs::PaginationStore; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; +use std::any::Any; use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; @@ -27,7 +28,11 @@ pub mod reply; pub type BoxFuture = Pin + Send>>; -pub trait RegistrableCommand: Send + Sync { +pub trait RegistrableCommand: Any + Send + Sync { + fn as_community(&self) -> Option<&dyn CommunityRegistrable> { + None + } + fn name(&self) -> &'static str; fn register(&self, config: &Config) -> BoxFuture>; @@ -43,6 +48,10 @@ pub trait RegistrableCommand: Send + Sync { ) -> BoxFuture>; } +pub trait CommunityRegistrable: RegistrableCommand { + fn register_community(&self, config: &Config) -> BoxFuture>; +} + pub struct CommandRegistry { commands: HashMap<&'static str, Arc>, _shutdown: Arc>, diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index d8e161c..5bf4bfe 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -1,10 +1,10 @@ use crate::commands::new_thread::common::send_welcome_message; -use crate::commands::{BoxFuture, RegistrableCommand}; +use crate::commands::{BoxFuture, CommunityRegistrable, RegistrableCommand}; use crate::config::Config; use crate::db::logs::get_logs_from_user_id; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; use crate::errors::{ - CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, common, + common, CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, }; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; @@ -24,6 +24,10 @@ pub struct NewThreadCommand; #[async_trait::async_trait] impl RegistrableCommand for NewThreadCommand { + fn as_community(&self) -> Option<&dyn CommunityRegistrable> { + Some(self) + } + fn name(&self) -> &'static str { "new_thread" } @@ -235,3 +239,9 @@ impl RegistrableCommand for NewThreadCommand { }) } } + +impl CommunityRegistrable for NewThreadCommand { + fn register_community(&self, _config: &Config) -> BoxFuture> { + Box::pin(async move { vec![CreateCommand::new("new_thread").kind(CommandType::User)] }) + } +} diff --git a/rustmail/src/commands/recover/slash_command/recover.rs b/rustmail/src/commands/recover/slash_command/recover.rs index 52beb71..ae2d5cf 100644 --- a/rustmail/src/commands/recover/slash_command/recover.rs +++ b/rustmail/src/commands/recover/slash_command/recover.rs @@ -1,6 +1,6 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; use crate::types::logs::PaginationStore; diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index c74afca..1d211e2 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -1,7 +1,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index 4458417..69e3020 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index 9277056..09f5d4b 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -2,12 +2,12 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{ Attachment, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, diff --git a/rustmail/src/handlers/ready_handler.rs b/rustmail/src/handlers/ready_handler.rs index 4f989e0..291ab2c 100644 --- a/rustmail/src/handlers/ready_handler.rs +++ b/rustmail/src/handlers/ready_handler.rs @@ -1,4 +1,4 @@ -use crate::commands::CommandRegistry; +use crate::commands::{CommandRegistry, CommunityRegistrable}; use crate::config::Config; use crate::features::sync_features; use crate::modules::message_recovery::{recover_missing_messages, send_recovery_summary}; @@ -61,15 +61,29 @@ impl EventHandler for ReadyHandler { load_reminders(&ctx, &self.config, pool, self.shutdown.clone()).await; let guild_id = GuildId::new(self.config.bot.get_staff_guild_id()); + let guild_id2 = GuildId::new(self.config.bot.get_community_guild_id()); - let mut commands: Vec = Vec::new(); + let mut guild_commands: Vec = Vec::new(); + let mut community_commands: Vec = Vec::new(); for command in self.registry.all() { let mut cmds = command.register(&self.config).await; - commands.append(&mut cmds); + guild_commands.append(&mut cmds); + + if let Some(commu) = command.as_community() { + let mut commu_cmds = commu.register_community(&self.config).await; + community_commands.append(&mut commu_cmds); + } + } + + if let Err(e) = guild_id + .set_commands(&ctx.http, guild_commands.clone()) + .await + { + eprintln!("set_commands() failed: {:?}", e); } - if let Err(e) = guild_id.set_commands(&ctx.http, commands).await { + if let Err(e) = guild_id2.set_commands(&ctx.http, community_commands).await { eprintln!("set_commands() failed: {:?}", e); } } From 783cc9d071b2530f5681c9807d9e41c674a54d76 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 13:52:31 +0200 Subject: [PATCH 056/100] fix(add_reminder): reminder can now be set for the day after --- .../src/commands/add_reminder/slash_command/add_reminder.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index eed594d..03aa0ca 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -157,10 +157,10 @@ impl RegistrableCommand for AddReminderCommand { .unwrap_or(0); let time = NaiveTime::from_hms_opt(hours, minutes, 0).unwrap(); - let now = Local::now(); - let mut trigger_dt = now.date_naive().and_time(time); + let now = Local::now().naive_local(); + let mut trigger_dt = now.date().and_time(time); - if trigger_dt < now.date_naive().and_time(time) { + if trigger_dt < now { trigger_dt += chrono::Duration::days(1); } From eff84b4ef3c2d51b419694fc7624bb3ea105421d Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 23 Oct 2025 15:00:33 +0200 Subject: [PATCH 057/100] fix(reminder): add error message when trying to set the same reminder twice --- Cargo.lock | 35 +++++++++++++++++++ rustmail/Cargo.toml | 1 + .../slash_command/add_reminder.rs | 23 ++++++------ .../add_reminder/text_command/add_reminder.rs | 23 ++++++------ .../add_staff/slash_command/add_staff.rs | 2 +- .../src/commands/alert/slash_command/alert.rs | 2 +- .../src/commands/close/slash_command/close.rs | 2 +- .../commands/delete/slash_command/delete.rs | 2 +- rustmail/src/commands/edit/common.rs | 1 + .../src/commands/edit/slash_command/edit.rs | 2 +- .../force_close/slash_command/force_close.rs | 2 +- .../new_thread/slash_command/new_thread.rs | 2 +- .../commands/recover/slash_command/recover.rs | 2 +- .../slash_command/remove_reminder.rs | 2 +- .../slash_command/remove_staff.rs | 2 +- .../src/commands/reply/slash_command/reply.rs | 4 +-- rustmail/src/config.rs | 20 ++++++++++- rustmail/src/db/operations/messages.rs | 2 +- rustmail/src/db/operations/reminders.rs | 26 +++++++++++--- rustmail/src/errors/dictionary.rs | 3 ++ rustmail/src/errors/types.rs | 2 ++ rustmail/src/i18n/language/en.rs | 4 +++ rustmail/src/i18n/language/fr.rs | 4 +++ 23 files changed, 128 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ec4f8b..af1252b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -319,6 +319,16 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + [[package]] name = "command_attr" version = "0.5.3" @@ -2100,6 +2110,24 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -2652,6 +2680,7 @@ dependencies = [ "axum-extra", "base64 0.22.1", "chrono", + "chrono-tz", "hyper 1.7.0", "mime_guess", "rand", @@ -2966,6 +2995,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "skeptic" version = "0.13.7" diff --git a/rustmail/Cargo.toml b/rustmail/Cargo.toml index 539ccc9..de680c0 100644 --- a/rustmail/Cargo.toml +++ b/rustmail/Cargo.toml @@ -24,6 +24,7 @@ serde_json = "1.0.145" rand = "0.8.5" base64 = "0.22.1" subtle = "2.6.1" +chrono-tz = "0.10.4" [dependencies.uuid] version = "1.18.1" diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index 03aa0ca..935d298 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -3,15 +3,13 @@ use crate::commands::add_reminder::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::db::reminders::{insert_reminder, Reminder}; +use crate::db::reminders::{Reminder, insert_reminder}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{ - common, CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, -}; +use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; -use chrono::{Local, NaiveTime}; +use chrono::{Local, NaiveTime, TimeZone}; use regex::Regex; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, @@ -157,14 +155,19 @@ impl RegistrableCommand for AddReminderCommand { .unwrap_or(0); let time = NaiveTime::from_hms_opt(hours, minutes, 0).unwrap(); - let now = Local::now().naive_local(); - let mut trigger_dt = now.date().and_time(time); + let now = Local::now().with_timezone(&config.bot.timezone); + + let mut trigger_dt = config + .bot + .timezone + .from_local_datetime(&now.date_naive().and_time(time)) + .unwrap(); if trigger_dt < now { trigger_dt += chrono::Duration::days(1); } - let trigger_timestamp = trigger_dt.and_local_timezone(Local).unwrap().timestamp(); + let trigger_timestamp = trigger_dt.with_timezone(&config.bot.timezone).timestamp(); let thread = match get_thread_by_user_id(command.user.id, pool).await { Some(t) => t, @@ -188,9 +191,7 @@ impl RegistrableCommand for AddReminderCommand { Ok(id) => id, Err(e) => { eprintln!("Failed to insert reminder: {}", e); - return Err(ModmailError::Database(DatabaseError::InsertFailed( - e.to_string(), - ))); + return Err(e); } }; diff --git a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs index a418539..0bb1b97 100644 --- a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs @@ -4,12 +4,10 @@ use crate::commands::add_reminder::common::{ use crate::config::Config; use crate::db::reminders::{Reminder, insert_reminder}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{ - CommandError, DatabaseError, ModmailError, ModmailResult, ThreadError, common, -}; +use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; -use chrono::{Local, NaiveTime}; +use chrono::{Local, NaiveTime, TimeZone}; use regex::Regex; use serenity::all::{Context, Message}; use std::sync::Arc; @@ -60,14 +58,19 @@ pub async fn add_reminder( .unwrap_or(0); let time = NaiveTime::from_hms_opt(hours, minutes, 0).unwrap(); - let now = Local::now(); - let mut trigger_dt = now.date_naive().and_time(time); + let now = Local::now().with_timezone(&config.bot.timezone); + + let mut trigger_dt = config + .bot + .timezone + .from_local_datetime(&now.date_naive().and_time(time)) + .unwrap(); - if trigger_dt < now.date_naive().and_time(time) { + if trigger_dt < now { trigger_dt += chrono::Duration::days(1); } - let trigger_timestamp = trigger_dt.and_local_timezone(Local).unwrap().timestamp(); + let trigger_timestamp = trigger_dt.with_timezone(&config.bot.timezone).timestamp(); let thread = match get_thread_by_user_id(msg.author.id, pool).await { Some(t) => t, @@ -91,9 +94,7 @@ pub async fn add_reminder( Ok(id) => id, Err(e) => { eprintln!("Failed to insert reminder: {}", e); - return Err(ModmailError::Database(DatabaseError::InsertFailed( - e.to_string(), - ))); + return Err(e); } }; diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index dd339bd..b34c4b8 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/alert/slash_command/alert.rs b/rustmail/src/commands/alert/slash_command/alert.rs index a95f8ba..f4a0536 100644 --- a/rustmail/src/commands/alert/slash_command/alert.rs +++ b/rustmail/src/commands/alert/slash_command/alert.rs @@ -4,7 +4,7 @@ use crate::commands::alert::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index ab9fa04..25eaa93 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::category::{ diff --git a/rustmail/src/commands/delete/slash_command/delete.rs b/rustmail/src/commands/delete/slash_command/delete.rs index 764fac8..5b4bf26 100644 --- a/rustmail/src/commands/delete/slash_command/delete.rs +++ b/rustmail/src/commands/delete/slash_command/delete.rs @@ -5,7 +5,7 @@ use crate::commands::delete::common::{ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::messages::get_thread_message_by_message_id; -use crate::errors::{common, MessageError, ModmailError, ModmailResult}; +use crate::errors::{MessageError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response_ephemeral; diff --git a/rustmail/src/commands/edit/common.rs b/rustmail/src/commands/edit/common.rs index d5166bd..8711b29 100644 --- a/rustmail/src/commands/edit/common.rs +++ b/rustmail/src/commands/edit/common.rs @@ -40,6 +40,7 @@ mod tests { client_secret: "secret".to_string(), redirect_url: "http://localhost:3002/api/auth/callback".to_string(), ip: Option::from("0.0.0.0".to_string()), + timezone: "Europe/Paris".to_string().parse().unwrap(), }, command: CommandConfig { prefix: "!".to_string(), diff --git a/rustmail/src/commands/edit/slash_command/edit.rs b/rustmail/src/commands/edit/slash_command/edit.rs index 8da890b..1195206 100644 --- a/rustmail/src/commands/edit/slash_command/edit.rs +++ b/rustmail/src/commands/edit/slash_command/edit.rs @@ -4,7 +4,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::{get_thread_message_by_inbox_message_id, update_message_content}; use crate::errors::common::message_not_found; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/force_close/slash_command/force_close.rs b/rustmail/src/commands/force_close/slash_command/force_close.rs index 3708746..c3cb460 100644 --- a/rustmail/src/commands/force_close/slash_command/force_close.rs +++ b/rustmail/src/commands/force_close/slash_command/force_close.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index 5bf4bfe..c96a778 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::logs::get_logs_from_user_id; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; use crate::errors::{ - common, CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, + CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, common, }; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; diff --git a/rustmail/src/commands/recover/slash_command/recover.rs b/rustmail/src/commands/recover/slash_command/recover.rs index ae2d5cf..52beb71 100644 --- a/rustmail/src/commands/recover/slash_command/recover.rs +++ b/rustmail/src/commands/recover/slash_command/recover.rs @@ -1,6 +1,6 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; use crate::types::logs::PaginationStore; diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index 1d211e2..c74afca 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -1,7 +1,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index 69e3020..4458417 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index 09f5d4b..9277056 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -2,12 +2,12 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; +use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{ Attachment, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index 062e350..403b139 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -1,5 +1,6 @@ use crate::errors::handler::ErrorHandler; use crate::i18n::languages::Language; +use chrono_tz::Tz; use serde::Deserialize; use serenity::all::GuildId; use serenity::http::Http; @@ -42,7 +43,11 @@ pub struct BotConfig { pub client_id: u64, pub client_secret: String, pub redirect_url: String, - + #[serde( + default = "default_timezone", + deserialize_with = "deserialize_timezone" + )] + pub timezone: Tz, #[serde(default)] pub logs_channel_id: Option, #[serde(default)] @@ -51,6 +56,19 @@ pub struct BotConfig { pub ip: Option, } +fn deserialize_timezone<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + s.parse::().map_err(serde::de::Error::custom) +} + +fn default_timezone() -> Tz { + println!("yes"); + chrono_tz::UTC +} + #[derive(Debug, Deserialize, Clone)] #[serde(tag = "type", rename_all = "lowercase")] pub enum ServerMode { diff --git a/rustmail/src/db/operations/messages.rs b/rustmail/src/db/operations/messages.rs index df73937..64b5774 100644 --- a/rustmail/src/db/operations/messages.rs +++ b/rustmail/src/db/operations/messages.rs @@ -1,7 +1,7 @@ use crate::config::Config; use crate::db::operations::threads::get_user_name_from_thread_id; -use crate::errors::common::message_not_found; use crate::errors::ModmailResult; +use crate::errors::common::message_not_found; use serenity::all::{Message, MessageId, UserId}; use serenity::client::Context; use sqlx::{Error, SqlitePool}; diff --git a/rustmail/src/db/operations/reminders.rs b/rustmail/src/db/operations/reminders.rs index 12d427d..dc3cebf 100644 --- a/rustmail/src/db/operations/reminders.rs +++ b/rustmail/src/db/operations/reminders.rs @@ -1,3 +1,8 @@ +use crate::errors::CommandError::ReminderAlreadyExists; +use crate::errors::{ModmailError, ModmailResult}; +use sqlx::Error::Database; +use sqlx::error::ErrorKind::UniqueViolation; + #[derive(Debug, Clone)] pub struct Reminder { pub thread_id: String, @@ -10,14 +15,27 @@ pub struct Reminder { pub completed: bool, } -pub async fn insert_reminder( - reminder: &Reminder, - pool: &sqlx::SqlitePool, -) -> Result { +pub async fn insert_reminder(reminder: &Reminder, pool: &sqlx::SqlitePool) -> ModmailResult { let user_id = reminder.user_id; let channel_id = &reminder.channel_id; let guild_id = &reminder.guild_id; + let existing = sqlx::query_scalar!( + r#" + SELECT id FROM reminders + WHERE user_id = ? AND guild_id = ? AND trigger_time = ? AND completed = 0 + "#, + reminder.user_id, + reminder.guild_id, + reminder.trigger_time + ) + .fetch_optional(pool) + .await?; + + if let Some(_existging_id) = existing { + return Err(ModmailError::Command(ReminderAlreadyExists)); + } + let result = sqlx::query!( r#" INSERT INTO reminders (thread_id, user_id, channel_id, guild_id, reminder_content, trigger_time, created_at, completed) diff --git a/rustmail/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs index cbe21dd..e779e91 100644 --- a/rustmail/src/errors/dictionary.rs +++ b/rustmail/src/errors/dictionary.rs @@ -241,6 +241,9 @@ impl DictionaryManager { CommandError::DiscordDeleteFailed => { ("command.discord_delete_failed".to_string(), None) } + CommandError::ReminderAlreadyExists => { + ("reminder.reminder_already_exists".to_string(), None) + } _ => ("command.invalid_format".to_string(), None), }, ModmailError::Thread(thread_err) => match thread_err { diff --git a/rustmail/src/errors/types.rs b/rustmail/src/errors/types.rs index 1843ba0..ec7054c 100644 --- a/rustmail/src/errors/types.rs +++ b/rustmail/src/errors/types.rs @@ -76,6 +76,7 @@ pub enum CommandError { NoSchedulableClosureToCancel, SendDmFailed, DiscordDeleteFailed, + ReminderAlreadyExists, } #[derive(Debug, Clone)] @@ -228,6 +229,7 @@ impl fmt::Display for CommandError { } CommandError::SendDmFailed => write!(f, "Failed to send DM to user"), CommandError::DiscordDeleteFailed => write!(f, "Failed to delete message on Discord"), + CommandError::ReminderAlreadyExists => write!(f, "A reminder already exists"), } } } diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index e040011..ae43397 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -794,4 +794,8 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "This user has {count} previous rustmail ticket(s). Use `{prefix}logs` to view them.", ), ); + dict.messages.insert( + "reminder.reminder_already_exists".to_string(), + DictionaryMessage::new("Vous avez déjà un rappel programmé à cette heure."), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 9667fb7..b1ca780 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -816,4 +816,8 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "new_thread.show_logs".to_string(), DictionaryMessage::new("Cet utilisateur a **{logs_count}** ancien(s) ticket(s) rustmail. Utilisez `{prefix}logs` pour les voir."), ); + dict.messages.insert( + "reminder.reminder_already_exists".to_string(), + DictionaryMessage::new("Vous avez déjà un rappel programmé à cette heure."), + ); } From 8fc3864b016d5c9f784ace610f443e37ca7b11ca Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 07:54:05 +0200 Subject: [PATCH 058/100] refactor(db): add missing sqlx files --- ...4dced4343f955a6361389fd36c932fe249b2d.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .sqlx/query-dee8be2076aabbf5c6895c2b0824dced4343f955a6361389fd36c932fe249b2d.json diff --git a/.sqlx/query-dee8be2076aabbf5c6895c2b0824dced4343f955a6361389fd36c932fe249b2d.json b/.sqlx/query-dee8be2076aabbf5c6895c2b0824dced4343f955a6361389fd36c932fe249b2d.json new file mode 100644 index 0000000..9b916e0 --- /dev/null +++ b/.sqlx/query-dee8be2076aabbf5c6895c2b0824dced4343f955a6361389fd36c932fe249b2d.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT id FROM reminders\n WHERE user_id = ? AND guild_id = ? AND trigger_time = ? AND completed = 0\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "dee8be2076aabbf5c6895c2b0824dced4343f955a6361389fd36c932fe249b2d" +} From 816b57370900a7c6bc8b78212a27e852db533680 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 08:45:52 +0200 Subject: [PATCH 059/100] refactor(commands): rename 'add_reminder' command to 'remind' and 'rem' --- .../slash_command/add_reminder.rs | 6 ++--- .../add_reminder/text_command/add_reminder.rs | 25 ++++++++----------- .../src/handlers/guild_messages_handler.rs | 4 +-- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index 935d298..5173a25 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -3,9 +3,9 @@ use crate::commands::add_reminder::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::db::reminders::{Reminder, insert_reminder}; +use crate::db::reminders::{insert_reminder, Reminder}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; @@ -23,7 +23,7 @@ pub struct AddReminderCommand; #[async_trait::async_trait] impl RegistrableCommand for AddReminderCommand { fn name(&self) -> &'static str { - "add_reminder" + "remind" } fn register(&self, config: &Config) -> BoxFuture> { diff --git a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs index 0bb1b97..1b6ba6f 100644 --- a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs @@ -2,9 +2,9 @@ use crate::commands::add_reminder::common::{ send_register_confirmation_from_message, spawn_reminder, }; use crate::config::Config; -use crate::db::reminders::{Reminder, insert_reminder}; +use crate::db::reminders::{insert_reminder, Reminder}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use chrono::{Local, NaiveTime, TimeZone}; @@ -25,18 +25,15 @@ pub async fn add_reminder( .as_ref() .ok_or_else(common::database_connection_failed)?; - let content = match extract_reply_content( - &msg.content, - &config.command.prefix, - &["add_reminder", "add_rap"], - ) { - Some(c) => c, - None => { - return Err(ModmailError::Command(CommandError::InvalidArguments( - "No content provided".to_string(), - ))); - } - }; + let content = + match extract_reply_content(&msg.content, &config.command.prefix, &["remind", "rem"]) { + Some(c) => c, + None => { + return Err(ModmailError::Command(CommandError::InvalidArguments( + "No content provided".to_string(), + ))); + } + }; let mut parts = content.splitn(2, ' '); let duration_str = parts.next().unwrap_or(""); diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 52aef7a..2321903 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -24,7 +24,7 @@ use crate::db::operations::{ }; use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; @@ -85,7 +85,7 @@ impl GuildMessagesHandler { wrap_command!(h.commands, ["remove_staff", "rs"], remove_staff); wrap_command!(h.commands, "id", id); wrap_command!(h.commands, "help", help); - wrap_command!(h.commands, ["add_reminder", "add_rap"], add_reminder); + wrap_command!(h.commands, ["remind", "rem"], add_reminder); wrap_command!(h.commands, ["remove_reminder", "rr"], remove_reminder); wrap_command!(h.commands, "logs", logs); h From 5fe87e0f30a1d7aa0df341ef90e7d15a6b922932 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 08:57:57 +0200 Subject: [PATCH 060/100] refactor(commands): rename 'add_staff' command to 'addmod' and 'am' --- rustmail/src/commands/add_staff/common.rs | 8 ++++---- .../commands/add_staff/slash_command/add_staff.rs | 15 +++++++-------- rustmail/src/handlers/guild_messages_handler.rs | 2 +- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/rustmail/src/commands/add_staff/common.rs b/rustmail/src/commands/add_staff/common.rs index 2bdda8b..95aeb25 100644 --- a/rustmail/src/commands/add_staff/common.rs +++ b/rustmail/src/commands/add_staff/common.rs @@ -29,13 +29,13 @@ pub async fn add_user_to_channel( pub async fn extract_user_id(msg: &Message, config: &Config) -> String { let content = msg.content.trim(); let prefix = &config.command.prefix; - let command_names = ["add_staff", "as"]; + let command_names = ["addmod", "am"]; - if command_names + if let Some(matched_name) = command_names .iter() - .any(|&name| content.starts_with(&format!("{}{}", prefix, name))) + .find(|&name| content.starts_with(&format!("{}{}", prefix, name))) { - let start = prefix.len() + command_names[0].len(); + let start = prefix.len() + matched_name.len(); content[start..].trim().to_string() } else { String::new() diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index b34c4b8..e8610f4 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; @@ -21,11 +21,12 @@ pub struct AddStaffCommand; impl RegistrableCommand for AddStaffCommand { fn name(&self) -> &'static str { - "add_staff" + "addmod" } fn register(&self, config: &Config) -> BoxFuture> { let config = config.clone(); + let name = self.name(); Box::pin(async move { let cmd_desc = get_translated_message( @@ -49,12 +50,10 @@ impl RegistrableCommand for AddStaffCommand { .await; vec![ - CreateCommand::new("add_staff") - .description(cmd_desc) - .add_option( - CreateCommandOption::new(CommandOptionType::User, "user_id", user_id_desc) - .required(true), - ), + CreateCommand::new(name).description(cmd_desc).add_option( + CreateCommandOption::new(CommandOptionType::User, "user_id", user_id_desc) + .required(true), + ), ] }) } diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 2321903..0cae734 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -81,7 +81,7 @@ impl GuildMessagesHandler { wrap_command!(h.commands, "delete", delete); wrap_command!(h.commands, ["anonreply", "ar"], anonreply); wrap_command!(h.commands, ["force_close", "fc"], force_close); - wrap_command!(h.commands, ["add_staff", "as"], add_staff); + wrap_command!(h.commands, ["addmod", "am"], add_staff); wrap_command!(h.commands, ["remove_staff", "rs"], remove_staff); wrap_command!(h.commands, "id", id); wrap_command!(h.commands, "help", help); From 6a4eceaba106030bc823c63fa4f24ff6001d6243 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 09:03:40 +0200 Subject: [PATCH 061/100] refactor(commands): rename 'remove_reminder' command to 'unremind' and 'urem' --- .../slash_command/remove_reminder.rs | 4 ++-- .../text_command/remove_reminder.rs | 23 ++++++++----------- .../src/handlers/guild_messages_handler.rs | 2 +- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index c74afca..335bda8 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -1,7 +1,7 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; @@ -18,7 +18,7 @@ pub struct RemoveReminderCommand; impl RegistrableCommand for RemoveReminderCommand { fn name(&self) -> &'static str { - "remove_reminder" + "unremind" } fn register(&self, config: &Config) -> BoxFuture> { diff --git a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs index 0640d26..6f1836f 100644 --- a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; use crate::types::logs::PaginationStore; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; @@ -21,18 +21,15 @@ pub async fn remove_reminder( .as_ref() .ok_or_else(common::database_connection_failed)?; - let content = match extract_reply_content( - &msg.content, - &config.command.prefix, - &["remove_reminder", "rr"], - ) { - Some(c) => c, - None => { - return Err(ModmailError::Command(CommandError::InvalidArguments( - "".to_string(), - ))); - } - }; + let content = + match extract_reply_content(&msg.content, &config.command.prefix, &["unremind", "urem"]) { + Some(c) => c, + None => { + return Err(ModmailError::Command(CommandError::InvalidArguments( + "".to_string(), + ))); + } + }; let reminder_id = match content.parse::() { Ok(id) => id, diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 0cae734..c3d3755 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -86,7 +86,7 @@ impl GuildMessagesHandler { wrap_command!(h.commands, "id", id); wrap_command!(h.commands, "help", help); wrap_command!(h.commands, ["remind", "rem"], add_reminder); - wrap_command!(h.commands, ["remove_reminder", "rr"], remove_reminder); + wrap_command!(h.commands, ["unremind", "urem"], remove_reminder); wrap_command!(h.commands, "logs", logs); h } From e1745562ac7ed2cc4240eebf3a8990f61067d8ad Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 09:08:34 +0200 Subject: [PATCH 062/100] refactor(commands): rename 'remove_staff' command to 'delmod' and 'dm' --- rustmail/src/commands/remove_staff/common.rs | 8 ++++---- .../remove_staff/slash_command/remove_staff.rs | 17 ++++++++--------- rustmail/src/handlers/guild_messages_handler.rs | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/rustmail/src/commands/remove_staff/common.rs b/rustmail/src/commands/remove_staff/common.rs index d89937e..f280334 100644 --- a/rustmail/src/commands/remove_staff/common.rs +++ b/rustmail/src/commands/remove_staff/common.rs @@ -28,13 +28,13 @@ pub async fn remove_user_from_channel( pub async fn extract_user_id(msg: &Message, config: &Config) -> String { let content = msg.content.trim(); let prefix = &config.command.prefix; - let command_names = ["remove_staff", "rs"]; + let command_names = ["delmod", "dm"]; - if command_names + if let Some(matched_name) = command_names .iter() - .any(|&name| content.starts_with(&format!("{}{}", prefix, name))) + .find(|&name| content.starts_with(&format!("{}{}", prefix, name))) { - let start = prefix.len() + command_names[0].len(); + let start = prefix.len() + matched_name.len(); content[start..].trim().to_string() } else { String::new() diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index 4458417..242b2f1 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; @@ -22,11 +22,12 @@ pub struct RemoveStaffCommand; #[async_trait::async_trait] impl RegistrableCommand for RemoveStaffCommand { fn name(&self) -> &'static str { - "remove_staff" + "delmod" } fn register(&self, config: &Config) -> BoxFuture> { let config = config.clone(); + let name = self.name(); Box::pin(async move { let cmd_desc = get_translated_message( @@ -50,13 +51,11 @@ impl RegistrableCommand for RemoveStaffCommand { .await; vec![ - CreateCommand::new("remove_staff") - .description(cmd_desc) - .add_option( - CreateCommandOption::new(CommandOptionType::User, "user_id", user_id_desc) - .required(true), - ), - CreateCommand::new("remove_staff").kind(CommandType::User), + CreateCommand::new(name).description(cmd_desc).add_option( + CreateCommandOption::new(CommandOptionType::User, "user_id", user_id_desc) + .required(true), + ), + CreateCommand::new(name).kind(CommandType::User), ] }) } diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index c3d3755..1e07b71 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -82,7 +82,7 @@ impl GuildMessagesHandler { wrap_command!(h.commands, ["anonreply", "ar"], anonreply); wrap_command!(h.commands, ["force_close", "fc"], force_close); wrap_command!(h.commands, ["addmod", "am"], add_staff); - wrap_command!(h.commands, ["remove_staff", "rs"], remove_staff); + wrap_command!(h.commands, ["delmod", "dm"], remove_staff); wrap_command!(h.commands, "id", id); wrap_command!(h.commands, "help", help); wrap_command!(h.commands, ["remind", "rem"], add_reminder); From e3e7a080a5ee4dc707a4c73d3eb67d996a1a3385 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 09:34:42 +0200 Subject: [PATCH 063/100] refactor(move_command): use channel option instead of string for 'category' --- .../move_thread/slash_command/move_thread.rs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/rustmail/src/commands/move_thread/slash_command/move_thread.rs b/rustmail/src/commands/move_thread/slash_command/move_thread.rs index 321326e..b4fbf61 100644 --- a/rustmail/src/commands/move_thread/slash_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/slash_command/move_thread.rs @@ -12,8 +12,8 @@ use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ - CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, - ResolvedOption, + CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, + CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; @@ -53,7 +53,7 @@ impl RegistrableCommand for MoveCommand { vec![ CreateCommand::new("move").description(cmd_desc).add_option( CreateCommandOption::new( - CommandOptionType::String, + CommandOptionType::Channel, "category", catagory_field_desc, ) @@ -93,19 +93,23 @@ impl RegistrableCommand for MoveCommand { return Err(ModmailError::Command(CommandError::NotInThread())); } - let category_name = match command + let category_option = command .data .options .iter() .find(|opt| opt.name == "category") - { - Some(opt) => match &opt.value { - serenity::all::CommandDataOptionValue::String(name) => name.trim().to_string(), - _ => String::new(), - }, - None => String::new(), + .ok_or(ModmailError::Command(CommandError::MissingArguments))?; + + let category_channel = match &category_option.value { + CommandDataOptionValue::Channel(category) => category, + _ => return Err(ModmailError::Command(CommandError::MissingArguments)), }; + let category_name = category_channel + .name(&ctx.http) + .await + .map_err(|_| ModmailError::Command(CommandError::MissingArguments))?; + if category_name.is_empty() { return Err(ModmailError::Thread(ThreadError::CategoryNotFound)); } From 76cf032ab1e3ee5d093710c32875a87eb17d290e Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 09:59:55 +0200 Subject: [PATCH 064/100] refactor(move_command): lower category name match tolerance --- rustmail/src/commands/move_thread/common.rs | 2 +- .../src/commands/move_thread/text_command/move_thread.rs | 2 +- rustmail/src/errors/dictionary.rs | 1 + rustmail/src/i18n/language/en.rs | 4 ++++ rustmail/src/i18n/language/fr.rs | 6 ++++++ 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/rustmail/src/commands/move_thread/common.rs b/rustmail/src/commands/move_thread/common.rs index 841b597..8055523 100644 --- a/rustmail/src/commands/move_thread/common.rs +++ b/rustmail/src/commands/move_thread/common.rs @@ -120,7 +120,7 @@ pub fn find_best_match_category( } if let Some((id, name)) = best_match { - let max_distance = (target_name.len().max(name.len()) as f64 * 0.5) as usize; + let max_distance = (target_name.len().max(name.len()) as f64 * 0.7) as usize; if best_distance <= max_distance { return Some((id, name)); } diff --git a/rustmail/src/commands/move_thread/text_command/move_thread.rs b/rustmail/src/commands/move_thread/text_command/move_thread.rs index e4fdd71..ef28f27 100644 --- a/rustmail/src/commands/move_thread/text_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/text_command/move_thread.rs @@ -5,7 +5,7 @@ use crate::commands::move_thread::common::{ use crate::config::Config; use crate::errors::CommandError::NotInThread; use crate::errors::ThreadError::CategoryNotFound; -use crate::errors::{CommandError, DiscordError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, DiscordError, ModmailError, ModmailResult}; use crate::types::logs::PaginationStore; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs index e779e91..d590782 100644 --- a/rustmail/src/errors/dictionary.rs +++ b/rustmail/src/errors/dictionary.rs @@ -244,6 +244,7 @@ impl DictionaryManager { CommandError::ReminderAlreadyExists => { ("reminder.reminder_already_exists".to_string(), None) } + CommandError::NotInThread() => ("command.not_in_thread".to_string(), None), _ => ("command.invalid_format".to_string(), None), }, ModmailError::Thread(thread_err) => match thread_err { diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index ae43397..74a5551 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -405,6 +405,10 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { DictionaryMessage::new("❌ This command can only be used in a support thread") .with_description("The alert command must be used in a thread channel"), ); + dict.messages.insert( + "command.not_in_thread".to_string(), + DictionaryMessage::new("This command can only be used in a support thread"), + ); dict.messages.insert( "alert.set_failed".to_string(), DictionaryMessage::new("❌ Failed to set alert") diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index b1ca780..cf0be59 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -431,6 +431,12 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ) .with_description("La commande alert doit être utilisée dans un canal de thread"), ); + dict.messages.insert( + "command.not_in_thread".to_string(), + DictionaryMessage::new( + "Cette commande ne peut être utilisée que dans un thread de support", + ), + ); dict.messages.insert( "alert.set_failed".to_string(), DictionaryMessage::new("❌ Échec de la définition de l'alerte") From acbc3f0119014ac6d1b550f5efa83bf3a62cba6f Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 10:28:11 +0200 Subject: [PATCH 065/100] refactor(alert): add warning message if an alert is already set for a thread --- ...c873b32e6eb406e83c73c3838d05b0ff68576.json | 12 ------ ...6a9e8feaa686c309ede399b64287fb77f2f69.json | 20 ++++++++++ rustmail/src/commands/alert/common.rs | 25 ++++-------- rustmail/src/db/operations/threads.rs | 39 ++++++++++++++++--- rustmail/src/errors/dictionary.rs | 1 + rustmail/src/errors/types.rs | 2 + rustmail/src/i18n/language/en.rs | 7 +++- rustmail/src/i18n/language/fr.rs | 7 +++- 8 files changed, 74 insertions(+), 39 deletions(-) delete mode 100644 .sqlx/query-4d42153597bee6a9019e13cb2f1c873b32e6eb406e83c73c3838d05b0ff68576.json create mode 100644 .sqlx/query-e8ad51199ceccfec9e6bbde94356a9e8feaa686c309ede399b64287fb77f2f69.json diff --git a/.sqlx/query-4d42153597bee6a9019e13cb2f1c873b32e6eb406e83c73c3838d05b0ff68576.json b/.sqlx/query-4d42153597bee6a9019e13cb2f1c873b32e6eb406e83c73c3838d05b0ff68576.json deleted file mode 100644 index de15875..0000000 --- a/.sqlx/query-4d42153597bee6a9019e13cb2f1c873b32e6eb406e83c73c3838d05b0ff68576.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM staff_alerts WHERE staff_user_id = ? AND thread_user_id = ? AND used = FALSE", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "4d42153597bee6a9019e13cb2f1c873b32e6eb406e83c73c3838d05b0ff68576" -} diff --git a/.sqlx/query-e8ad51199ceccfec9e6bbde94356a9e8feaa686c309ede399b64287fb77f2f69.json b/.sqlx/query-e8ad51199ceccfec9e6bbde94356a9e8feaa686c309ede399b64287fb77f2f69.json new file mode 100644 index 0000000..720d5ea --- /dev/null +++ b/.sqlx/query-e8ad51199ceccfec9e6bbde94356a9e8feaa686c309ede399b64287fb77f2f69.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT id FROM staff_alerts\n WHERE staff_user_id = ? AND thread_user_id = ? AND used = FALSE\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "e8ad51199ceccfec9e6bbde94356a9e8feaa686c309ede399b64287fb77f2f69" +} diff --git a/rustmail/src/commands/alert/common.rs b/rustmail/src/commands/alert/common.rs index 0d4cbb7..d4704f4 100644 --- a/rustmail/src/commands/alert/common.rs +++ b/rustmail/src/commands/alert/common.rs @@ -1,11 +1,13 @@ use crate::config::Config; use crate::db::{cancel_alert_for_staff, get_user_id_from_channel_id, set_alert_for_staff}; +use crate::errors::CommandError::AlertDoesNotExist; use crate::errors::DatabaseError::QueryFailed; use crate::errors::DiscordError::ApiError; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::colours::branding::GREEN; use serenity::all::{CommandInteraction, Context, CreateInteractionResponse, Message}; +use serenity::futures::SinkExt; use std::collections::HashMap; pub async fn get_thread_user_id_from_msg( @@ -74,9 +76,7 @@ pub async fn handle_cancel_alert_from_msg( pool: &sqlx::SqlitePool, ) -> ModmailResult<()> { if let Err(e) = cancel_alert_for_staff(msg.author.id, user_id, pool).await { - eprintln!("Failed to cancel alert: {}", e); - send_alert_message(ctx, msg, config, "alert.cancel_failed", None).await; - return Ok(()); + return Err(e); } let mut params = HashMap::new(); @@ -93,8 +93,8 @@ pub async fn handle_cancel_alert_from_command( user_id: i64, pool: &sqlx::SqlitePool, ) -> ModmailResult<()> { - if let Err(e) = cancel_alert_for_staff(command.user.id, user_id, pool).await { - Err(ModmailError::Database(QueryFailed(e.to_string()))) + if let Err(_) = cancel_alert_for_staff(command.user.id, user_id, pool).await { + Err(ModmailError::Command(AlertDoesNotExist)) } else { let mut params = HashMap::new(); params.insert("user".to_string(), format!("<@{}>", user_id)); @@ -125,7 +125,6 @@ pub async fn handle_set_alert_from_msg( pool: &sqlx::SqlitePool, ) -> ModmailResult<()> { if let Err(e) = set_alert_for_staff(msg.author.id, user_id, pool).await { - eprintln!("Failed to set alert: {}", e); send_alert_message(ctx, msg, config, "alert.set_failed", None).await; return Ok(()); } @@ -175,14 +174,7 @@ pub async fn send_alert_message( message_key: &str, params: Option<&HashMap>, ) { - let bot_user = match ctx.http.get_current_user().await { - Ok(user) => user, - Err(_) => return, - }; - - let bot_user_id = ctx.cache.current_user().id; - - let _ = MessageBuilder::staff_message(ctx, config, bot_user_id, bot_user.name.clone()) + let _ = MessageBuilder::system_message(ctx, config) .translated_content( message_key, params, @@ -191,7 +183,6 @@ pub async fn send_alert_message( ) .await .to_channel(msg.channel_id) - .color(GREEN.0) .send(false) .await; } @@ -205,7 +196,7 @@ pub async fn extract_alert_action(msg: &Message, config: &Config) -> bool { let start = prefix.len() + command_name.len(); let args = content[start..].trim(); - args.to_lowercase() == "cancel" + args.contains("cancel") || args.contains("c") } else { false } diff --git a/rustmail/src/db/operations/threads.rs b/rustmail/src/db/operations/threads.rs index fee528c..57fe88e 100644 --- a/rustmail/src/db/operations/threads.rs +++ b/rustmail/src/db/operations/threads.rs @@ -1,5 +1,6 @@ use crate::db::repr::Thread; -use crate::errors::ModmailResult; +use crate::errors::CommandError::AlertDoesNotExist; +use crate::errors::{DatabaseError, ModmailError, ModmailResult}; use chrono::Utc; use serenity::all::{ChannelId, GuildChannel, UserId}; use sqlx::{Error, SqlitePool}; @@ -248,8 +249,24 @@ pub async fn cancel_alert_for_staff( staff_user_id: serenity::all::UserId, thread_user_id: i64, pool: &SqlitePool, -) -> Result<(), Error> { +) -> ModmailResult<()> { let staff_user_id_i64 = staff_user_id.get() as i64; + + let existing = sqlx::query_scalar!( + r#" + SELECT id FROM staff_alerts + WHERE staff_user_id = ? AND thread_user_id = ? AND used = FALSE + "#, + staff_user_id_i64, + thread_user_id + ) + .fetch_optional(pool) + .await?; + + if existing.is_none() { + return Err(ModmailError::Command(AlertDoesNotExist)); + } + sqlx::query!( "DELETE FROM staff_alerts WHERE staff_user_id = ? AND thread_user_id = ?", staff_user_id_i64, @@ -265,16 +282,26 @@ pub async fn set_alert_for_staff( staff_user_id: UserId, thread_user_id: i64, pool: &SqlitePool, -) -> Result<(), Error> { +) -> ModmailResult<()> { let staff_user_id_i64 = staff_user_id.get() as i64; - sqlx::query!( - "DELETE FROM staff_alerts WHERE staff_user_id = ? AND thread_user_id = ? AND used = FALSE", + + let existing = sqlx::query_scalar!( + r#" + SELECT id FROM staff_alerts + WHERE staff_user_id = ? AND thread_user_id = ? AND used = FALSE + "#, staff_user_id_i64, thread_user_id ) - .execute(pool) + .fetch_optional(pool) .await?; + if existing.is_some() { + return Err(ModmailError::Database(DatabaseError::InsertFailed( + "".to_string(), + ))); + } + sqlx::query!( "INSERT INTO staff_alerts (staff_user_id, thread_user_id) VALUES (?, ?)", staff_user_id_i64, diff --git a/rustmail/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs index d590782..d21e43d 100644 --- a/rustmail/src/errors/dictionary.rs +++ b/rustmail/src/errors/dictionary.rs @@ -245,6 +245,7 @@ impl DictionaryManager { ("reminder.reminder_already_exists".to_string(), None) } CommandError::NotInThread() => ("command.not_in_thread".to_string(), None), + CommandError::AlertDoesNotExist => ("alert.alert_not_found".to_string(), None), _ => ("command.invalid_format".to_string(), None), }, ModmailError::Thread(thread_err) => match thread_err { diff --git a/rustmail/src/errors/types.rs b/rustmail/src/errors/types.rs index ec7054c..c14c265 100644 --- a/rustmail/src/errors/types.rs +++ b/rustmail/src/errors/types.rs @@ -77,6 +77,7 @@ pub enum CommandError { SendDmFailed, DiscordDeleteFailed, ReminderAlreadyExists, + AlertDoesNotExist, } #[derive(Debug, Clone)] @@ -230,6 +231,7 @@ impl fmt::Display for CommandError { CommandError::SendDmFailed => write!(f, "Failed to send DM to user"), CommandError::DiscordDeleteFailed => write!(f, "Failed to delete message on Discord"), CommandError::ReminderAlreadyExists => write!(f, "A reminder already exists"), + CommandError::AlertDoesNotExist => write!(f, "Alert does not exist"), } } } diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 74a5551..35bd6b4 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -405,14 +405,17 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { DictionaryMessage::new("❌ This command can only be used in a support thread") .with_description("The alert command must be used in a thread channel"), ); + dict.messages.insert( + "alert.alert_not_found".to_string(), + DictionaryMessage::new("No alert set for this thread"), + ); dict.messages.insert( "command.not_in_thread".to_string(), DictionaryMessage::new("This command can only be used in a support thread"), ); dict.messages.insert( "alert.set_failed".to_string(), - DictionaryMessage::new("❌ Failed to set alert") - .with_description("An error occurred while setting the alert"), + DictionaryMessage::new("You already have an alert for this thread!"), ); dict.messages.insert( "alert.confirmation".to_string(), diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index cf0be59..1d4926e 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -431,6 +431,10 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ) .with_description("La commande alert doit être utilisée dans un canal de thread"), ); + dict.messages.insert( + "alert.alert_not_found".to_string(), + DictionaryMessage::new("Aucune alerte définie pour ce ticket"), + ); dict.messages.insert( "command.not_in_thread".to_string(), DictionaryMessage::new( @@ -439,8 +443,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "alert.set_failed".to_string(), - DictionaryMessage::new("❌ Échec de la définition de l'alerte") - .with_description("Une erreur s'est produite lors de la définition de l'alerte"), + DictionaryMessage::new("❌ Vous avez déjà définie une alerte pour ce ticket !"), ); dict.messages.insert( "alert.confirmation".to_string(), From fd17cac44ac4c0fd09875abd043f4529319a0443 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 24 Oct 2025 14:55:53 +0200 Subject: [PATCH 066/100] refactor(commands): add Arc to all textual commands to allow access to slash command registry --- rustmail/src/bot.rs | 20 +++-- .../slash_command/add_reminder.rs | 19 +++-- .../add_reminder/text_command/add_reminder.rs | 21 +++-- .../add_staff/slash_command/add_staff.rs | 10 +-- .../add_staff/text_command/add_staff.rs | 18 ++-- .../src/commands/alert/slash_command/alert.rs | 12 ++- .../src/commands/alert/text_command/alert.rs | 20 ++--- .../anonreply/text_command/anonreply.rs | 26 +++--- .../src/commands/close/slash_command/close.rs | 12 ++- .../src/commands/close/text_command/close.rs | 26 +++--- .../commands/delete/slash_command/delete.rs | 12 ++- .../commands/delete/text_command/delete.rs | 18 ++-- .../src/commands/edit/slash_command/edit.rs | 12 ++- .../src/commands/edit/text_command/edit.rs | 30 ++++--- .../force_close/slash_command/force_close.rs | 12 ++- .../force_close/text_command/force_close.rs | 16 ++-- .../src/commands/help/slash_command/help.rs | 10 +-- .../src/commands/help/text_command/help.rs | 12 ++- rustmail/src/commands/id/slash_command/id.rs | 10 +-- rustmail/src/commands/id/text_command/id.rs | 14 ++-- .../src/commands/logs/slash_command/logs.rs | 14 ++-- .../src/commands/logs/text_command/logs.rs | 21 +++-- rustmail/src/commands/mod.rs | 16 ++-- .../move_thread/slash_command/move_thread.rs | 10 +-- .../move_thread/text_command/move_thread.rs | 20 ++--- .../new_thread/slash_command/new_thread.rs | 14 ++-- .../new_thread/text_command/new_thread.rs | 41 +++++---- .../commands/recover/slash_command/recover.rs | 12 ++- .../commands/recover/text_command/recover.rs | 14 ++-- .../slash_command/remove_reminder.rs | 10 +-- .../text_command/remove_reminder.rs | 10 +-- .../slash_command/remove_staff.rs | 10 +-- .../remove_staff/text_command/remove_staff.rs | 18 ++-- .../src/commands/reply/slash_command/reply.rs | 14 ++-- .../src/commands/reply/text_command/reply.rs | 18 ++-- .../src/handlers/guild_interaction_handler.rs | 18 ++-- .../src/handlers/guild_messages_handler.rs | 83 +++++++++++-------- rustmail/src/handlers/ready_handler.rs | 8 +- rustmail/src/utils/command/wrap_command.rs | 30 ++++--- 39 files changed, 343 insertions(+), 368 deletions(-) diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 9e69a9c..04842a5 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -1,4 +1,3 @@ -use crate::commands::CommandRegistry; use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; use crate::commands::alert::slash_command::alert::AlertCommand; @@ -15,9 +14,10 @@ use crate::commands::recover::slash_command::recover::RecoverCommand; use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; use crate::commands::reply::slash_command::reply::ReplyCommand; +use crate::commands::CommandRegistry; use crate::config::load_config; -use crate::errors::ModmailError; use crate::errors::types::ConfigError; +use crate::errors::ModmailError; use crate::handlers::guild_handler::GuildHandler; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::handlers::guild_members_handler::GuildMembersHandler; @@ -28,7 +28,7 @@ use crate::handlers::ready_handler::ReadyHandler; use crate::handlers::typing_proxy_handler::TypingProxyHandler; use crate::panel_commands::user::is_member::is_member; use crate::types::logs::PaginationContext; -use crate::{BotCommand, BotState, BotStatus, db}; +use crate::{db, BotCommand, BotState, BotStatus}; use base64::Engine; use rand::RngCore; use serenity::all::{ClientBuilder, GatewayIntents}; @@ -168,13 +168,17 @@ pub async fn run_bot( .event_handler(ReadyHandler::new( &config, registry.clone(), - Arc::new(shutdown_rx.clone()), - )) - .event_handler(GuildMessagesHandler::new( - &config, shutdown_rx.clone(), - pagination.clone(), )) + .event_handler( + GuildMessagesHandler::new( + &config, + registry.clone(), + shutdown_rx.clone(), + pagination.clone(), + ) + .await, + ) .event_handler(TypingProxyHandler::new(&config)) .event_handler(GuildMembersHandler::new(&config)) .event_handler(GuildMessageReactionsHandler::new(&config)) diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index 5173a25..6b42793 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -6,8 +6,8 @@ use crate::config::Config; use crate::db::reminders::{insert_reminder, Reminder}; use crate::db::threads::get_thread_by_user_id; use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use chrono::{Local, NaiveTime, TimeZone}; use regex::Regex; @@ -16,7 +16,6 @@ use serenity::all::{ CreateCommandOption, ResolvedOption, }; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct AddReminderCommand; @@ -26,7 +25,7 @@ impl RegistrableCommand for AddReminderCommand { "remind" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); let name = self.name(); @@ -84,9 +83,8 @@ impl RegistrableCommand for AddReminderCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); @@ -205,7 +203,14 @@ impl RegistrableCommand for AddReminderCommand { ) .await; - spawn_reminder(&reminder, Some(reminder_id), &ctx, &config, &pool, shutdown); + spawn_reminder( + &reminder, + Some(reminder_id), + &ctx, + &config, + &pool, + handler.shutdown.clone(), + ); Ok(()) }) diff --git a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs index 1b6ba6f..2aa08e5 100644 --- a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs @@ -5,20 +5,18 @@ use crate::config::Config; use crate::db::reminders::{insert_reminder, Reminder}; use crate::db::threads::get_thread_by_user_id; use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; -use crate::types::logs::PaginationStore; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::command::extract_reply_content::extract_reply_content; use chrono::{Local, NaiveTime, TimeZone}; use regex::Regex; use serenity::all::{Context, Message}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn add_reminder( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - shutdown: Arc>, - _pagination: PaginationStore, + handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool @@ -98,7 +96,7 @@ pub async fn add_reminder( send_register_confirmation_from_message( reminder_id, reminder_content, - ctx, + &ctx, &msg, config, trigger_timestamp, @@ -107,7 +105,14 @@ pub async fn add_reminder( let _ = msg.delete(&ctx.http).await; - spawn_reminder(&reminder, Some(reminder_id), &ctx, &config, &pool, shutdown); + spawn_reminder( + &reminder, + Some(reminder_id), + &ctx, + &config, + &pool, + handler.shutdown.clone(), + ); Ok(()) } diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index e8610f4..39bf6a1 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -5,8 +5,8 @@ use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -15,7 +15,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct AddStaffCommand; @@ -24,7 +23,7 @@ impl RegistrableCommand for AddStaffCommand { "addmod" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); let name = self.name(); @@ -64,9 +63,8 @@ impl RegistrableCommand for AddStaffCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/add_staff/text_command/add_staff.rs b/rustmail/src/commands/add_staff/text_command/add_staff.rs index db10a5c..dc93da2 100644 --- a/rustmail/src/commands/add_staff/text_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/text_command/add_staff.rs @@ -4,27 +4,25 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{ModmailError, ModmailResult, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, ModmailError, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn add_staff( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool .as_ref() .ok_or_else(common::database_connection_failed)?; - let user_id_str = extract_user_id(msg, config).await; + let user_id_str = extract_user_id(&msg, config).await; if user_id_str.is_empty() { return Err(ModmailError::Command(InvalidFormat)); @@ -36,12 +34,12 @@ pub async fn add_staff( }; if thread_exists(msg.author.id, pool).await { - match add_user_to_channel(ctx, msg.channel_id, user_id).await { + match add_user_to_channel(&ctx, msg.channel_id, user_id).await { Ok(_) => { let mut params = HashMap::new(); params.insert("user".to_string(), format!("<@{}>", user_id)); - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content("add_staff.add_success", Some(¶ms), None, None) .await .to_channel(msg.channel_id) diff --git a/rustmail/src/commands/alert/slash_command/alert.rs b/rustmail/src/commands/alert/slash_command/alert.rs index f4a0536..4758a49 100644 --- a/rustmail/src/commands/alert/slash_command/alert.rs +++ b/rustmail/src/commands/alert/slash_command/alert.rs @@ -4,16 +4,15 @@ use crate::commands::alert::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct AlertCommand; @@ -23,7 +22,7 @@ impl RegistrableCommand for AlertCommand { "alert" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -63,9 +62,8 @@ impl RegistrableCommand for AlertCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/alert/text_command/alert.rs b/rustmail/src/commands/alert/text_command/alert.rs index 6ea4fd8..ff28262 100644 --- a/rustmail/src/commands/alert/text_command/alert.rs +++ b/rustmail/src/commands/alert/text_command/alert.rs @@ -3,30 +3,28 @@ use crate::commands::alert::common::{ handle_set_alert_from_msg, }; use crate::config::Config; -use crate::errors::{ModmailResult, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use serenity::all::{Context, Message}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn alert( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool .as_ref() .ok_or_else(common::database_connection_failed)?; - let user_id = get_thread_user_id_from_msg(ctx, msg, config, pool).await?; - let is_cancel = extract_alert_action(msg, config).await; + let user_id = get_thread_user_id_from_msg(&ctx, &msg, config, pool).await?; + let is_cancel = extract_alert_action(&msg, config).await; if is_cancel { - handle_cancel_alert_from_msg(ctx, msg, config, user_id, pool).await + handle_cancel_alert_from_msg(&ctx, &msg, config, user_id, pool).await } else { - handle_set_alert_from_msg(ctx, msg, config, user_id, pool).await + handle_set_alert_from_msg(&ctx, &msg, config, user_id, pool).await } } diff --git a/rustmail/src/commands/anonreply/text_command/anonreply.rs b/rustmail/src/commands/anonreply/text_command/anonreply.rs index 7f63df5..66d38d9 100644 --- a/rustmail/src/commands/anonreply/text_command/anonreply.rs +++ b/rustmail/src/commands/anonreply/text_command/anonreply.rs @@ -1,22 +1,20 @@ use crate::config::Config; use crate::db::operations::allocate_next_message_number; -use crate::errors::{ModmailResult, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn anonreply( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let db_pool = config .db_pool @@ -27,7 +25,7 @@ pub async fn anonreply( let intent = extract_intent(content, &msg.attachments).await; let Some(intent) = intent else { - MessageBuilder::system_message(ctx, config) + MessageBuilder::system_message(&ctx, config) .translated_content( "reply.missing_content", None, @@ -52,7 +50,7 @@ pub async fn anonreply( let mut params = HashMap::new(); params.insert("username".to_string(), thread.user_name.clone()); - MessageBuilder::user_message(ctx, config, msg.author.id, msg.author.name.clone()) + MessageBuilder::user_message(&ctx, config, msg.author.id, msg.author.name.clone()) .translated_content( "user.left_server", Some(¶ms), @@ -74,7 +72,7 @@ pub async fn anonreply( let _ = msg.delete(&ctx.http).await; let mut sr = MessageBuilder::begin_staff_reply( - ctx, + &ctx, config, thread.id.clone(), msg.author.id, @@ -100,7 +98,7 @@ pub async fn anonreply( let (thread_msg, dm_msg_opt) = match sr.send_msg_and_record(db_pool).await { Ok(tuple) => tuple, Err(_) => { - MessageBuilder::system_message(ctx, config) + MessageBuilder::system_message(&ctx, config) .translated_content( "reply.send_failed_thread", None, @@ -116,7 +114,7 @@ pub async fn anonreply( }; if dm_msg_opt.is_none() { - MessageBuilder::system_message(ctx, config) + MessageBuilder::system_message(&ctx, config) .translated_content( "reply.send_failed_dm", None, @@ -136,7 +134,7 @@ pub async fn anonreply( params.insert("number".to_string(), next_message_number.to_string()); let _ = error_handler .send_success_message( - ctx, + &ctx, msg.channel_id, "success.message_sent", Some(params), diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index 25eaa93..cda4d63 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -4,9 +4,9 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::category::{ get_category_id_from_command, get_category_name_from_command, get_required_permissions_channel_from_command, @@ -22,7 +22,6 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -use tokio::sync::watch::Receiver; use tokio::time::sleep; pub struct CloseCommand; @@ -33,7 +32,7 @@ impl RegistrableCommand for CloseCommand { "close" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -103,9 +102,8 @@ impl RegistrableCommand for CloseCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/close/text_command/close.rs b/rustmail/src/commands/close/text_command/close.rs index 994f339..369dc66 100644 --- a/rustmail/src/commands/close/text_command/close.rs +++ b/rustmail/src/commands/close/text_command/close.rs @@ -3,8 +3,8 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::category::{ get_category_id_from_message, get_category_name_from_message, get_required_permissions_channel_from_message, @@ -16,15 +16,13 @@ use serenity::all::{Channel, Context, GuildId, Message, PermissionOverwriteType, use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -use tokio::sync::watch::Receiver; use tokio::time::sleep; pub async fn close( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let db_pool = config .db_pool @@ -84,7 +82,7 @@ pub async fn close( .await .unwrap_or(false); if existed { - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "close.closure_canceled", None, @@ -96,7 +94,7 @@ pub async fn close( .send(false) .await; } else { - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "close.no_scheduled_closures_to_cancel", None, @@ -119,7 +117,7 @@ pub async fn close( params.insert("seconds".to_string(), remaining.to_string()); if remaining > 0 { - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "close.closure_already_scheduled", Some(¶ms), @@ -150,7 +148,7 @@ pub async fn close( params.insert("time".to_string(), human); let _ = if silent { - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "close.silent_closing", Some(¶ms), @@ -162,7 +160,7 @@ pub async fn close( .send(false) .await; } else { - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "close.closing", Some(¶ms), @@ -330,7 +328,7 @@ pub async fn close( let required_permissions = get_required_permissions_channel_from_message(&ctx, &msg).await; if user_still_member && !silent { - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .content(&config.bot.close_message) .to_user(user_id) .send(false) @@ -339,7 +337,7 @@ pub async fn close( let mut params = HashMap::new(); params.insert("username".to_string(), thread.user_name.clone()); - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "user.left_server_close", Some(¶ms), diff --git a/rustmail/src/commands/delete/slash_command/delete.rs b/rustmail/src/commands/delete/slash_command/delete.rs index 5b4bf26..5f9947d 100644 --- a/rustmail/src/commands/delete/slash_command/delete.rs +++ b/rustmail/src/commands/delete/slash_command/delete.rs @@ -5,9 +5,9 @@ use crate::commands::delete::common::{ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::messages::get_thread_message_by_message_id; -use crate::errors::{MessageError, ModmailError, ModmailResult, common}; +use crate::errors::{common, MessageError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response_ephemeral; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -16,7 +16,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct DeleteCommand; @@ -26,7 +25,7 @@ impl RegistrableCommand for DeleteCommand { "delete" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -71,9 +70,8 @@ impl RegistrableCommand for DeleteCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/delete/text_command/delete.rs b/rustmail/src/commands/delete/text_command/delete.rs index 42e79a2..bfa9b0f 100644 --- a/rustmail/src/commands/delete/text_command/delete.rs +++ b/rustmail/src/commands/delete/text_command/delete.rs @@ -3,18 +3,16 @@ use crate::commands::delete::common::{ get_thread_info, send_delete_message, update_message_numbers, }; use crate::config::Config; -use crate::errors::{ModmailResult, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use serenity::all::{Context, Message}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn delete( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool @@ -22,17 +20,17 @@ pub async fn delete( .ok_or_else(common::database_connection_failed)?; let (user_id, thread) = get_thread_info(&msg.channel_id.to_string(), pool).await?; - let message_number = extract_message_number(msg, config).await; + let message_number = extract_message_number(&msg, config).await; if message_number.is_none() { - send_delete_message(ctx, msg, config, "delete.missing_number", None).await; + send_delete_message(&ctx, &msg, config, "delete.missing_number", None).await; return Ok(()); } let message_number = message_number.unwrap(); let message_ids = get_message_ids(user_id, &thread, message_number, pool).await?; - delete_discord_messages(ctx, &msg.channel_id, user_id, &message_ids).await?; + delete_discord_messages(&ctx, &msg.channel_id, user_id, &message_ids).await?; delete_database_message(&message_ids, pool).await?; update_message_numbers(&thread.channel_id, message_number, pool).await; diff --git a/rustmail/src/commands/edit/slash_command/edit.rs b/rustmail/src/commands/edit/slash_command/edit.rs index 1195206..1abc2d8 100644 --- a/rustmail/src/commands/edit/slash_command/edit.rs +++ b/rustmail/src/commands/edit/slash_command/edit.rs @@ -4,9 +4,9 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::{get_thread_message_by_inbox_message_id, update_message_content}; use crate::errors::common::message_not_found; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; @@ -16,7 +16,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct EditCommand; @@ -26,7 +25,7 @@ impl RegistrableCommand for EditCommand { "edit" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -87,9 +86,8 @@ impl RegistrableCommand for EditCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/edit/text_command/edit.rs b/rustmail/src/commands/edit/text_command/edit.rs index 1848e47..b5759e5 100644 --- a/rustmail/src/commands/edit/text_command/edit.rs +++ b/rustmail/src/commands/edit/text_command/edit.rs @@ -3,34 +3,32 @@ use crate::commands::edit::message_ops::{ cleanup_command_message, edit_messages, format_new_message, get_message_ids, }; use crate::commands::edit::validation::{ - EditCommandInput, parse_edit_command, validate_edit_permissions, + parse_edit_command, validate_edit_permissions, EditCommandInput, }; use crate::config::Config; use crate::db::get_thread_message_by_inbox_message_id; use crate::db::update_message_content; use crate::errors::common::message_not_found; -use crate::errors::{ModmailResult, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn edit( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool .as_ref() .ok_or_else(common::database_connection_failed)?; - let raw_content: String = match extract_command_content(msg, config) { + let raw_content: String = match extract_command_content(&msg, config) { Ok(content) => content, Err(e) => return Err(e), }; @@ -56,7 +54,7 @@ pub async fn edit( command_input.message_number, msg.author.id, pool, - ctx, + &ctx, msg.channel_id, ) .await @@ -76,8 +74,8 @@ pub async fn edit( }; let edited_messages_builder = match format_new_message( - ctx, - (Some(msg), None), + &ctx, + (Some(&msg), None), &command_input.new_content, &inbox_message_id, command_input.message_number as u64, @@ -97,7 +95,7 @@ pub async fn edit( }; let edit_result = edit_messages( - ctx, + &ctx, msg.channel_id, dm_msg_id.clone(), inbox_message_id.clone(), @@ -110,7 +108,7 @@ pub async fn edit( match edit_result { Ok(()) => { if config.notifications.show_success_on_edit { - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "success.message_edited", None, @@ -147,7 +145,7 @@ pub async fn edit( ); params.insert("link".to_string(), message_link); - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "edit.modification_from_staff", Some(¶ms), @@ -160,7 +158,7 @@ pub async fn edit( .await; } - cleanup_command_message(ctx, msg).await; + cleanup_command_message(&ctx, &msg).await; match update_message_content(&dm_msg_id, &command_input.new_content, pool).await { Ok(()) => (), diff --git a/rustmail/src/commands/force_close/slash_command/force_close.rs b/rustmail/src/commands/force_close/slash_command/force_close.rs index c3cb460..7d55dfe 100644 --- a/rustmail/src/commands/force_close/slash_command/force_close.rs +++ b/rustmail/src/commands/force_close/slash_command/force_close.rs @@ -4,12 +4,11 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{ModmailError, ModmailResult, common}; +use crate::errors::{common, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct ForceCloseCommand; @@ -19,7 +18,7 @@ impl RegistrableCommand for ForceCloseCommand { "force_close" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -43,9 +42,8 @@ impl RegistrableCommand for ForceCloseCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/force_close/text_command/force_close.rs b/rustmail/src/commands/force_close/text_command/force_close.rs index b9ee1ff..50c3ee9 100644 --- a/rustmail/src/commands/force_close/text_command/force_close.rs +++ b/rustmail/src/commands/force_close/text_command/force_close.rs @@ -3,18 +3,16 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{ModmailError, ModmailResult, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, ModmailError, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use serenity::all::{Context, Message}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn force_close( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let db_pool = config .db_pool @@ -24,7 +22,7 @@ pub async fn force_close( if !is_a_ticket_channel(msg.channel_id, db_pool).await { return match msg.category_id(&ctx.http).await { Some(category_id) if category_id == config.thread.inbox_category_id => { - delete_channel(ctx, msg.channel_id).await + delete_channel(&ctx, msg.channel_id).await } _ => Err(ModmailError::Thread(NotAThreadChannel)), }; @@ -35,7 +33,7 @@ pub async fn force_close( if !res { return Err(ModmailError::Thread(UserStillInServer)); } - delete_channel(ctx, msg.channel_id).await + delete_channel(&ctx, msg.channel_id).await } Err(..) => Err(ModmailError::Database(QueryFailed( "Failed to check if thread channel is orphaned".to_string(), diff --git a/rustmail/src/commands/help/slash_command/help.rs b/rustmail/src/commands/help/slash_command/help.rs index ea6c784..d8d1c51 100644 --- a/rustmail/src/commands/help/slash_command/help.rs +++ b/rustmail/src/commands/help/slash_command/help.rs @@ -1,13 +1,12 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::errors::ModmailResult; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct HelpCommand; @@ -17,7 +16,7 @@ impl RegistrableCommand for HelpCommand { "help" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -41,9 +40,8 @@ impl RegistrableCommand for HelpCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/help/text_command/help.rs b/rustmail/src/commands/help/text_command/help.rs index 4e36e0e..fb1a328 100644 --- a/rustmail/src/commands/help/text_command/help.rs +++ b/rustmail/src/commands/help/text_command/help.rs @@ -1,17 +1,15 @@ use crate::config::Config; use crate::errors::ModmailResult; -use crate::types::logs::PaginationStore; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn help( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let help_message = "# Available commands:\n\n\ **!add_staff ** - Add a staff to an hidden ticket\n\ @@ -28,7 +26,7 @@ pub async fn help( **!force_close** - Force close the current thread if it's orphaned\n\ **!id** - Show the user ID associated with the current thread"; - MessageBuilder::system_message(ctx, config) + MessageBuilder::system_message(&ctx, config) .content(help_message) .to_channel(msg.channel_id) .send(false) diff --git a/rustmail/src/commands/id/slash_command/id.rs b/rustmail/src/commands/id/slash_command/id.rs index 6dff7e5..b689cdf 100644 --- a/rustmail/src/commands/id/slash_command/id.rs +++ b/rustmail/src/commands/id/slash_command/id.rs @@ -4,14 +4,13 @@ use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; use crate::errors::ThreadError::{NotAThreadChannel, ThreadNotFound}; use crate::errors::{DatabaseError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, ResolvedOption}; use serenity::builder::CreateCommand; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct IdCommand; @@ -21,7 +20,7 @@ impl RegistrableCommand for IdCommand { "id" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -45,9 +44,8 @@ impl RegistrableCommand for IdCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/id/text_command/id.rs b/rustmail/src/commands/id/text_command/id.rs index 7dd61d6..d311a37 100644 --- a/rustmail/src/commands/id/text_command/id.rs +++ b/rustmail/src/commands/id/text_command/id.rs @@ -1,21 +1,19 @@ use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::common::{database_connection_failed, thread_not_found}; +use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::{ModmailError, ModmailResult}; -use crate::types::logs::PaginationStore; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn id( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let db_pool = config .db_pool @@ -35,7 +33,7 @@ pub async fn id( format!("||{}||", thread.user_id.to_string()), ); - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content("id.show_id", Some(¶ms), None, None) .await .to_channel(msg.channel_id) diff --git a/rustmail/src/commands/logs/slash_command/logs.rs b/rustmail/src/commands/logs/slash_command/logs.rs index e61e185..b081274 100644 --- a/rustmail/src/commands/logs/slash_command/logs.rs +++ b/rustmail/src/commands/logs/slash_command/logs.rs @@ -2,15 +2,14 @@ use crate::commands::logs::text_command::logs::{handle_logs_from_user_id, handle use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::errors::{DatabaseError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, UserId, }; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct LogsCommand; @@ -19,7 +18,7 @@ impl RegistrableCommand for LogsCommand { "logs" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -54,9 +53,8 @@ impl RegistrableCommand for LogsCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - pagination: PaginationStore, - ) -> BoxFuture> { + handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); @@ -89,7 +87,7 @@ impl RegistrableCommand for LogsCommand { Some(command.clone()), &config, &pool, - pagination, + handler.pagination.clone(), ) .await } else { @@ -100,7 +98,7 @@ impl RegistrableCommand for LogsCommand { &config, &pool, &user_id.unwrap().to_string(), - pagination, + handler.pagination.clone(), ) .await } diff --git a/rustmail/src/commands/logs/text_command/logs.rs b/rustmail/src/commands/logs/text_command/logs.rs index aa84214..af87590 100644 --- a/rustmail/src/commands/logs/text_command/logs.rs +++ b/rustmail/src/commands/logs/text_command/logs.rs @@ -4,13 +4,13 @@ use crate::db::get_thread_by_channel_id; use crate::db::logs::get_logs_from_user_id; use crate::errors::{DatabaseError, ModmailError, ModmailResult, ThreadError}; use crate::features::make_buttons; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::i18n::get_translated_message; use crate::modules::commands::LOGS_PAGE_SIZE; use crate::types::logs::{PaginationContext, PaginationStore}; use serenity::all::{ButtonStyle, ChannelId, CommandInteraction, Context, Message}; use sqlx::SqlitePool; use std::sync::Arc; -use tokio::sync::watch::Receiver; use uuid::Uuid; pub async fn handle_logs_in_thread( @@ -106,11 +106,10 @@ pub async fn handle_logs_from_user_id( } pub async fn logs( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - pagination: PaginationStore, + handler: Arc, ) -> ModmailResult<()> { let pool = match config.db_pool.clone() { Some(pool) => pool.clone(), @@ -120,7 +119,15 @@ pub async fn logs( let user_id = extract_user_id(&msg, &config); if user_id.is_empty() { - handle_logs_in_thread(&ctx, &msg.channel_id, None, &config, &pool, pagination).await + handle_logs_in_thread( + &ctx, + &msg.channel_id, + None, + &config, + &pool, + handler.pagination.clone(), + ) + .await } else { handle_logs_from_user_id( &ctx, @@ -129,7 +136,7 @@ pub async fn logs( config, &pool, &user_id.to_string(), - pagination, + handler.pagination.clone(), ) .await } diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index 8626516..6a857ee 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -1,5 +1,6 @@ use crate::config::Config; use crate::errors::ModmailResult; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::types::logs::PaginationStore; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::any::Any; @@ -26,7 +27,7 @@ pub mod remove_reminder; pub mod remove_staff; pub mod reply; -pub type BoxFuture = Pin + Send>>; +pub type BoxFuture<'a, T> = Pin + Send + 'a>>; pub trait RegistrableCommand: Any + Send + Sync { fn as_community(&self) -> Option<&dyn CommunityRegistrable> { @@ -35,7 +36,7 @@ pub trait RegistrableCommand: Any + Send + Sync { fn name(&self) -> &'static str; - fn register(&self, config: &Config) -> BoxFuture>; + fn register(&self, config: &Config) -> BoxFuture<'_, Vec>; fn run( &self, @@ -43,19 +44,18 @@ pub trait RegistrableCommand: Any + Send + Sync { command: &CommandInteraction, options: &[ResolvedOption<'_>], config: &Config, - shutdown: Arc>, - pagination: PaginationStore, - ) -> BoxFuture>; + handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>>; } pub trait CommunityRegistrable: RegistrableCommand { - fn register_community(&self, config: &Config) -> BoxFuture>; + fn register_community(&self, config: &Config) -> BoxFuture<'_, Vec>; } pub struct CommandRegistry { commands: HashMap<&'static str, Arc>, _shutdown: Arc>, - pagination: PaginationStore, + _pagination: PaginationStore, } impl CommandRegistry { @@ -63,7 +63,7 @@ impl CommandRegistry { Self { commands: HashMap::new(), _shutdown: Arc::new(shutdown), - pagination, + _pagination: pagination, } } diff --git a/rustmail/src/commands/move_thread/slash_command/move_thread.rs b/rustmail/src/commands/move_thread/slash_command/move_thread.rs index b4fbf61..9969121 100644 --- a/rustmail/src/commands/move_thread/slash_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/slash_command/move_thread.rs @@ -7,8 +7,8 @@ use crate::db::get_user_id_from_channel_id; use crate::errors::{ CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, ThreadError, }; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -17,7 +17,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct MoveCommand; @@ -27,7 +26,7 @@ impl RegistrableCommand for MoveCommand { "move" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -69,9 +68,8 @@ impl RegistrableCommand for MoveCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/move_thread/text_command/move_thread.rs b/rustmail/src/commands/move_thread/text_command/move_thread.rs index ef28f27..253b2e5 100644 --- a/rustmail/src/commands/move_thread/text_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/text_command/move_thread.rs @@ -6,33 +6,31 @@ use crate::config::Config; use crate::errors::CommandError::NotInThread; use crate::errors::ThreadError::CategoryNotFound; use crate::errors::{common, CommandError, DiscordError, ModmailError, ModmailResult}; -use crate::types::logs::PaginationStore; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use serenity::all::{Context, Message}; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn move_thread( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool .as_ref() .ok_or_else(common::database_connection_failed)?; - if !is_in_thread(msg, pool).await { + if !is_in_thread(&msg, pool).await { return Err(ModmailError::Command(NotInThread())); } - let category_name = extract_category_name(msg, config).await; + let category_name = extract_category_name(&msg, config).await; if category_name.is_empty() { return Err(ModmailError::Thread(CategoryNotFound)); } - let categories = fetch_server_categories(ctx, config).await; + let categories = fetch_server_categories(&ctx, config).await; if categories.is_empty() { return Err(ModmailError::Discord(DiscordError::FailedToFetchCategories)); } @@ -41,13 +39,13 @@ pub async fn move_thread( match target_category { Some((category_id, category_name)) => { - if let Err(e) = move_channel_to_category_by_msg(ctx, msg, category_id).await { + if let Err(e) = move_channel_to_category_by_msg(&ctx, &msg, category_id).await { return Err(ModmailError::Command(CommandError::CommandFailed( e.to_string(), ))); } - send_success_message(ctx, msg, config, &category_name).await; + send_success_message(&ctx, &msg, config, &category_name).await; } None => { return Err(ModmailError::Thread(CategoryNotFound)); diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index c96a778..d066610 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -4,10 +4,10 @@ use crate::config::Config; use crate::db::logs::get_logs_from_user_id; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; use crate::errors::{ - CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, common, + common, CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, }; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::user_recap::get_user_recap; @@ -18,7 +18,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct NewThreadCommand; @@ -32,7 +31,7 @@ impl RegistrableCommand for NewThreadCommand { "new_thread" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -74,9 +73,8 @@ impl RegistrableCommand for NewThreadCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); @@ -241,7 +239,7 @@ impl RegistrableCommand for NewThreadCommand { } impl CommunityRegistrable for NewThreadCommand { - fn register_community(&self, _config: &Config) -> BoxFuture> { + fn register_community(&self, _config: &Config) -> BoxFuture<'_, Vec> { Box::pin(async move { vec![CreateCommand::new("new_thread").kind(CommandType::User)] }) } } diff --git a/rustmail/src/commands/new_thread/text_command/new_thread.rs b/rustmail/src/commands/new_thread/text_command/new_thread.rs index 166e1b8..6c6cb42 100644 --- a/rustmail/src/commands/new_thread/text_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/text_command/new_thread.rs @@ -5,32 +5,30 @@ use crate::commands::new_thread::common::{ use crate::config::Config; use crate::db::logs::get_logs_from_user_id; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; -use crate::errors::{DiscordError, ModmailError, ModmailResult, common}; +use crate::errors::{common, DiscordError, ModmailError, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::user_recap::get_user_recap; use crate::utils::time::get_member_join_date::get_member_join_date_for_user; use serenity::all::{ChannelId, Context, GuildId, Message}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn new_thread( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool .as_ref() .ok_or_else(common::database_connection_failed)?; - let user_id = extract_user_id(msg, config).await; + let user_id = extract_user_id(&msg, config).await; if user_id.is_none() { - send_error_message(ctx, msg, config, "new_thread.missing_user", None).await; + send_error_message(&ctx, &msg, config, "new_thread.missing_user", None).await; return Ok(()); } @@ -52,15 +50,15 @@ pub async fn new_thread( params.insert("channel_id".to_string(), channel_id_str.clone()); send_error_message( - ctx, - msg, + &ctx, + &msg, config, "new_thread.user_has_thread_with_link", Some(¶ms), ) .await; } else { - send_error_message(ctx, msg, config, "new_thread.user_has_thread", None).await; + send_error_message(&ctx, &msg, config, "new_thread.user_has_thread", None).await; } return Ok(()); } @@ -80,7 +78,14 @@ pub async fn new_thread( Ok(channel) => channel, Err(e) => { eprintln!("Failed to create channel: {}", e); - send_error_message(ctx, msg, config, "new_thread.channel_creation_failed", None).await; + send_error_message( + &ctx, + &msg, + config, + "new_thread.channel_creation_failed", + None, + ) + .await; return Ok(()); } }; @@ -128,20 +133,20 @@ pub async fn new_thread( Err(e) => { eprintln!("Failed to create thread in database: {}", e); let _ = guild_channel.delete(&ctx.http).await; - send_error_message(ctx, msg, config, "new_thread.database_error", None).await; + send_error_message(&ctx, &msg, config, "new_thread.database_error", None).await; return Ok(()); } }; - send_welcome_message(ctx, &guild_channel, config, &user).await; + send_welcome_message(&ctx, &guild_channel, config, &user).await; - match send_dm_to_user(ctx, &user, config).await { + match send_dm_to_user(&ctx, &user, config).await { Ok(_) => { - send_success_message(ctx, msg, config, &user, &guild_channel, true).await; + send_success_message(&ctx, &msg, config, &user, &guild_channel, true).await; } Err(dm_error) => { eprintln!("Failed to send DM to user: {}", dm_error); - send_success_message(ctx, msg, config, &user, &guild_channel, false).await; + send_success_message(&ctx, &msg, config, &user, &guild_channel, false).await; } } diff --git a/rustmail/src/commands/recover/slash_command/recover.rs b/rustmail/src/commands/recover/slash_command/recover.rs index 52beb71..5fb8b38 100644 --- a/rustmail/src/commands/recover/slash_command/recover.rs +++ b/rustmail/src/commands/recover/slash_command/recover.rs @@ -1,15 +1,14 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct RecoverCommand; @@ -19,7 +18,7 @@ impl RegistrableCommand for RecoverCommand { "recover" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -43,9 +42,8 @@ impl RegistrableCommand for RecoverCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/recover/text_command/recover.rs b/rustmail/src/commands/recover/text_command/recover.rs index e3e1bd9..8e88e3d 100644 --- a/rustmail/src/commands/recover/text_command/recover.rs +++ b/rustmail/src/commands/recover/text_command/recover.rs @@ -1,20 +1,18 @@ use crate::config::Config; -use crate::errors::{ModmailResult, common}; +use crate::errors::{common, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; -use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn recover( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let _ = config .db_pool @@ -34,7 +32,7 @@ pub async fn recover( ) .await; - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .content(confirmation_message) .to_channel(msg.channel_id) .send(false) diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index 335bda8..346318e 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -2,8 +2,8 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -12,7 +12,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct RemoveReminderCommand; @@ -21,7 +20,7 @@ impl RegistrableCommand for RemoveReminderCommand { "unremind" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); let name = self.name(); @@ -57,9 +56,8 @@ impl RegistrableCommand for RemoveReminderCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs index 6f1836f..71df5c6 100644 --- a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -1,20 +1,18 @@ use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; -use crate::types::logs::PaginationStore; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn remove_reminder( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index 242b2f1..f6292be 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -5,8 +5,8 @@ use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ @@ -15,7 +15,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct RemoveStaffCommand; @@ -25,7 +24,7 @@ impl RegistrableCommand for RemoveStaffCommand { "delmod" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); let name = self.name(); @@ -66,9 +65,8 @@ impl RegistrableCommand for RemoveStaffCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs index 54528d0..ec53f5d 100644 --- a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs @@ -3,27 +3,25 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{ModmailError, ModmailResult, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, ModmailError, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn remove_staff( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let pool = config .db_pool .as_ref() .ok_or_else(common::database_connection_failed)?; - let user_id_str = extract_user_id(msg, config).await; + let user_id_str = extract_user_id(&msg, config).await; if user_id_str.is_empty() { return Err(ModmailError::Command(InvalidFormat)); @@ -35,12 +33,12 @@ pub async fn remove_staff( }; if thread_exists(msg.author.id, pool).await { - match remove_user_from_channel(ctx, msg.channel_id, user_id).await { + match remove_user_from_channel(&ctx, msg.channel_id, user_id).await { Ok(_) => { let mut params = HashMap::new(); params.insert("user".to_string(), format!("<@{}>", user_id)); - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content("add_staff.remove_success", Some(¶ms), None, None) .await .to_channel(msg.channel_id) diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index 9277056..b5ee528 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -2,12 +2,12 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{ Attachment, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, @@ -15,7 +15,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub struct ReplyCommand; @@ -25,7 +24,7 @@ impl RegistrableCommand for ReplyCommand { "reply" } - fn register(&self, config: &Config) -> BoxFuture> { + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); Box::pin(async move { @@ -100,9 +99,8 @@ impl RegistrableCommand for ReplyCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, - ) -> BoxFuture> { + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index d181160..b5c5da5 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -1,23 +1,21 @@ use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; -use crate::types::logs::PaginationStore; +use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; +use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::watch::Receiver; pub async fn reply( - ctx: &Context, - msg: &Message, + ctx: Context, + msg: Message, config: &Config, - _shutdown: Arc>, - _pagination: PaginationStore, + _handler: Arc, ) -> ModmailResult<()> { let db_pool = config .db_pool @@ -47,7 +45,7 @@ pub async fn reply( let _ = msg.delete(&ctx.http).await; let mut sr = MessageBuilder::begin_staff_reply( - ctx, + &ctx, config, thread.id.clone(), msg.author.id, @@ -84,7 +82,7 @@ pub async fn reply( let mut params = HashMap::new(); params.insert("number".to_string(), next_message_number.to_string()); - let _ = MessageBuilder::system_message(ctx, config) + let _ = MessageBuilder::system_message(&ctx, config) .translated_content( "success.message_sent", Some(¶ms), diff --git a/rustmail/src/handlers/guild_interaction_handler.rs b/rustmail/src/handlers/guild_interaction_handler.rs index 1320dc5..66489fc 100644 --- a/rustmail/src/handlers/guild_interaction_handler.rs +++ b/rustmail/src/handlers/guild_interaction_handler.rs @@ -12,7 +12,7 @@ use tokio::sync::watch::Receiver; #[derive(Clone)] pub struct InteractionHandler { - pub config: Config, + pub config: Arc, pub registry: Arc, pub shutdown: Arc>, pub pagination: PaginationStore, @@ -21,13 +21,13 @@ pub struct InteractionHandler { impl InteractionHandler { pub fn new( config: &Config, - register: Arc, + registry: Arc, shutdown: Receiver, pagination: PaginationStore, ) -> Self { Self { - config: config.clone(), - registry: register, + config: Arc::new(config.clone()), + registry, shutdown: Arc::new(shutdown), pagination, } @@ -72,18 +72,10 @@ impl EventHandler for InteractionHandler { let command = command.clone(); let options = command.data.options().clone(); let config = self.config.clone(); - let pagination = self.pagination.clone(); if let Some(handler) = self.registry.get(command.data.name.as_str()) { let result = handler - .run( - &ctx, - &command, - &options, - &config, - self.shutdown.clone(), - pagination, - ) + .run(&ctx, &command, &options, &config, Arc::new(self.clone())) .await; if let Err(e) = result { diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 1e07b71..cb162ab 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -16,6 +16,7 @@ use crate::commands::recover::text_command::recover::recover; use crate::commands::remove_reminder::text_command::remove_reminder::remove_reminder; use crate::commands::remove_staff::text_command::remove_staff::remove_staff; use crate::commands::reply::text_command::reply::reply; +use crate::commands::CommandRegistry; use crate::config::Config; use crate::db::messages::get_thread_message_by_dm_message_id; use crate::db::operations::messages::get_thread_message_by_message_id; @@ -40,54 +41,67 @@ use std::collections::HashSet; use std::sync::{LazyLock, Mutex}; use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; use tokio::sync::watch::Receiver; +use tokio::sync::Mutex as AsyncMutex; static SUPPRESSED_DELETES: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); type CommandFunc = Arc; -type StaticCommandFunc = dyn Fn( +type StaticCommandFunc = dyn for<'a> Fn( Context, Message, - Config, - Receiver, - PaginationStore, - ) -> Pin> + Send>> + &'a Config, + Arc, + ) -> Pin> + Send + 'a>> + Send + Sync + 'static; +#[derive(Clone)] pub struct GuildMessagesHandler { - pub config: Config, - pub commands: HashMap, - pub shutdown: Receiver, + pub config: Arc, + pub commands: Arc>>, + pub registry: Arc, + pub shutdown: Arc>, pub pagination: PaginationStore, } impl GuildMessagesHandler { - pub fn new(config: &Config, shutdown: Receiver, pagination: PaginationStore) -> Self { - let mut h = Self { - config: config.clone(), - commands: HashMap::new(), + pub async fn new( + config: &Config, + registry: Arc, + shutdown: Receiver, + pagination: PaginationStore, + ) -> Self { + let h = Self { + config: Arc::new(config.clone()), + commands: Arc::new(AsyncMutex::new(HashMap::new())), + registry, + shutdown: Arc::new(shutdown), pagination, - shutdown, }; - wrap_command!(h.commands, ["reply", "r"], reply); - wrap_command!(h.commands, ["edit", "e"], edit); - wrap_command!(h.commands, ["close", "c"], close); - wrap_command!(h.commands, "recover", recover); - wrap_command!(h.commands, "alert", alert); - wrap_command!(h.commands, ["move", "mv"], move_thread); - wrap_command!(h.commands, ["nt", "new_thread"], new_thread); - wrap_command!(h.commands, "delete", delete); - wrap_command!(h.commands, ["anonreply", "ar"], anonreply); - wrap_command!(h.commands, ["force_close", "fc"], force_close); - wrap_command!(h.commands, ["addmod", "am"], add_staff); - wrap_command!(h.commands, ["delmod", "dm"], remove_staff); - wrap_command!(h.commands, "id", id); - wrap_command!(h.commands, "help", help); - wrap_command!(h.commands, ["remind", "rem"], add_reminder); - wrap_command!(h.commands, ["unremind", "urem"], remove_reminder); - wrap_command!(h.commands, "logs", logs); + + let mut lock = h.commands.lock().await; + + wrap_command!(lock, ["reply", "r"], reply); + wrap_command!(lock, ["edit", "e"], edit); + wrap_command!(lock, ["close", "c"], close); + wrap_command!(lock, "recover", recover); + wrap_command!(lock, "alert", alert); + wrap_command!(lock, ["move", "mv"], move_thread); + wrap_command!(lock, ["nt", "new_thread"], new_thread); + wrap_command!(lock, "delete", delete); + wrap_command!(lock, ["anonreply", "ar"], anonreply); + wrap_command!(lock, ["force_close", "fc"], force_close); + wrap_command!(lock, ["addmod", "am"], add_staff); + wrap_command!(lock, ["delmod", "dm"], remove_staff); + wrap_command!(lock, "id", id); + wrap_command!(lock, "help", help); + wrap_command!(lock, ["remind", "rem"], add_reminder); + wrap_command!(lock, ["unremind", "urem"], remove_reminder); + wrap_command!(lock, "logs", logs); + + drop(lock); h } } @@ -190,13 +204,14 @@ impl EventHandler for GuildMessagesHandler { command_name = &message_content[self.config.command.prefix.len()..i]; } - if let Some(command_func) = self.commands.get(command_name) + let commands_lock = self.commands.lock().await; + + if let Some(command_func) = commands_lock.get(command_name) && let Err(error) = command_func( ctx.clone(), msg.clone(), - self.config.clone(), - self.shutdown.clone(), - self.pagination.clone(), + &self.config.clone(), + Arc::new(self.clone()), ) .await { diff --git a/rustmail/src/handlers/ready_handler.rs b/rustmail/src/handlers/ready_handler.rs index 291ab2c..d0f504c 100644 --- a/rustmail/src/handlers/ready_handler.rs +++ b/rustmail/src/handlers/ready_handler.rs @@ -20,15 +20,11 @@ pub struct ReadyHandler { } impl ReadyHandler { - pub fn new( - config: &Config, - registry: Arc, - shutdown: Arc>, - ) -> Self { + pub fn new(config: &Config, registry: Arc, shutdown: Receiver) -> Self { Self { config: config.clone(), registry, - shutdown, + shutdown: Arc::new(shutdown), } } } diff --git a/rustmail/src/utils/command/wrap_command.rs b/rustmail/src/utils/command/wrap_command.rs index 186b1be..7307df7 100644 --- a/rustmail/src/utils/command/wrap_command.rs +++ b/rustmail/src/utils/command/wrap_command.rs @@ -1,23 +1,27 @@ #[macro_export] macro_rules! wrap_command { ($map:expr, [$($name:expr),+], $func:expr) => {{ - let command: std::sync::Arc, PaginationStore) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = - std::sync::Arc::new(|ctx, msg, config, shutdown, pagination| { - Box::pin(async move { - $func(&ctx, &msg, &config, Arc::new(shutdown), pagination).await - }) - }); + let command: std::sync::Arc< + dyn for<'a> Fn( + serenity::prelude::Context, + serenity::model::prelude::Message, + &'a $crate::config::Config, + Arc, + ) -> std::pin::Pin< + Box< + dyn std::future::Future> + Send + 'a + > + > + Send + Sync + 'static + > = std::sync::Arc::new(|ctx, msg, config, handler| { + Box::pin(async move { + $func(ctx, msg, config, handler).await + }) + }); $( $map.insert($name.to_string(), std::sync::Arc::clone(&command)); )+ }}; ($map:expr, $name:expr, $func:expr) => {{ - let command: std::sync::Arc, PaginationStore) -> std::pin::Pin> + Send>> + Send + Sync + 'static> = - std::sync::Arc::new(|ctx, msg, config, shutdown, pagination| { - Box::pin(async move { - $func(&ctx, &msg, &config, Arc::new(shutdown), pagination).await - }) - }); - $map.insert($name.to_string(), command); + wrap_command!($map, [$name], $func); }}; } From 1842a2b12ce0d03c5236bbf68eabe65361b91640 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 26 Oct 2025 19:31:23 +0100 Subject: [PATCH 067/100] feat(commands): add doc's method to set commands documentation --- rustmail/src/commands/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index 6a857ee..7b0704a 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -36,6 +36,8 @@ pub trait RegistrableCommand: Any + Send + Sync { fn name(&self) -> &'static str; + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String>; + fn register(&self, config: &Config) -> BoxFuture<'_, Vec>; fn run( From dc1188ac7784d577fa52c12b956020d1b9d54e14 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 26 Oct 2025 19:32:43 +0100 Subject: [PATCH 068/100] refactor(trads): add all traductions for command's description --- rustmail/src/i18n/language/en.rs | 70 +++++++++++++++++++++++++++++++- rustmail/src/i18n/language/fr.rs | 68 +++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 35bd6b4..3a0ec96 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -803,6 +803,74 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "reminder.reminder_already_exists".to_string(), - DictionaryMessage::new("Vous avez déjà un rappel programmé à cette heure."), + DictionaryMessage::new("You already have a reminder scheduled for that time."), + ); + dict.messages.insert( + "help.add_reminder".to_string(), + DictionaryMessage::new("Sets a reminder for a specific time. To do so, use `!remind ` or `!rem `. If the specified time has already passed today, the reminder will be scheduled for tomorrow."), + ); + dict.messages.insert( + "help.add_staff".to_string(), + DictionaryMessage::new("Adds a staff member to a ticket. To do so, use `!addmod ` or `!am ` inside a ticket."), + ); + dict.messages.insert( + "help.alert".to_string(), + DictionaryMessage::new("Sets an alert for a user when they send a new message. To create an alert, use `!alert` inside a ticket. To cancel a scheduled alert, use `!alert cancel` or `!alert c`."), + ); + dict.messages.insert( + "help.close".to_string(), + DictionaryMessage::new("Closes the current ticket. You can specify a delay before closing using `!close ` or `!c `. You can also add the `--silent` or `-s` option to avoid notifying the user that their ticket has been closed. To cancel a scheduled closure, use `!close --cancel`, `!close -c`, or `!close cancel`."), + ); + dict.messages.insert( + "help.delete".to_string(), + DictionaryMessage::new("Deletes a specific message within a thread. To do so, use `!delete ` inside a ticket."), + ); + dict.messages.insert( + "help.edit".to_string(), + DictionaryMessage::new("Edits the content of a previously sent message in a ticket. To edit a message, use `!edit ` or `!e ` inside a ticket."), + ); + dict.messages.insert( + "help.force_close".to_string(), + DictionaryMessage::new("Force-closes a ticket when an error prevents normal closure. This command will be removed in future versions. To force-close a ticket, use `!force_close` or `!fc` inside a ticket."), + ); + dict.messages.insert( + "help.help".to_string(), + DictionaryMessage::new("Displays a list of all available commands with a short description. To view the help message, use `!help`."), + ); + dict.messages.insert( + "help.id".to_string(), + DictionaryMessage::new("Displays the Discord ID of the user associated with the ticket. To view the user's ID, use `!id` inside a ticket."), + ); + dict.messages.insert( + "help.logs".to_string(), + DictionaryMessage::new("Retrieves logs from all previous tickets of a user. You can either specify a Discord ID (`!logs `) or run the command inside a ticket to get that ticket’s logs."), + ); + dict.messages.insert( + "help.move".to_string(), + DictionaryMessage::new("Moves the current ticket to another category. To move a ticket, use `!move ` or `!mv ` inside the ticket."), + ); + dict.messages.insert( + "help.new_thread".to_string(), + DictionaryMessage::new("Creates a new ticket for a specified user. To create a ticket, use `!new_thread ` or `!nt `."), + ); + dict.messages.insert( + "help.recover".to_string(), + DictionaryMessage::new("Starts the process of recovering missing messages in Modmail tickets. This process runs automatically, but you can trigger it manually if needed. To do so, use `!recover`."), + ); + dict.messages.insert( + "help.remove_reminder".to_string(), + DictionaryMessage::new("Deletes a reminder you previously set. To remove a reminder, use `!unremind ` or `!urem `."), + ); + dict.messages.insert( + "help.remove_staff".to_string(), + DictionaryMessage::new("Removes a staff member from the current ticket. To remove a staff member, use `!delmod ` or `!dm ` inside the ticket."), + ); + dict.messages.insert( + "help.reply".to_string(), + DictionaryMessage::new("Replies in a ticket. To reply, use `!reply [attachment]` or `!r [attachment]` inside the ticket. If you want to reply anonymously, use `!anonreply`, `!ar`, or specify the option in the slash command `/reply`."), + ); + dict.messages.insert( + "help.message".to_string(), + DictionaryMessage::new("## Commands:\n\n**All commands** are also available as **__slash commands_** with the **__same name__**.\n\n"), ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 1d4926e..17d1166 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -829,4 +829,72 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "reminder.reminder_already_exists".to_string(), DictionaryMessage::new("Vous avez déjà un rappel programmé à cette heure."), ); + dict.messages.insert( + "help.add_reminder".to_string(), + DictionaryMessage::new("Configure un rappel à une heure spécifique. Pour ce faire, faites `!remind ` ou `!rem `. Si l'heure est déjà passée aujourd'hui, le rappel sera programmé pour demain."), + ); + dict.messages.insert( + "help.add_staff".to_string(), + DictionaryMessage::new("Ajoute un membre du staff à un ticket. Pour ce faire, faites `!addmod ` ou `!am ` dans un ticket."), + ); + dict.messages.insert( + "help.alert".to_string(), + DictionaryMessage::new("Configure une alerte pour un utilisateur lorsqu'il envoie un nouveau message. Pour programmer une alerte, faites `!alert` dans un ticket. Pour annuler une alerte programmée, faites `!alert cancel` ou `!alert c`."), + ); + dict.messages.insert( + "help.close".to_string(), + DictionaryMessage::new("Ferme le ticket actuel. Vous pouvez spécifier un délai avant la fermeture en faisant : `!close ` ou `!c `. Vous pouvez ajouter l'option `--silent` ou `-s` pour ne pas avertir l'utilisateur que son ticket a été fermé. Vous pouvez également annuler une fermeture programmée en faisant `!close --cancel`, `!close -c` ou `!close cancel`."), + ); + dict.messages.insert( + "help.delete".to_string(), + DictionaryMessage::new("Supprime un message spécifique dans un fil de discussion. Pour ce faire, faites `!delete ` dans un ticket."), + ); + dict.messages.insert( + "help.edit".to_string(), + DictionaryMessage::new("Modifie le contenu d'un message précédemment envoyé dans un ticket. Pour modifier un message, faites `!edit ` ou `!e ` dans un ticket."), + ); + dict.messages.insert( + "help.force_close".to_string(), + DictionaryMessage::new("Ferme un ticket lorsqu'une erreur empêche la fermeture normale. Cette commande disparaîtra dans les prochaines versions. Pour forcer la fermeture d'un ticket, faites `!force_close` ou `!fc` dans un ticket."), + ); + dict.messages.insert( + "help.help".to_string(), + DictionaryMessage::new("Affiche une liste de toutes les commandes disponibles avec une brève description. Pour afficher le message d'aide, faites `!help`."), + ); + dict.messages.insert( + "help.id".to_string(), + DictionaryMessage::new("Affiche l'identifiant Discord de l'utilisateur associé au ticket. Pour afficher l'ID de l'utilisateur, faites `!id` dans un ticket."), + ); + dict.messages.insert( + "help.logs".to_string(), + DictionaryMessage::new("Récupère les logs de tous les anciens tickets d'un utilisateur. Vous pouvez soit spécifier un identifiant Discord (`!logs `), soit exécuter la commande dans un ticket pour obtenir les logs de ce ticket."), + ); + dict.messages.insert( + "help.move".to_string(), + DictionaryMessage::new("Déplace le ticket actuel vers une autre catégorie. Pour déplacer un ticket, faites `!move ` ou `!mv ` dans le ticket."), + ); + dict.messages.insert( + "help.new_thread".to_string(), + DictionaryMessage::new("Crée un nouveau ticket pour un utilisateur spécifié. Pour créer un ticket, faites `!new_thread ` ou `!nt `."), + ); + dict.messages.insert( + "help.recover".to_string(), + DictionaryMessage::new("Lance le processus de récupération des messages manquants dans les tickets Modmail. Ce processus est automatique, mais cette commande permet de le relancer manuellement si nécessaire. Pour cela, faites `!recover`."), + ); + dict.messages.insert( + "help.remove_reminder".to_string(), + DictionaryMessage::new("Supprime un rappel que vous avez précédemment configuré. Pour supprimer un rappel, faites `!unremind ` ou `!urem `."), + ); + dict.messages.insert( + "help.remove_staff".to_string(), + DictionaryMessage::new("Retire un membre du staff du ticket actuel. Pour retirer un staff, faites `!delmod ` ou `!dm ` dans le ticket."), + ); + dict.messages.insert( + "help.reply".to_string(), + DictionaryMessage::new("Répond dans un ticket. Pour répondre, faites `!reply [attachment]` ou `!r [attachment]` dans le ticket. Si vous souhaitez répondre anonymement, utilisez la commande `!anonreply`, `!ar`, ou spécifiez l'option dans la commande slash `reply`."), + ); + dict.messages.insert( + "help.message".to_string(), + DictionaryMessage::new("## Commandes :\n\n**Toutes les commandes** disponibles sont également utilisables via des **__commandes slash__** portant le __même nom__.\n\n"), + ); } From 9d562b0a249e628a4f45fcd5170b6077d01cc7ff Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Sun, 26 Oct 2025 19:33:05 +0100 Subject: [PATCH 069/100] refactor(commands): implement doc's method in all commands --- .../slash_command/add_reminder.rs | 7 +++ .../add_staff/slash_command/add_staff.rs | 7 +++ .../src/commands/alert/slash_command/alert.rs | 7 +++ .../src/commands/close/slash_command/close.rs | 9 +++- .../commands/delete/slash_command/delete.rs | 7 +++ .../src/commands/edit/slash_command/edit.rs | 7 +++ .../force_close/slash_command/force_close.rs | 7 +++ .../src/commands/help/slash_command/help.rs | 44 ++++++++++++------- .../src/commands/help/text_command/help.rs | 25 ++++------- rustmail/src/commands/id/slash_command/id.rs | 7 +++ .../src/commands/logs/slash_command/logs.rs | 7 +++ .../move_thread/slash_command/move_thread.rs | 7 +++ .../new_thread/slash_command/new_thread.rs | 7 +++ .../commands/recover/slash_command/recover.rs | 7 +++ .../slash_command/remove_reminder.rs | 7 +++ .../slash_command/remove_staff.rs | 7 +++ .../src/commands/reply/slash_command/reply.rs | 7 +++ 17 files changed, 143 insertions(+), 33 deletions(-) diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index 6b42793..aa95d0f 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -16,6 +16,7 @@ use serenity::all::{ CreateCommandOption, ResolvedOption, }; use std::sync::Arc; +use serenity::FutureExt; pub struct AddReminderCommand; @@ -25,6 +26,12 @@ impl RegistrableCommand for AddReminderCommand { "remind" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.add_reminder", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); let name = self.name(); diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index 39bf6a1..eed8177 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -15,6 +15,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct AddStaffCommand; @@ -23,6 +24,12 @@ impl RegistrableCommand for AddStaffCommand { "addmod" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.add_staff", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); let name = self.name(); diff --git a/rustmail/src/commands/alert/slash_command/alert.rs b/rustmail/src/commands/alert/slash_command/alert.rs index 4758a49..b53e2db 100644 --- a/rustmail/src/commands/alert/slash_command/alert.rs +++ b/rustmail/src/commands/alert/slash_command/alert.rs @@ -13,6 +13,7 @@ use serenity::all::{ CreateCommandOption, ResolvedOption, }; use std::sync::Arc; +use serenity::FutureExt; pub struct AlertCommand; @@ -22,6 +23,12 @@ impl RegistrableCommand for AlertCommand { "alert" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.alert", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index cda4d63..eb8b8d5 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::category::{ @@ -22,6 +22,7 @@ use serenity::all::{ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use serenity::FutureExt; use tokio::time::sleep; pub struct CloseCommand; @@ -32,6 +33,12 @@ impl RegistrableCommand for CloseCommand { "close" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.close", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/delete/slash_command/delete.rs b/rustmail/src/commands/delete/slash_command/delete.rs index 5f9947d..9621eb3 100644 --- a/rustmail/src/commands/delete/slash_command/delete.rs +++ b/rustmail/src/commands/delete/slash_command/delete.rs @@ -16,6 +16,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct DeleteCommand; @@ -25,6 +26,12 @@ impl RegistrableCommand for DeleteCommand { "delete" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.delete", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/edit/slash_command/edit.rs b/rustmail/src/commands/edit/slash_command/edit.rs index 1abc2d8..a5a87dc 100644 --- a/rustmail/src/commands/edit/slash_command/edit.rs +++ b/rustmail/src/commands/edit/slash_command/edit.rs @@ -16,6 +16,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct EditCommand; @@ -25,6 +26,12 @@ impl RegistrableCommand for EditCommand { "edit" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.edit", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/force_close/slash_command/force_close.rs b/rustmail/src/commands/force_close/slash_command/force_close.rs index 7d55dfe..7f068fb 100644 --- a/rustmail/src/commands/force_close/slash_command/force_close.rs +++ b/rustmail/src/commands/force_close/slash_command/force_close.rs @@ -9,6 +9,7 @@ use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; +use serenity::FutureExt; pub struct ForceCloseCommand; @@ -18,6 +19,12 @@ impl RegistrableCommand for ForceCloseCommand { "force_close" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.force_close", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/help/slash_command/help.rs b/rustmail/src/commands/help/slash_command/help.rs index d8d1c51..8d1b52d 100644 --- a/rustmail/src/commands/help/slash_command/help.rs +++ b/rustmail/src/commands/help/slash_command/help.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; +use std::error::Error; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::errors::ModmailResult; @@ -7,6 +9,8 @@ use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; +use serenity::FutureExt; +use serenity::futures::future::join_all; pub struct HelpCommand; @@ -16,6 +20,12 @@ impl RegistrableCommand for HelpCommand { "help" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.help", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); @@ -40,32 +50,34 @@ impl RegistrableCommand for HelpCommand { command: &CommandInteraction, _options: &[ResolvedOption<'_>], config: &Config, - _handler: Arc, + handler: Arc, ) -> BoxFuture<'_, ModmailResult<()>> { let ctx = ctx.clone(); let command = command.clone(); let config = config.clone(); Box::pin(async move { - let help_message = "# Available commands:\n\n\ - **!add_staff ** - Add a staff to an hidden ticket\n\ - **!remove_staff ** - Remove a staff from a ticket\n\ - **!alert** - Alert staff in the current thread when a new user answer arrives \n\ - **!help** - Show this help message\n\ - **!reply ** - Reply to a ticket\n\ - **!annonreply ** - Reply anonymously to a ticket\n\ - **!close [reason]** - Close the current thread with an optional reason\n\ - **!delete [reason]** - Delete the current thread with an optional reason\n\ - **!add_rap** - Add reminder\n\ - **!new_thread ** - Create a new ticket with a specific user\n\ - **!alert [cancel]** - Set or cancel an alert for staff in the current thread\n\ - **!force_close** - Force close the current thread if it's orphaned\n\ - **!id** - Show the user ID associated with the current thread"; + let mut docs_message = String::new(); + + let welcome_msg = get_translated_message(&config, "help.message", None, None, None, None).await; + docs_message.push_str(&welcome_msg); + + let futures = handler.registry.commands.iter().map(|(name, command)| { + let config = config.clone(); + + async move { + let doc = command.doc(&config).await; + format!("**{}** — {}\n\n", name, doc) + } + }); + + let results = join_all(futures).await; + docs_message.push_str(&results.join("")); defer_response(&ctx, &command).await?; let response = MessageBuilder::system_message(&ctx, &config) - .content(help_message) + .content(docs_message) .to_channel(command.channel_id) .build_interaction_message_followup() .await; diff --git a/rustmail/src/commands/help/text_command/help.rs b/rustmail/src/commands/help/text_command/help.rs index fb1a328..082306e 100644 --- a/rustmail/src/commands/help/text_command/help.rs +++ b/rustmail/src/commands/help/text_command/help.rs @@ -9,25 +9,18 @@ pub async fn help( ctx: Context, msg: Message, config: &Config, - _handler: Arc, + handler: Arc, ) -> ModmailResult<()> { - let help_message = "# Available commands:\n\n\ - **!add_staff ** - Add a staff to an hidden ticket\n\ - **!remove_staff ** - Remove a staff from a ticket\n\ - **!alert** - Alert staff in the current thread when a new user answer arrives \n\ - **!help** - Show this help message\n\ - **!reply ** - Reply to a ticket\n\ - **!annonreply ** - Reply anonymously to a ticket\n\ - **!close [reason]** - Close the current thread with an optional reason\n\ - **!delete [reason]** - Delete the current thread with an optional reason\n\ - **!add_rap** - Add reminder\n\ - **!new_thread ** - Create a new ticket with a specific user\n\ - **!alert [cancel]** - Set or cancel an alert for staff in the current thread\n\ - **!force_close** - Force close the current thread if it's orphaned\n\ - **!id** - Show the user ID associated with the current thread"; + let mut docs_message = String::new(); + + for (name, command) in &handler.registry.commands { + let doc = command.doc(&config).await; + + docs_message.push_str(&format!("**{}** — {}\n\n", name, doc)) + } MessageBuilder::system_message(&ctx, config) - .content(help_message) + .content(docs_message) .to_channel(msg.channel_id) .send(false) .await?; diff --git a/rustmail/src/commands/id/slash_command/id.rs b/rustmail/src/commands/id/slash_command/id.rs index b689cdf..ee5ee2b 100644 --- a/rustmail/src/commands/id/slash_command/id.rs +++ b/rustmail/src/commands/id/slash_command/id.rs @@ -11,6 +11,7 @@ use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, ResolvedOption}; use serenity::builder::CreateCommand; use std::sync::Arc; +use serenity::FutureExt; pub struct IdCommand; @@ -20,6 +21,12 @@ impl RegistrableCommand for IdCommand { "id" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.id", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/logs/slash_command/logs.rs b/rustmail/src/commands/logs/slash_command/logs.rs index b081274..53383f0 100644 --- a/rustmail/src/commands/logs/slash_command/logs.rs +++ b/rustmail/src/commands/logs/slash_command/logs.rs @@ -10,6 +10,7 @@ use serenity::all::{ CreateCommandOption, ResolvedOption, UserId, }; use std::sync::Arc; +use serenity::FutureExt; pub struct LogsCommand; @@ -18,6 +19,12 @@ impl RegistrableCommand for LogsCommand { "logs" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.logs", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/move_thread/slash_command/move_thread.rs b/rustmail/src/commands/move_thread/slash_command/move_thread.rs index 9969121..690ed43 100644 --- a/rustmail/src/commands/move_thread/slash_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/slash_command/move_thread.rs @@ -17,6 +17,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct MoveCommand; @@ -26,6 +27,12 @@ impl RegistrableCommand for MoveCommand { "move" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.move", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index d066610..d0d3150 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -18,6 +18,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct NewThreadCommand; @@ -31,6 +32,12 @@ impl RegistrableCommand for NewThreadCommand { "new_thread" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.new_thread", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/recover/slash_command/recover.rs b/rustmail/src/commands/recover/slash_command/recover.rs index 5fb8b38..340015c 100644 --- a/rustmail/src/commands/recover/slash_command/recover.rs +++ b/rustmail/src/commands/recover/slash_command/recover.rs @@ -9,6 +9,7 @@ use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct RecoverCommand; @@ -18,6 +19,12 @@ impl RegistrableCommand for RecoverCommand { "recover" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.recover", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index 346318e..da54c81 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -12,6 +12,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct RemoveReminderCommand; @@ -20,6 +21,12 @@ impl RegistrableCommand for RemoveReminderCommand { "unremind" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.remove_reminder", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); let name = self.name(); diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index f6292be..c38e434 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -15,6 +15,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct RemoveStaffCommand; @@ -24,6 +25,12 @@ impl RegistrableCommand for RemoveStaffCommand { "delmod" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.remove_staff", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); let name = self.name(); diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index b5ee528..414e8ad 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -15,6 +15,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use serenity::FutureExt; pub struct ReplyCommand; @@ -24,6 +25,12 @@ impl RegistrableCommand for ReplyCommand { "reply" } + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { + get_translated_message(config, "help.reply", None, None, None, None).await + }.boxed() + } + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { let config = config.clone(); From 9f8e8ca3d3b5c3207e4933cceefc6857a9b77e77 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 27 Oct 2025 08:22:12 +0100 Subject: [PATCH 070/100] fix(trad): add missing '_' on help message --- rustmail/src/i18n/language/en.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 3a0ec96..33a79d8 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -871,6 +871,6 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "help.message".to_string(), - DictionaryMessage::new("## Commands:\n\n**All commands** are also available as **__slash commands_** with the **__same name__**.\n\n"), + DictionaryMessage::new("## Commands:\n\n**All commands** are also available as **__slash commands__** with the **__same name__**.\n\n"), ); } From 14911bd165acba634705d341ad1d53336d983460 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 27 Oct 2025 09:10:48 +0100 Subject: [PATCH 071/100] refactor(commands): add more help in remind command helper --- .../add_reminder/slash_command/add_reminder.rs | 18 ++++++------------ .../add_reminder/text_command/add_reminder.rs | 10 ++++------ rustmail/src/errors/dictionary.rs | 1 + rustmail/src/errors/types.rs | 2 ++ rustmail/src/i18n/language/en.rs | 6 ++++++ rustmail/src/i18n/language/fr.rs | 4 ++++ 6 files changed, 23 insertions(+), 18 deletions(-) diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index aa95d0f..2b17b88 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -15,8 +15,8 @@ use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; -use std::sync::Arc; use serenity::FutureExt; +use std::sync::Arc; pub struct AddReminderCommand; @@ -126,28 +126,22 @@ impl RegistrableCommand for AddReminderCommand { let time = match time { Some(t) => t.clone(), None => { - return Err(ModmailError::Command(CommandError::InvalidArguments( - "Missing required arguments".to_string(), - ))); + return Err(ModmailError::Command(CommandError::InvalidReminderFormat)); } }; let content = match content { Some(c) => c, None => { - return Err(ModmailError::Command(CommandError::InvalidArguments( - "Missing required arguments".to_string(), - ))); + return Err(ModmailError::Command(CommandError::InvalidReminderFormat)); } }; let time_str = time.to_string(); let re = Regex::new(r"^(?P[01]?\d|2[0-3]):(?P[0-5]\d)$").unwrap(); - let captures = re.captures(&time_str).ok_or_else(|| { - return ModmailError::Command(CommandError::InvalidArguments( - "duration".to_string(), - )); - })?; + let captures = re + .captures(&time_str) + .ok_or_else(|| ModmailError::Command(CommandError::InvalidReminderFormat))?; let hours: u32 = captures .name("hour") diff --git a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs index 2aa08e5..ae8332c 100644 --- a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs @@ -27,9 +27,7 @@ pub async fn add_reminder( match extract_reply_content(&msg.content, &config.command.prefix, &["remind", "rem"]) { Some(c) => c, None => { - return Err(ModmailError::Command(CommandError::InvalidArguments( - "No content provided".to_string(), - ))); + return Err(ModmailError::Command(CommandError::InvalidReminderFormat)); } }; @@ -38,9 +36,9 @@ pub async fn add_reminder( let reminder_content = parts.next().unwrap_or(""); let re = Regex::new(r"^(?P[01]?\d|2[0-3]):(?P[0-5]\d)$").unwrap(); - let captures = re.captures(duration_str).ok_or_else(|| { - ModmailError::Command(CommandError::InvalidArguments("duration".to_string())) - })?; + let captures = re + .captures(&duration_str) + .ok_or_else(|| ModmailError::Command(CommandError::InvalidReminderFormat))?; let hours: u32 = captures .name("hour") diff --git a/rustmail/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs index d21e43d..669292a 100644 --- a/rustmail/src/errors/dictionary.rs +++ b/rustmail/src/errors/dictionary.rs @@ -246,6 +246,7 @@ impl DictionaryManager { } CommandError::NotInThread() => ("command.not_in_thread".to_string(), None), CommandError::AlertDoesNotExist => ("alert.alert_not_found".to_string(), None), + CommandError::InvalidReminderFormat => ("add_reminder.helper".to_string(), None), _ => ("command.invalid_format".to_string(), None), }, ModmailError::Thread(thread_err) => match thread_err { diff --git a/rustmail/src/errors/types.rs b/rustmail/src/errors/types.rs index c14c265..bac4d3c 100644 --- a/rustmail/src/errors/types.rs +++ b/rustmail/src/errors/types.rs @@ -78,6 +78,7 @@ pub enum CommandError { DiscordDeleteFailed, ReminderAlreadyExists, AlertDoesNotExist, + InvalidReminderFormat, } #[derive(Debug, Clone)] @@ -232,6 +233,7 @@ impl fmt::Display for CommandError { CommandError::DiscordDeleteFailed => write!(f, "Failed to delete message on Discord"), CommandError::ReminderAlreadyExists => write!(f, "A reminder already exists"), CommandError::AlertDoesNotExist => write!(f, "Alert does not exist"), + CommandError::InvalidReminderFormat => write!(f, "Invalid reminder format"), } } } diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 33a79d8..61bdde1 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -873,4 +873,10 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "help.message".to_string(), DictionaryMessage::new("## Commands:\n\n**All commands** are also available as **__slash commands__** with the **__same name__**.\n\n"), ); + dict.messages.insert( + "add_reminder.helper".to_string(), + DictionaryMessage::new( + "Incorrect format. Use : `{prefix}remind or {prefix}rem [content]`", + ), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 17d1166..e45d27c 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -897,4 +897,8 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "help.message".to_string(), DictionaryMessage::new("## Commandes :\n\n**Toutes les commandes** disponibles sont également utilisables via des **__commandes slash__** portant le __même nom__.\n\n"), ); + dict.messages.insert( + "add_reminder.helper".to_string(), + DictionaryMessage::new("Format incorrect. Utilisation : `{prefix}remind ou {prefix}rem [contenu du rappel]`"), + ); } From f071d236e4cf06e48276e0822f0e773aaf643c4e Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 27 Oct 2025 09:56:52 +0100 Subject: [PATCH 072/100] refactor(messages): all bot's messages are now recorded in db --- rustmail/src/commands/add_reminder/common.rs | 10 ++++---- .../add_staff/text_command/add_staff.rs | 2 +- rustmail/src/commands/alert/common.rs | 3 +-- .../src/commands/close/slash_command/close.rs | 15 ++++++----- .../src/commands/close/text_command/close.rs | 18 ++++++------- rustmail/src/commands/delete/common.rs | 4 +-- .../src/commands/edit/text_command/edit.rs | 4 +-- .../src/commands/help/text_command/help.rs | 2 +- rustmail/src/commands/id/text_command/id.rs | 2 +- rustmail/src/commands/logs/common.rs | 2 +- rustmail/src/commands/move_thread/common.rs | 2 +- rustmail/src/commands/new_thread/common.rs | 8 +++--- .../commands/recover/text_command/recover.rs | 4 +-- .../text_command/remove_reminder.rs | 2 +- .../remove_staff/text_command/remove_staff.rs | 2 +- .../src/commands/reply/text_command/reply.rs | 2 +- .../src/handlers/guild_members_handler.rs | 2 +- .../src/handlers/guild_messages_handler.rs | 4 +-- .../src/handlers/guild_moderation_handler.rs | 4 +-- rustmail/src/modules/message_recovery.rs | 2 +- rustmail/src/modules/scheduled_closures.rs | 6 ++--- rustmail/src/modules/threads.rs | 4 +-- rustmail/src/utils/message/message_builder.rs | 25 +++++++------------ rustmail/src/utils/thread/send_to_thread.rs | 6 ++--- 24 files changed, 63 insertions(+), 72 deletions(-) diff --git a/rustmail/src/commands/add_reminder/common.rs b/rustmail/src/commands/add_reminder/common.rs index 05fec6c..ea3f3c5 100644 --- a/rustmail/src/commands/add_reminder/common.rs +++ b/rustmail/src/commands/add_reminder/common.rs @@ -1,5 +1,5 @@ use crate::config::Config; -use crate::db::reminders::{Reminder, is_reminder_active, update_reminder_status}; +use crate::db::reminders::{is_reminder_active, update_reminder_status, Reminder}; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; use chrono::Local; @@ -42,7 +42,7 @@ pub async fn send_register_confirmation_from_message( .await .to_channel(msg.channel_id) .footer(format!("{}: {}", "ID", reminder_id)) - .send(false) + .send(true) .await; } else { let _ = MessageBuilder::system_message(&ctx, &config) @@ -55,7 +55,7 @@ pub async fn send_register_confirmation_from_message( .await .to_channel(msg.channel_id) .footer(format!("{}: {}", "ID", reminder_id)) - .send(false) + .send(true) .await; } } @@ -177,7 +177,7 @@ pub fn spawn_reminder( .to_channel(ChannelId::new(reminder.channel_id as u64)) .color(hex_string_to_int(&config.reminders.embed_color) as u32) .mention(mentions) - .send(false) + .send(true) .await; } else { let _ = MessageBuilder::system_message(&ctx, &config) @@ -186,7 +186,7 @@ pub fn spawn_reminder( .to_channel(ChannelId::new(reminder.channel_id as u64)) .color(hex_string_to_int(&config.reminders.embed_color) as u32) .mention(mentions) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/commands/add_staff/text_command/add_staff.rs b/rustmail/src/commands/add_staff/text_command/add_staff.rs index dc93da2..4920952 100644 --- a/rustmail/src/commands/add_staff/text_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/text_command/add_staff.rs @@ -43,7 +43,7 @@ pub async fn add_staff( .translated_content("add_staff.add_success", Some(¶ms), None, None) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; Ok(()) diff --git a/rustmail/src/commands/alert/common.rs b/rustmail/src/commands/alert/common.rs index d4704f4..a29de6e 100644 --- a/rustmail/src/commands/alert/common.rs +++ b/rustmail/src/commands/alert/common.rs @@ -7,7 +7,6 @@ use crate::errors::{common, ModmailError, ModmailResult}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::colours::branding::GREEN; use serenity::all::{CommandInteraction, Context, CreateInteractionResponse, Message}; -use serenity::futures::SinkExt; use std::collections::HashMap; pub async fn get_thread_user_id_from_msg( @@ -183,7 +182,7 @@ pub async fn send_alert_message( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index eb8b8d5..8f7a6f5 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::category::{ @@ -19,10 +19,10 @@ use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, GuildId, ResolvedOption, UserId, }; +use serenity::FutureExt; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -use serenity::FutureExt; use tokio::time::sleep; pub struct CloseCommand; @@ -34,9 +34,8 @@ impl RegistrableCommand for CloseCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.close", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.close", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { @@ -304,7 +303,7 @@ impl RegistrableCommand for CloseCommand { MessageBuilder::system_message(&ctx_clone, &config_clone) .content(&config_clone.bot.close_message) .to_user(user_id_clone) - .send(false) + .send(true) .await; } let _ = channel_id.delete(&ctx_clone.http).await; @@ -352,7 +351,7 @@ impl RegistrableCommand for CloseCommand { ) .content(&config_clone2.bot.close_message) .to_user(user_id_clone) - .send(false) + .send(true) .await; } let _ = channel_id.delete(&ctx_clone2.http).await; @@ -379,7 +378,7 @@ impl RegistrableCommand for CloseCommand { let _ = MessageBuilder::system_message(&ctx, &config) .content(&config.bot.close_message) .to_user(user_id) - .send(false) + .send(true) .await; } else if !user_still_member { let mut params = HashMap::new(); diff --git a/rustmail/src/commands/close/text_command/close.rs b/rustmail/src/commands/close/text_command/close.rs index 369dc66..19aca47 100644 --- a/rustmail/src/commands/close/text_command/close.rs +++ b/rustmail/src/commands/close/text_command/close.rs @@ -91,7 +91,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } else { let _ = MessageBuilder::system_message(&ctx, config) @@ -103,7 +103,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } return Ok(()); @@ -126,7 +126,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; return Ok(()); } @@ -157,7 +157,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } else { let _ = MessageBuilder::system_message(&ctx, config) @@ -169,7 +169,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; }; @@ -263,7 +263,7 @@ pub async fn close( let _ = MessageBuilder::system_message(&ctx_clone, &config_clone) .content(&config_clone.bot.close_message) .to_user(user_id_clone) - .send(false) + .send(true) .await; } let _ = channel_id.delete(&ctx_clone.http).await; @@ -305,7 +305,7 @@ pub async fn close( ) .content(&config_clone2.bot.close_message) .to_user(user_id_clone) - .send(false) + .send(true) .await; } let _ = channel_id.delete(&ctx_clone2.http).await; @@ -331,7 +331,7 @@ pub async fn close( let _ = MessageBuilder::system_message(&ctx, config) .content(&config.bot.close_message) .to_user(user_id) - .send(false) + .send(true) .await; } else if !user_still_member { let mut params = HashMap::new(); @@ -346,7 +346,7 @@ pub async fn close( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/commands/delete/common.rs b/rustmail/src/commands/delete/common.rs index cc102a8..c55af1a 100644 --- a/rustmail/src/commands/delete/common.rs +++ b/rustmail/src/commands/delete/common.rs @@ -5,7 +5,7 @@ use crate::db::{ delete_message, get_message_ids_by_number, get_thread_by_channel_id, get_user_id_from_channel_id, update_message_numbers_after_deletion, }; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; +use crate::errors::{common, CommandError, ModmailError, ModmailResult}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ChannelId, Context, Message, MessageId, UserId}; use std::collections::HashMap; @@ -166,6 +166,6 @@ pub async fn send_delete_message( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/commands/edit/text_command/edit.rs b/rustmail/src/commands/edit/text_command/edit.rs index b5759e5..47d31eb 100644 --- a/rustmail/src/commands/edit/text_command/edit.rs +++ b/rustmail/src/commands/edit/text_command/edit.rs @@ -118,7 +118,7 @@ pub async fn edit( .await .color(hex_string_to_int(&config.thread.system_message_color) as u32) .to_channel(msg.channel_id) - .send(false) + .send(true) .await; }; @@ -154,7 +154,7 @@ pub async fn edit( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/commands/help/text_command/help.rs b/rustmail/src/commands/help/text_command/help.rs index 082306e..c5ed08a 100644 --- a/rustmail/src/commands/help/text_command/help.rs +++ b/rustmail/src/commands/help/text_command/help.rs @@ -22,7 +22,7 @@ pub async fn help( MessageBuilder::system_message(&ctx, config) .content(docs_message) .to_channel(msg.channel_id) - .send(false) + .send(true) .await?; Ok(()) diff --git a/rustmail/src/commands/id/text_command/id.rs b/rustmail/src/commands/id/text_command/id.rs index d311a37..61273be 100644 --- a/rustmail/src/commands/id/text_command/id.rs +++ b/rustmail/src/commands/id/text_command/id.rs @@ -37,7 +37,7 @@ pub async fn id( .translated_content("id.show_id", Some(¶ms), None, None) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await?; Ok(()) diff --git a/rustmail/src/commands/logs/common.rs b/rustmail/src/commands/logs/common.rs index e7ee541..e431a1a 100644 --- a/rustmail/src/commands/logs/common.rs +++ b/rustmail/src/commands/logs/common.rs @@ -101,7 +101,7 @@ pub async fn get_response( .content(content) .components(components) .to_channel(channel_id) - .send(false) + .send(true) .await } } diff --git a/rustmail/src/commands/move_thread/common.rs b/rustmail/src/commands/move_thread/common.rs index 8055523..fa7f4b8 100644 --- a/rustmail/src/commands/move_thread/common.rs +++ b/rustmail/src/commands/move_thread/common.rs @@ -89,7 +89,7 @@ pub async fn send_success_message( let _ = MessageBuilder::system_message(ctx, config) .content(confirmation_msg) .to_channel(msg.channel_id) - .send(false) + .send(true) .await; let _ = msg.delete(&ctx.http).await; diff --git a/rustmail/src/commands/new_thread/common.rs b/rustmail/src/commands/new_thread/common.rs index 5021b84..3549f08 100644 --- a/rustmail/src/commands/new_thread/common.rs +++ b/rustmail/src/commands/new_thread/common.rs @@ -60,7 +60,7 @@ pub async fn send_welcome_message( ) .await .to_channel(channel.id) - .send(false) + .send(true) .await; let _ = MessageBuilder::system_message(ctx, config) @@ -81,7 +81,7 @@ pub async fn send_dm_to_user(ctx: &Context, user: &User, config: &Config) -> Mod .translated_content("new_thread.dm_notification", None, Some(user.id), None) .await .to_user(user.id) - .send(false) + .send(true) .await; Ok(()) @@ -103,7 +103,7 @@ pub async fn send_error_message( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } @@ -135,6 +135,6 @@ pub async fn send_success_message( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/commands/recover/text_command/recover.rs b/rustmail/src/commands/recover/text_command/recover.rs index 8e88e3d..a83ad01 100644 --- a/rustmail/src/commands/recover/text_command/recover.rs +++ b/rustmail/src/commands/recover/text_command/recover.rs @@ -35,7 +35,7 @@ pub async fn recover( let _ = MessageBuilder::system_message(&ctx, config) .content(confirmation_message) .to_channel(msg.channel_id) - .send(false) + .send(true) .await; let ctx_clone = ctx.clone(); @@ -67,7 +67,7 @@ pub async fn recover( let _ = MessageBuilder::system_message(&ctx_clone, &config_clone) .content(summary_message) .to_channel(channel_id) - .send(false) + .send(true) .await; }); diff --git a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs index 71df5c6..89d70da 100644 --- a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -61,7 +61,7 @@ pub async fn remove_reminder( .translated_content("remove_reminder.confirmation", Some(¶ms), None, None) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } Err(e) => { diff --git a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs index ec53f5d..aa954a7 100644 --- a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs @@ -42,7 +42,7 @@ pub async fn remove_staff( .translated_content("add_staff.remove_success", Some(¶ms), None, None) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; Ok(()) diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index b5c5da5..5348d78 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -91,7 +91,7 @@ pub async fn reply( ) .await .to_channel(msg.channel_id) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/handlers/guild_members_handler.rs b/rustmail/src/handlers/guild_members_handler.rs index 3db3941..2c94d63 100644 --- a/rustmail/src/handlers/guild_members_handler.rs +++ b/rustmail/src/handlers/guild_members_handler.rs @@ -96,7 +96,7 @@ impl EventHandler for GuildMembersHandler { .await .to_channel(channel_id) .components(close_buttons) - .send(false) + .send(true) .await; if let Err(e) = update_thread_user_left(&thread.channel_id, pool).await { diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index cb162ab..b400427 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -290,7 +290,7 @@ impl EventHandler for GuildMessagesHandler { .to_channel(ChannelId::new( thread.channel_id.parse::().unwrap_or(0), )) - .send(false) + .send(true) .await; } @@ -418,7 +418,7 @@ impl EventHandler for GuildMessagesHandler { ) .await .to_channel(channel_id_parse) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/handlers/guild_moderation_handler.rs b/rustmail/src/handlers/guild_moderation_handler.rs index 7c245ba..f6b147c 100644 --- a/rustmail/src/handlers/guild_moderation_handler.rs +++ b/rustmail/src/handlers/guild_moderation_handler.rs @@ -256,7 +256,7 @@ async fn manage_creating_ticket_via_opening_thread( .await .components(res_button) .to_channel(channel_id_inner) - .send(false) + .send(true) .await; } @@ -299,7 +299,7 @@ impl EventHandler for GuildModerationHandler { let _ = MessageBuilder::user_message(&ctx, &self.config, user.id, user.name) .to_channel(ChannelId::new(logs_channel_id)) .content(format_audit_log(&entry)) - .send(false) + .send(true) .await; } } diff --git a/rustmail/src/modules/message_recovery.rs b/rustmail/src/modules/message_recovery.rs index 0019435..c391468 100644 --- a/rustmail/src/modules/message_recovery.rs +++ b/rustmail/src/modules/message_recovery.rs @@ -193,7 +193,7 @@ async fn recover_messages_for_thread( ) .await .to_channel(channel_id) - .send(false) + .send(true) .await; } diff --git a/rustmail/src/modules/scheduled_closures.rs b/rustmail/src/modules/scheduled_closures.rs index 55a82cf..0a9ead5 100644 --- a/rustmail/src/modules/scheduled_closures.rs +++ b/rustmail/src/modules/scheduled_closures.rs @@ -6,7 +6,7 @@ use crate::db::{ use crate::utils::message::message_builder::MessageBuilder; use chrono::Utc; use serenity::all::{ChannelId, Context, UserId}; -use tokio::time::{Duration, sleep}; +use tokio::time::{sleep, Duration}; fn schedule_one(ctx: &Context, config: &Config, thread_id: String, close_at: i64) { let now = Utc::now().timestamp(); @@ -40,7 +40,7 @@ fn schedule_one(ctx: &Context, config: &Config, thread_id: String, close_at: i64 let _ = MessageBuilder::system_message(&ctx_clone, &config_clone) .content(&config_clone.bot.close_message) .to_user(user_id) - .send(false) + .send(true) .await; } let _ = channel_id.delete(&ctx_clone.http).await; @@ -90,7 +90,7 @@ pub async fn hydrate_scheduled_closures(ctx: &Context, config: &Config) { let _ = MessageBuilder::system_message(ctx, config) .content(&config.bot.close_message) .to_user(user_id) - .send(false) + .send(true) .await; } let _ = channel_id.delete(&ctx.http).await; diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index c40c137..8e30da6 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -458,14 +458,14 @@ pub async fn handle_thread_modal_interaction( ) .await .to_channel(interaction.channel_id) - .send(false) + .send(true) .await; let _ = MessageBuilder::system_message(ctx, config) .translated_content("new_thread.dm_notification", None, Some(user_id), None) .await .to_user(user_id) - .send(false) + .send(true) .await; let mut params = HashMap::new(); diff --git a/rustmail/src/utils/message/message_builder.rs b/rustmail/src/utils/message/message_builder.rs index 9d9737a..83c0d5e 100644 --- a/rustmail/src/utils/message/message_builder.rs +++ b/rustmail/src/utils/message/message_builder.rs @@ -481,7 +481,7 @@ impl<'a> MessageBuilder<'a> { }; if to_be_recorded { - match insert_staff_message( + let _ = insert_staff_message( &self.ctx, &message, dm_message_id, @@ -492,14 +492,7 @@ impl<'a> MessageBuilder<'a> { self.config, None, ) - .await - { - Ok(_) => (), - Err(e) => { - println!("Failed to record sent message: {}", e); - return Err(ModmailError::from(e)); - } - }; + .await; } Ok(message) @@ -607,7 +600,7 @@ impl<'a> MessageBuilder<'a> { } pub async fn send_and_forget(self) { - if let Err(e) = self.send(false).await { + if let Err(e) = self.send(true).await { eprintln!("Failed to send message: {}", e); } } @@ -651,7 +644,7 @@ impl<'a> MessageBuilder<'a> { Self::system_message(ctx, config) .content(content) .to_channel(channel_id) - .send(false) + .send(true) .await } @@ -664,7 +657,7 @@ impl<'a> MessageBuilder<'a> { Self::system_message(ctx, config) .content(content) .to_user(user_id) - .send(false) + .send(true) .await } @@ -677,7 +670,7 @@ impl<'a> MessageBuilder<'a> { Self::system_message(ctx, config) .content(content) .reply_to(message) - .send(false) + .send(true) .await } } @@ -794,7 +787,7 @@ impl<'a> StaffReply<'a> { .color(hex_string_to_int(&self.config.thread.staff_message_color) as u32) .to_user(dm_user); - match dm_builder.send(false).await { + match dm_builder.send(true).await { Ok(m) => Some(m), Err(e) => { eprintln!("Failed to send DM to user: {}", e); @@ -838,7 +831,7 @@ impl<'a> StaffReply<'a> { .add_attachments(self.attachments.clone()) .to_channel(thread_channel); - let thread_msg = thread_builder.send(false).await?; + let thread_msg = thread_builder.send(true).await?; let dm_msg_opt: Option = self.build_and_send_message(top_role_name).await; @@ -990,7 +983,7 @@ impl<'a> UserIncoming<'a> { .content(self.content) .add_attachments(self.attachments) .to_channel(thread_channel); - let sent = builder.send(false).await?; + let sent = builder.send(true).await?; if let Err(e) = insert_user_message_with_ids( self.dm_msg, &sent, diff --git a/rustmail/src/utils/thread/send_to_thread.rs b/rustmail/src/utils/thread/send_to_thread.rs index 713ef98..fbee0d9 100644 --- a/rustmail/src/utils/thread/send_to_thread.rs +++ b/rustmail/src/utils/thread/send_to_thread.rs @@ -89,7 +89,7 @@ pub async fn send_to_thread( .translated_content("user.left_server", Some(¶ms), Some(msg.author.id), None) .await .to_channel(channel_id) - .send(false) + .send(true) .await; } @@ -107,7 +107,7 @@ pub async fn send_to_thread( ) .await .to_channel(channel_id) - .send(false) + .send(true) .await; } @@ -171,7 +171,7 @@ pub async fn send_to_thread( .content(full_content) .mention(mentions) .to_channel(channel_id) - .send(false) + .send(true) .await; for staff_id in &alerts { From dd0b805eef7fe6fa223466c95a947ac872d2ef52 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 27 Oct 2025 10:37:47 +0100 Subject: [PATCH 073/100] fix(panel): redirect user to desired url after login --- Cargo.lock | 1 + rustmail/src/api/handler/auth/callback.rs | 4 +++- rustmail/src/api/handler/auth/login.rs | 18 +++++++++++++----- rustmail_panel/Cargo.toml | 1 + rustmail_panel/src/pages/panel.rs | 22 ++++++++++++++-------- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af1252b..a8629f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2710,6 +2710,7 @@ dependencies = [ "serde", "serde_json", "tailyew", + "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index 0d283da..1a2f411 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -11,6 +11,7 @@ use uuid::Uuid; #[derive(serde::Deserialize)] pub struct AuthRequest { pub code: String, + pub state: Option, } #[derive(serde::Deserialize, Debug)] @@ -146,5 +147,6 @@ pub async fn handle_callback( let jar = jar.add(cookie_session); - (jar, Redirect::to("/panel")) + let target = params.state.unwrap_or_else(|| "/panel".to_string()); + (jar, Redirect::to(&target)) } diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index 3ff26f8..55e481b 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -1,16 +1,23 @@ -use crate::BotState; use crate::api::utils::get_user_id_from_session::get_user_id_from_session; -use axum::extract::State; +use crate::BotState; +use axum::extract::{Query, State}; use axum::response::Redirect; use axum_extra::extract::CookieJar; -use sqlx::{Row, query}; +use sqlx::{query, Row}; +use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; pub async fn handle_login( jar: CookieJar, State(bot_state): State>>, + Query(params): Query>, ) -> Redirect { + let redirect_after = params + .get("redirect") + .cloned() + .unwrap_or_else(|| "/panel".to_string()); + let db_pool = { let state_lock = bot_state.lock().await; @@ -54,9 +61,10 @@ pub async fn handle_login( } let url = format!( - "https://discord.com/oauth2/authorize?client_id={}&redirect_uri={}&response_type=code&scope=identify%20guilds", + "https://discord.com/oauth2/authorize?client_id={}&redirect_uri={}&response_type=code&scope=identify%20guilds&state={}", bot_config.bot.client_id, - urlencoding::encode(bot_config.bot.redirect_url.as_str()) + urlencoding::encode(bot_config.bot.redirect_url.as_str()), + urlencoding::encode(&redirect_after), ); Redirect::to(&url) diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index 3b33d96..49739dc 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -17,5 +17,6 @@ tailyew = "0.1.38" serde = { version = "1.0.228", features = ["derive"] } js-sys = "0.3.77" wasm-bindgen = "0.2.100" +urlencoding = "2.1.3" [build-dependencies] navigator = "0.3.0" diff --git a/rustmail_panel/src/pages/panel.rs b/rustmail_panel/src/pages/panel.rs index 103aa76..744e849 100644 --- a/rustmail_panel/src/pages/panel.rs +++ b/rustmail_panel/src/pages/panel.rs @@ -49,15 +49,21 @@ pub fn panel() -> Html { }); } - { - let authorized = authorized.clone(); - use_effect_with((*authorized).clone(), move |auth| { - if matches!(auth, Some(false)) { - let _ = window().location().set_href("/"); + let authorized = authorized.clone(); + use_effect_with((*authorized).clone(), move |auth| { + if matches!(auth, Some(false)) { + let win = window(); + if let Ok(pathname) = win.location().pathname() { + let redirect = urlencoding::encode(&pathname); + let _ = win + .location() + .set_href(&format!("/api/auth/login?redirect={}", redirect)); + } else { + let _ = win.location().set_href("/api/auth/login"); } - || () - }); - } + } + || () + }); let avatar = use_state(|| None::); { From 922cceb654bd0eabdb3f9706f106e43e7fdedf5d Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 27 Oct 2025 11:50:36 +0100 Subject: [PATCH 074/100] feat(commands): add take command --- rustmail/src/bot.rs | 2 + rustmail/src/commands/mod.rs | 1 + rustmail/src/commands/take/mod.rs | 2 + .../src/commands/take/slash_command/mod.rs | 1 + .../src/commands/take/slash_command/take.rs | 118 ++++++++++++++++++ .../src/commands/take/text_command/mod.rs | 1 + .../src/commands/take/text_command/take.rs | 64 ++++++++++ rustmail/src/errors/dictionary.rs | 1 + rustmail/src/errors/types.rs | 2 + .../src/handlers/guild_messages_handler.rs | 2 + rustmail/src/i18n/language/en.rs | 16 +++ rustmail/src/i18n/language/fr.rs | 16 +++ 12 files changed, 226 insertions(+) create mode 100644 rustmail/src/commands/take/mod.rs create mode 100644 rustmail/src/commands/take/slash_command/mod.rs create mode 100644 rustmail/src/commands/take/slash_command/take.rs create mode 100644 rustmail/src/commands/take/text_command/mod.rs create mode 100644 rustmail/src/commands/take/text_command/take.rs diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 04842a5..a2d92aa 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -14,6 +14,7 @@ use crate::commands::recover::slash_command::recover::RecoverCommand; use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; use crate::commands::reply::slash_command::reply::ReplyCommand; +use crate::commands::take::slash_command::take::TakeCommand; use crate::commands::CommandRegistry; use crate::config::load_config; use crate::errors::types::ConfigError; @@ -160,6 +161,7 @@ pub async fn run_bot( registry.register_command(AddReminderCommand); registry.register_command(RemoveReminderCommand); registry.register_command(LogsCommand); + registry.register_command(TakeCommand); let registry = Arc::new(registry); diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index 7b0704a..a62cb65 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -26,6 +26,7 @@ pub mod recover; pub mod remove_reminder; pub mod remove_staff; pub mod reply; +pub mod take; pub type BoxFuture<'a, T> = Pin + Send + 'a>>; diff --git a/rustmail/src/commands/take/mod.rs b/rustmail/src/commands/take/mod.rs new file mode 100644 index 0000000..571237c --- /dev/null +++ b/rustmail/src/commands/take/mod.rs @@ -0,0 +1,2 @@ +pub mod slash_command; +pub mod text_command; diff --git a/rustmail/src/commands/take/slash_command/mod.rs b/rustmail/src/commands/take/slash_command/mod.rs new file mode 100644 index 0000000..f1cf257 --- /dev/null +++ b/rustmail/src/commands/take/slash_command/mod.rs @@ -0,0 +1 @@ +pub mod take; diff --git a/rustmail/src/commands/take/slash_command/take.rs b/rustmail/src/commands/take/slash_command/take.rs new file mode 100644 index 0000000..d37fee6 --- /dev/null +++ b/rustmail/src/commands/take/slash_command/take.rs @@ -0,0 +1,118 @@ +use crate::commands::{BoxFuture, RegistrableCommand}; +use crate::config::Config; +use crate::db::threads::is_a_ticket_channel; +use crate::db::get_thread_by_channel_id; +use crate::errors::common::{database_connection_failed, thread_not_found}; +use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::{CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; +use crate::i18n::get_translated_message; +use crate::utils::command::defer_response::defer_response; +use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{ + ChannelId, CommandInteraction, Context, CreateCommand, EditChannel, ResolvedOption, +}; +use serenity::FutureExt; +use std::sync::Arc; + +pub struct TakeCommand; + +#[async_trait::async_trait] +impl RegistrableCommand for TakeCommand { + fn name(&self) -> &'static str { + "take" + } + + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { get_translated_message(config, "help.take", None, None, None, None).await } + .boxed() + } + + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { + let config = config.clone(); + + Box::pin(async move { + let cmd_desc = get_translated_message( + &config, + "slash_command.help_command_description", + None, + None, + None, + None, + ) + .await; + + vec![CreateCommand::new(self.name()).description(cmd_desc)] + }) + } + + fn run( + &self, + ctx: &Context, + command: &CommandInteraction, + _options: &[ResolvedOption<'_>], + config: &Config, + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { + let ctx = ctx.clone(); + let command = command.clone(); + let config = config.clone(); + + Box::pin(async move { + let db_pool = config + .db_pool + .as_ref() + .ok_or_else(database_connection_failed)?; + + defer_response(&ctx, &command).await?; + + if is_a_ticket_channel(command.channel_id, &db_pool).await { + let thread = match get_thread_by_channel_id( + &command.channel_id.to_string(), + db_pool, + ) + .await + { + Some(thread) => thread, + None => return Err(thread_not_found()), + }; + + let parse_thread_id = thread.channel_id.parse::().unwrap_or(0); + + let thread_id = ChannelId::new(parse_thread_id); + + let thread_name = thread_id + .name(&ctx) + .await + .unwrap_or_else(|_| "Unknown".to_string()); + + if thread_name == format!("🔵-{}", command.user.name) { + return Err(ModmailError::Command(CommandError::TicketAlreadyTaken)); + } + + let _ = thread_id + .edit( + &ctx.http, + EditChannel::new().name(format!("🔵-{}", command.user.name)), + ) + .await; + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", command.user.id)); + + let response = MessageBuilder::system_message(&ctx, &config) + .translated_content("take.confirmation", Some(¶ms), None, None) + .await + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + let _ = command.create_followup(ctx.clone(), response).await; + + Ok(()) + } else { + Err(ModmailError::Thread(NotAThreadChannel)) + } + }) + } +} diff --git a/rustmail/src/commands/take/text_command/mod.rs b/rustmail/src/commands/take/text_command/mod.rs new file mode 100644 index 0000000..f1cf257 --- /dev/null +++ b/rustmail/src/commands/take/text_command/mod.rs @@ -0,0 +1 @@ +pub mod take; diff --git a/rustmail/src/commands/take/text_command/take.rs b/rustmail/src/commands/take/text_command/take.rs new file mode 100644 index 0000000..1015e9f --- /dev/null +++ b/rustmail/src/commands/take/text_command/take.rs @@ -0,0 +1,64 @@ +use crate::config::Config; +use crate::db::get_thread_by_channel_id; +use crate::db::threads::is_a_ticket_channel; +use crate::errors::common::{database_connection_failed, thread_not_found}; +use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::{CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; +use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{ChannelId, Context, Message}; +use serenity::builder::EditChannel; +use std::sync::Arc; + +pub async fn take( + ctx: Context, + msg: Message, + config: &Config, + _handler: Arc, +) -> ModmailResult<()> { + let db_pool = config + .db_pool + .as_ref() + .ok_or_else(database_connection_failed)?; + + if is_a_ticket_channel(msg.channel_id, &db_pool).await { + let thread = match get_thread_by_channel_id(&msg.channel_id.to_string(), db_pool).await { + Some(thread) => thread, + None => return Err(thread_not_found()), + }; + + let parse_thread_id = thread.channel_id.parse::().unwrap_or(0); + + let thread_id = ChannelId::new(parse_thread_id); + + let thread_name = thread_id + .name(&ctx) + .await + .unwrap_or_else(|_| "Unknown".to_string()); + + if thread_name == format!("🔵-{}", msg.author.name) { + return Err(ModmailError::Command(CommandError::TicketAlreadyTaken)); + } + + let _ = thread_id + .edit( + &ctx.http, + EditChannel::new().name(format!("🔵-{}", msg.author.name)), + ) + .await; + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); + + let _ = MessageBuilder::system_message(&ctx, config) + .translated_content("take.confirmation", Some(¶ms), None, None) + .await + .to_channel(msg.channel_id) + .send(true) + .await?; + + Ok(()) + } else { + Err(ModmailError::Thread(NotAThreadChannel)) + } +} diff --git a/rustmail/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs index 669292a..dcb0306 100644 --- a/rustmail/src/errors/dictionary.rs +++ b/rustmail/src/errors/dictionary.rs @@ -247,6 +247,7 @@ impl DictionaryManager { CommandError::NotInThread() => ("command.not_in_thread".to_string(), None), CommandError::AlertDoesNotExist => ("alert.alert_not_found".to_string(), None), CommandError::InvalidReminderFormat => ("add_reminder.helper".to_string(), None), + CommandError::TicketAlreadyTaken => ("take.ticket_already_taken".to_string(), None), _ => ("command.invalid_format".to_string(), None), }, ModmailError::Thread(thread_err) => match thread_err { diff --git a/rustmail/src/errors/types.rs b/rustmail/src/errors/types.rs index bac4d3c..fd03fe1 100644 --- a/rustmail/src/errors/types.rs +++ b/rustmail/src/errors/types.rs @@ -79,6 +79,7 @@ pub enum CommandError { ReminderAlreadyExists, AlertDoesNotExist, InvalidReminderFormat, + TicketAlreadyTaken, } #[derive(Debug, Clone)] @@ -234,6 +235,7 @@ impl fmt::Display for CommandError { CommandError::ReminderAlreadyExists => write!(f, "A reminder already exists"), CommandError::AlertDoesNotExist => write!(f, "Alert does not exist"), CommandError::InvalidReminderFormat => write!(f, "Invalid reminder format"), + CommandError::TicketAlreadyTaken => write!(f, "Ticket already taken"), } } } diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index b400427..8c686c0 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -16,6 +16,7 @@ use crate::commands::recover::text_command::recover::recover; use crate::commands::remove_reminder::text_command::remove_reminder::remove_reminder; use crate::commands::remove_staff::text_command::remove_staff::remove_staff; use crate::commands::reply::text_command::reply::reply; +use crate::commands::take::text_command::take::take; use crate::commands::CommandRegistry; use crate::config::Config; use crate::db::messages::get_thread_message_by_dm_message_id; @@ -100,6 +101,7 @@ impl GuildMessagesHandler { wrap_command!(lock, ["remind", "rem"], add_reminder); wrap_command!(lock, ["unremind", "urem"], remove_reminder); wrap_command!(lock, "logs", logs); + wrap_command!(lock, "take", take); drop(lock); h diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 61bdde1..628fb46 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -873,10 +873,26 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "help.message".to_string(), DictionaryMessage::new("## Commands:\n\n**All commands** are also available as **__slash commands__** with the **__same name__**.\n\n"), ); + dict.messages.insert( + "help.take".to_string(), + DictionaryMessage::new("Allows you to take ownership of a ticket by replacing its name with yours. To take a ticket, use `!take` in the ticket."), + ); dict.messages.insert( "add_reminder.helper".to_string(), DictionaryMessage::new( "Incorrect format. Use : `{prefix}remind or {prefix}rem [content]`", ), ); + dict.messages.insert( + "take.ticket_already_taken".to_string(), + DictionaryMessage::new("You have already taken this ticket."), + ); + dict.messages.insert( + "take.confirmation".to_string(), + DictionaryMessage::new("The ticket is now taken by {staff}."), + ); + dict.messages.insert( + "slash_command.help_command_description".to_string(), + DictionaryMessage::new("Take ownership of the current ticket."), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index e45d27c..2d40cc1 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -897,8 +897,24 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "help.message".to_string(), DictionaryMessage::new("## Commandes :\n\n**Toutes les commandes** disponibles sont également utilisables via des **__commandes slash__** portant le __même nom__.\n\n"), ); + dict.messages.insert( + "help.take".to_string(), + DictionaryMessage::new("Permet de prendre en charge un ticket en remplaçant le nom de celui-ci par le vôtre. Pour prendre en charge un ticket, faites `!take` dans le ticket."), + ); dict.messages.insert( "add_reminder.helper".to_string(), DictionaryMessage::new("Format incorrect. Utilisation : `{prefix}remind ou {prefix}rem [contenu du rappel]`"), ); + dict.messages.insert( + "take.ticket_already_taken".to_string(), + DictionaryMessage::new("Vous avez déjà pris en charge ce ticket."), + ); + dict.messages.insert( + "take.confirmation".to_string(), + DictionaryMessage::new("Le ticket est maintenant pris en charge par {staff}."), + ); + dict.messages.insert( + "slash_command.help_command_description".to_string(), + DictionaryMessage::new("Prendre en charge le ticket actuel"), + ); } From 6e4cf42d1dd3b03edaeafe5dbdd17bc9db413d9b Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 27 Oct 2025 13:31:26 +0100 Subject: [PATCH 075/100] feat(panel): add markdown support for ticket page --- Cargo.lock | 224 +++++++++++++++++++++++- rustmail_panel/Cargo.toml | 2 + rustmail_panel/src/components/ticket.rs | 3 +- rustmail_panel/src/main.rs | 5 +- rustmail_panel/src/utils/markdown.rs | 13 ++ rustmail_panel/src/utils/mod.rs | 1 + 6 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 rustmail_panel/src/utils/markdown.rs create mode 100644 rustmail_panel/src/utils/mod.rs diff --git a/Cargo.lock b/Cargo.lock index c5425b8..8a0119a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,19 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ammonia" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" +dependencies = [ + "cssparser", + "html5ever", + "maplit", + "tendril", + "url", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -302,7 +315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" dependencies = [ "chrono", - "phf", + "phf 0.12.1", ] [[package]] @@ -435,6 +448,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.11.3", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.101", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -505,6 +541,21 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "either" version = "1.15.0" @@ -644,6 +695,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.31" @@ -1350,6 +1411,17 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever", + "match_token", +] + [[package]] name = "http" version = "0.2.12" @@ -1807,6 +1879,40 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "matchit" version = "0.8.4" @@ -1903,6 +2009,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acd7563c38e388f6d2a568ae781a71ca4e957768655923526d2e0d64a67bc4d6" +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2060,13 +2172,65 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared 0.11.3", +] + [[package]] name = "phf" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ - "phf_shared", + "phf_shared 0.12.1", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", ] [[package]] @@ -2172,6 +2336,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.34" @@ -2646,11 +2816,13 @@ dependencies = [ name = "rustmail_panel" version = "0.1.0" dependencies = [ + "ammonia", "gloo-net 0.6.0", "gloo-utils 0.2.0", "i18nrs", "js-sys", "navigator", + "pulldown-cmark 0.13.0", "serde", "serde_json", "tailyew", @@ -3218,6 +3390,31 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + [[package]] name = "stringprep" version = "0.1.5" @@ -3366,6 +3563,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4029,6 +4237,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf 0.11.3", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "webpki-roots" version = "0.25.4" diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index 49739dc..9796784 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -18,5 +18,7 @@ serde = { version = "1.0.228", features = ["derive"] } js-sys = "0.3.77" wasm-bindgen = "0.2.100" urlencoding = "2.1.3" +ammonia = "4.1.2" +pulldown-cmark = "0.13.0" [build-dependencies] navigator = "0.3.0" diff --git a/rustmail_panel/src/components/ticket.rs b/rustmail_panel/src/components/ticket.rs index 7e3b700..3271079 100644 --- a/rustmail_panel/src/components/ticket.rs +++ b/rustmail_panel/src/components/ticket.rs @@ -1,3 +1,4 @@ +use crate::utils::markdown::markdown_to_html_safe; use gloo_net::http::Request; use i18nrs::yew::use_translation; use js_sys::Date; @@ -300,7 +301,7 @@ pub fn ticket_details(props: &TicketDetailsProps) -> Html { { &m.user_name } { &m.created_at } -

{ &m.content }

+

{ markdown_to_html_safe(&m.content) }

}) } diff --git a/rustmail_panel/src/main.rs b/rustmail_panel/src/main.rs index 3d107b1..f2fd5d8 100644 --- a/rustmail_panel/src/main.rs +++ b/rustmail_panel/src/main.rs @@ -1,10 +1,11 @@ mod components; mod pages; mod router; +mod utils; -use crate::router::{Route, switch}; -use i18nrs::StorageType; +use crate::router::{switch, Route}; use i18nrs::yew::I18nProvider; +use i18nrs::StorageType; use std::collections::HashMap; use yew::prelude::*; use yew_router::prelude::*; diff --git a/rustmail_panel/src/utils/markdown.rs b/rustmail_panel/src/utils/markdown.rs new file mode 100644 index 0000000..58179d7 --- /dev/null +++ b/rustmail_panel/src/utils/markdown.rs @@ -0,0 +1,13 @@ +use ammonia::clean; +use pulldown_cmark::{html, Parser}; +use yew::{AttrValue, Html}; + +pub fn markdown_to_html_safe(markdown_input: &str) -> Html { + println!("Markdown input: {}", markdown_input); + let parser = Parser::new(markdown_input); + let mut html_output = String::new(); + html::push_html(&mut html_output, parser); + + let sanitized = clean(&html_output); + Html::from_html_unchecked(AttrValue::from(sanitized)) +} diff --git a/rustmail_panel/src/utils/mod.rs b/rustmail_panel/src/utils/mod.rs new file mode 100644 index 0000000..163a4fb --- /dev/null +++ b/rustmail_panel/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod markdown; From 262b975bd3540f9b09772a2d0dfaadbc77f2f921 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Mon, 27 Oct 2025 15:01:19 +0100 Subject: [PATCH 076/100] feat(commands): add partial release command --- rustmail/src/bot.rs | 2 + rustmail/src/commands/mod.rs | 1 + rustmail/src/commands/release/mod.rs | 2 + .../src/commands/release/slash_command/mod.rs | 1 + .../commands/release/slash_command/release.rs | 115 ++++++++++++++++++ .../src/commands/release/text_command/mod.rs | 1 + .../commands/release/text_command/release.rs | 61 ++++++++++ rustmail/src/errors/dictionary.rs | 3 + rustmail/src/errors/types.rs | 2 + .../src/handlers/guild_messages_handler.rs | 2 + rustmail/src/i18n/language/en.rs | 22 +++- rustmail/src/i18n/language/fr.rs | 18 ++- 12 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 rustmail/src/commands/release/mod.rs create mode 100644 rustmail/src/commands/release/slash_command/mod.rs create mode 100644 rustmail/src/commands/release/slash_command/release.rs create mode 100644 rustmail/src/commands/release/text_command/mod.rs create mode 100644 rustmail/src/commands/release/text_command/release.rs diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index a2d92aa..69a8bb5 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -11,6 +11,7 @@ use crate::commands::logs::slash_command::logs::LogsCommand; use crate::commands::move_thread::slash_command::move_thread::MoveCommand; use crate::commands::new_thread::slash_command::new_thread::NewThreadCommand; use crate::commands::recover::slash_command::recover::RecoverCommand; +use crate::commands::release::slash_command::release::ReleaseCommand; use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; use crate::commands::reply::slash_command::reply::ReplyCommand; @@ -162,6 +163,7 @@ pub async fn run_bot( registry.register_command(RemoveReminderCommand); registry.register_command(LogsCommand); registry.register_command(TakeCommand); + registry.register_command(ReleaseCommand); let registry = Arc::new(registry); diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index a62cb65..b3b3545 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -23,6 +23,7 @@ pub mod logs; pub mod move_thread; pub mod new_thread; pub mod recover; +pub mod release; pub mod remove_reminder; pub mod remove_staff; pub mod reply; diff --git a/rustmail/src/commands/release/mod.rs b/rustmail/src/commands/release/mod.rs new file mode 100644 index 0000000..571237c --- /dev/null +++ b/rustmail/src/commands/release/mod.rs @@ -0,0 +1,2 @@ +pub mod slash_command; +pub mod text_command; diff --git a/rustmail/src/commands/release/slash_command/mod.rs b/rustmail/src/commands/release/slash_command/mod.rs new file mode 100644 index 0000000..f35143a --- /dev/null +++ b/rustmail/src/commands/release/slash_command/mod.rs @@ -0,0 +1 @@ +pub mod release; diff --git a/rustmail/src/commands/release/slash_command/release.rs b/rustmail/src/commands/release/slash_command/release.rs new file mode 100644 index 0000000..67b0076 --- /dev/null +++ b/rustmail/src/commands/release/slash_command/release.rs @@ -0,0 +1,115 @@ +use crate::commands::{BoxFuture, RegistrableCommand}; +use crate::config::Config; +use crate::db::get_thread_by_channel_id; +use crate::db::threads::is_a_ticket_channel; +use crate::errors::common::{database_connection_failed, thread_not_found}; +use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::{CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_interaction_handler::InteractionHandler; +use crate::i18n::get_translated_message; +use crate::utils::command::defer_response::defer_response; +use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{ + ChannelId, CommandInteraction, Context, CreateCommand, EditChannel, ResolvedOption, +}; +use serenity::FutureExt; +use std::sync::Arc; + +pub struct ReleaseCommand; + +#[async_trait::async_trait] +impl RegistrableCommand for ReleaseCommand { + fn name(&self) -> &'static str { + "release" + } + + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { get_translated_message(config, "help.release", None, None, None, None).await } + .boxed() + } + + fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { + let config = config.clone(); + + Box::pin(async move { + let cmd_desc = get_translated_message( + &config, + "slash_command.release_command_description", + None, + None, + None, + None, + ) + .await; + + vec![CreateCommand::new(self.name()).description(cmd_desc)] + }) + } + + fn run( + &self, + ctx: &Context, + command: &CommandInteraction, + _options: &[ResolvedOption<'_>], + config: &Config, + _handler: Arc, + ) -> BoxFuture<'_, ModmailResult<()>> { + let ctx = ctx.clone(); + let command = command.clone(); + let config = config.clone(); + + Box::pin(async move { + let db_pool = config + .db_pool + .as_ref() + .ok_or_else(database_connection_failed)?; + + defer_response(&ctx, &command).await?; + + if is_a_ticket_channel(command.channel_id, &db_pool).await { + let thread = match get_thread_by_channel_id( + &command.channel_id.to_string(), + db_pool, + ) + .await + { + Some(thread) => thread, + None => return Err(thread_not_found()), + }; + + let parse_thread_id = thread.channel_id.parse::().unwrap_or(0); + + let thread_id = ChannelId::new(parse_thread_id); + + let thread_name = thread_id + .name(&ctx) + .await + .unwrap_or_else(|_| "Unknown".to_string()); + + if thread_name == thread.user_name { + return Err(ModmailError::Command(CommandError::TicketAlreadyReleased)); + } + + let _ = thread_id + .edit(&ctx.http, EditChannel::new().name(thread.user_name)) + .await; + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", command.user.id)); + + let response = MessageBuilder::system_message(&ctx, &config) + .translated_content("release.confirmation", Some(¶ms), None, None) + .await + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + let _ = command.create_followup(ctx.clone(), response).await; + + Ok(()) + } else { + Err(ModmailError::Thread(NotAThreadChannel)) + } + }) + } +} diff --git a/rustmail/src/commands/release/text_command/mod.rs b/rustmail/src/commands/release/text_command/mod.rs new file mode 100644 index 0000000..f35143a --- /dev/null +++ b/rustmail/src/commands/release/text_command/mod.rs @@ -0,0 +1 @@ +pub mod release; diff --git a/rustmail/src/commands/release/text_command/release.rs b/rustmail/src/commands/release/text_command/release.rs new file mode 100644 index 0000000..1d9d234 --- /dev/null +++ b/rustmail/src/commands/release/text_command/release.rs @@ -0,0 +1,61 @@ +use crate::config::Config; +use crate::db::get_thread_by_channel_id; +use crate::db::threads::is_a_ticket_channel; +use crate::errors::common::{database_connection_failed, thread_not_found}; +use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::{CommandError, ModmailError, ModmailResult}; +use crate::handlers::guild_messages_handler::GuildMessagesHandler; +use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{ChannelId, Context, Message}; +use serenity::builder::EditChannel; +use std::sync::Arc; + +pub async fn release( + ctx: Context, + msg: Message, + config: &Config, + _handler: Arc, +) -> ModmailResult<()> { + let db_pool = config + .db_pool + .as_ref() + .ok_or_else(database_connection_failed)?; + + if is_a_ticket_channel(msg.channel_id, &db_pool).await { + let thread = match get_thread_by_channel_id(&msg.channel_id.to_string(), db_pool).await { + Some(thread) => thread, + None => return Err(thread_not_found()), + }; + + let parse_thread_id = thread.channel_id.parse::().unwrap_or(0); + + let thread_id = ChannelId::new(parse_thread_id); + + let thread_name = thread_id + .name(&ctx) + .await + .unwrap_or_else(|_| "Unknown".to_string()); + + if thread_name == thread.user_name { + return Err(ModmailError::Command(CommandError::TicketAlreadyReleased)); + } + + let _ = thread_id + .edit(&ctx.http, EditChannel::new().name(thread.user_name)) + .await; + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); + + let _ = MessageBuilder::system_message(&ctx, config) + .translated_content("release.confirmation", Some(¶ms), None, None) + .await + .to_channel(msg.channel_id) + .send(true) + .await?; + + Ok(()) + } else { + Err(ModmailError::Thread(NotAThreadChannel)) + } +} diff --git a/rustmail/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs index dcb0306..33e7ace 100644 --- a/rustmail/src/errors/dictionary.rs +++ b/rustmail/src/errors/dictionary.rs @@ -248,6 +248,9 @@ impl DictionaryManager { CommandError::AlertDoesNotExist => ("alert.alert_not_found".to_string(), None), CommandError::InvalidReminderFormat => ("add_reminder.helper".to_string(), None), CommandError::TicketAlreadyTaken => ("take.ticket_already_taken".to_string(), None), + CommandError::TicketAlreadyReleased => { + ("release.ticket_already_taken".to_string(), None) + } _ => ("command.invalid_format".to_string(), None), }, ModmailError::Thread(thread_err) => match thread_err { diff --git a/rustmail/src/errors/types.rs b/rustmail/src/errors/types.rs index fd03fe1..2d8e78a 100644 --- a/rustmail/src/errors/types.rs +++ b/rustmail/src/errors/types.rs @@ -80,6 +80,7 @@ pub enum CommandError { AlertDoesNotExist, InvalidReminderFormat, TicketAlreadyTaken, + TicketAlreadyReleased, } #[derive(Debug, Clone)] @@ -236,6 +237,7 @@ impl fmt::Display for CommandError { CommandError::AlertDoesNotExist => write!(f, "Alert does not exist"), CommandError::InvalidReminderFormat => write!(f, "Invalid reminder format"), CommandError::TicketAlreadyTaken => write!(f, "Ticket already taken"), + CommandError::TicketAlreadyReleased => write!(f, "Ticket already released"), } } } diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 8c686c0..a66b740 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -13,6 +13,7 @@ use crate::commands::logs::text_command::logs::logs; use crate::commands::move_thread::text_command::move_thread::move_thread; use crate::commands::new_thread::text_command::new_thread::new_thread; use crate::commands::recover::text_command::recover::recover; +use crate::commands::release::text_command::release::release; use crate::commands::remove_reminder::text_command::remove_reminder::remove_reminder; use crate::commands::remove_staff::text_command::remove_staff::remove_staff; use crate::commands::reply::text_command::reply::reply; @@ -102,6 +103,7 @@ impl GuildMessagesHandler { wrap_command!(lock, ["unremind", "urem"], remove_reminder); wrap_command!(lock, "logs", logs); wrap_command!(lock, "take", take); + wrap_command!(lock, "release", release); drop(lock); h diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 628fb46..6381f28 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -877,6 +877,10 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "help.take".to_string(), DictionaryMessage::new("Allows you to take ownership of a ticket by replacing its name with yours. To take a ticket, use `!take` in the ticket."), ); + dict.messages.insert( + "help.release".to_string(), + DictionaryMessage::new("Releases ownership of a ticket previously taken with the `!take` command. To release a ticket, use `!release` in the ticket."), + ); dict.messages.insert( "add_reminder.helper".to_string(), DictionaryMessage::new( @@ -892,7 +896,23 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { DictionaryMessage::new("The ticket is now taken by {staff}."), ); dict.messages.insert( - "slash_command.help_command_description".to_string(), + "slash_command.take_command_description".to_string(), + DictionaryMessage::new("Take ownership of the current ticket."), + ); + dict.messages.insert( + "slash_command.take_command_description".to_string(), DictionaryMessage::new("Take ownership of the current ticket."), ); + dict.messages.insert( + "slash_command.release_command_description".to_string(), + DictionaryMessage::new("Release ownership of the current ticket."), + ); + dict.messages.insert( + "release.ticket_already_taken".to_string(), + DictionaryMessage::new("The ticket is not taken by anyone."), + ); + dict.messages.insert( + "release.confirmation".to_string(), + DictionaryMessage::new("The ticket has been released by {staff}."), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 2d40cc1..8229847 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -901,6 +901,10 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "help.take".to_string(), DictionaryMessage::new("Permet de prendre en charge un ticket en remplaçant le nom de celui-ci par le vôtre. Pour prendre en charge un ticket, faites `!take` dans le ticket."), ); + dict.messages.insert( + "help.release".to_string(), + DictionaryMessage::new("Permet de ne plus prendre en charge un ticket pris en charge via la commande `take`. Pour libérer un ticket, faites `!release` dans le ticket."), + ); dict.messages.insert( "add_reminder.helper".to_string(), DictionaryMessage::new("Format incorrect. Utilisation : `{prefix}remind ou {prefix}rem [contenu du rappel]`"), @@ -914,7 +918,19 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { DictionaryMessage::new("Le ticket est maintenant pris en charge par {staff}."), ); dict.messages.insert( - "slash_command.help_command_description".to_string(), + "slash_command.take_command_description".to_string(), DictionaryMessage::new("Prendre en charge le ticket actuel"), ); + dict.messages.insert( + "slash_command.release_command_description".to_string(), + DictionaryMessage::new("Ne plus prendre en charge le ticket actuel"), + ); + dict.messages.insert( + "release.ticket_already_taken".to_string(), + DictionaryMessage::new("Le ticket n'est pris en charge par personne."), + ); + dict.messages.insert( + "release.confirmation".to_string(), + DictionaryMessage::new("Le ticket n'est plus pris en charge par {staff}."), + ); } From a8a9a395e93d4a5c7120e77a4bde7bb3ed75fabe Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 28 Oct 2025 09:31:38 +0100 Subject: [PATCH 077/100] feat(commands): add complete release command --- .../commands/release/slash_command/release.rs | 19 +++-- .../commands/release/text_command/release.rs | 16 ++-- rustmail/src/commands/take/common.rs | 75 +++++++++++++++++++ rustmail/src/commands/take/mod.rs | 1 + .../src/commands/take/slash_command/take.rs | 24 +++--- .../src/commands/take/text_command/take.rs | 19 +++-- rustmail/src/i18n/language/en.rs | 7 ++ rustmail/src/i18n/language/fr.rs | 4 + 8 files changed, 134 insertions(+), 31 deletions(-) create mode 100644 rustmail/src/commands/take/common.rs diff --git a/rustmail/src/commands/release/slash_command/release.rs b/rustmail/src/commands/release/slash_command/release.rs index 67b0076..c61ab27 100644 --- a/rustmail/src/commands/release/slash_command/release.rs +++ b/rustmail/src/commands/release/slash_command/release.rs @@ -1,18 +1,17 @@ +use crate::commands::take::common::rename_channel_with_timeout; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; -use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::{CommandError, ModmailError, ModmailResult}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; -use serenity::all::{ - ChannelId, CommandInteraction, Context, CreateCommand, EditChannel, ResolvedOption, -}; use serenity::FutureExt; +use serenity::all::{ChannelId, CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; pub struct ReleaseCommand; @@ -90,9 +89,15 @@ impl RegistrableCommand for ReleaseCommand { return Err(ModmailError::Command(CommandError::TicketAlreadyReleased)); } - let _ = thread_id - .edit(&ctx.http, EditChannel::new().name(thread.user_name)) - .await; + rename_channel_with_timeout( + &ctx, + &config, + thread_id, + thread.user_name.clone(), + None, + Some(&command), + ) + .await?; let mut params = std::collections::HashMap::new(); params.insert("staff".to_string(), format!("<@{}>", command.user.id)); diff --git a/rustmail/src/commands/release/text_command/release.rs b/rustmail/src/commands/release/text_command/release.rs index 1d9d234..05b1b51 100644 --- a/rustmail/src/commands/release/text_command/release.rs +++ b/rustmail/src/commands/release/text_command/release.rs @@ -1,13 +1,13 @@ +use crate::commands::take::common::rename_channel_with_timeout; use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; -use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::{CommandError, ModmailError, ModmailResult}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ChannelId, Context, Message}; -use serenity::builder::EditChannel; use std::sync::Arc; pub async fn release( @@ -40,9 +40,15 @@ pub async fn release( return Err(ModmailError::Command(CommandError::TicketAlreadyReleased)); } - let _ = thread_id - .edit(&ctx.http, EditChannel::new().name(thread.user_name)) - .await; + rename_channel_with_timeout( + &ctx, + &config, + thread_id, + thread.user_name.clone(), + Some(&msg), + None, + ) + .await?; let mut params = std::collections::HashMap::new(); params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); diff --git a/rustmail/src/commands/take/common.rs b/rustmail/src/commands/take/common.rs new file mode 100644 index 0000000..d9bdbd9 --- /dev/null +++ b/rustmail/src/commands/take/common.rs @@ -0,0 +1,75 @@ +use crate::config::Config; +use crate::errors::{DiscordError, ModmailError, ModmailResult}; +use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{ChannelId, CommandInteraction, Context, EditChannel, Message}; +use std::time::Duration; +use tokio::time::sleep; + +pub async fn rename_channel_with_timeout( + ctx: &Context, + config: &Config, + channel_id: ChannelId, + new_name: String, + msg: Option<&Message>, + command: Option<&CommandInteraction>, +) -> ModmailResult<()> { + let rename_future = channel_id.edit( + &ctx.http, + EditChannel::new().name(format!("🔵-{}", new_name)), + ); + let timeout = sleep(Duration::from_secs(2)); + + tokio::select! { + res = rename_future => { + if let Err(e) = res { + return Err(ModmailError::Discord(DiscordError::ApiError(e.to_string()))); + } + return Ok(()); + } + _ = timeout => { + let message_response: Option = if let Some(message) = msg { + let response = MessageBuilder::system_message(ctx, config) + .translated_content("take.timeout", None, None, None).await + .to_channel(message.channel_id) + .send(true) + .await; + + match response { + Ok(msg) => Some(msg), + Err(_) => None, + } + } else { + None + }; + + let command_response: Option = if let Some(command) = command { + let message = MessageBuilder::system_message(ctx, config) + .translated_content("take.timeout", None, None, None).await + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + match command.create_followup(&ctx.http, message).await { + Ok(msg) => Some(msg), + Err(_) => None, + } + } else { + None + }; + + if let Err(e) = channel_id.edit(&ctx.http, EditChannel::new().name(format!("🔵-{}", new_name))).await { + return Err(ModmailError::Discord(DiscordError::ApiError(e.to_string()))); + } + + if let Some(msg) = message_response { + let _ = msg.delete(&ctx.http).await; + } + + if let Some(command) = command_response { + let _ = command.delete(&ctx.http).await; + } + + return Ok(()); + } + } +} diff --git a/rustmail/src/commands/take/mod.rs b/rustmail/src/commands/take/mod.rs index 571237c..de6da7a 100644 --- a/rustmail/src/commands/take/mod.rs +++ b/rustmail/src/commands/take/mod.rs @@ -1,2 +1,3 @@ +pub mod common; pub mod slash_command; pub mod text_command; diff --git a/rustmail/src/commands/take/slash_command/take.rs b/rustmail/src/commands/take/slash_command/take.rs index d37fee6..a919d92 100644 --- a/rustmail/src/commands/take/slash_command/take.rs +++ b/rustmail/src/commands/take/slash_command/take.rs @@ -1,18 +1,17 @@ +use crate::commands::take::common::rename_channel_with_timeout; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::db::threads::is_a_ticket_channel; use crate::db::get_thread_by_channel_id; -use crate::errors::common::{database_connection_failed, thread_not_found}; +use crate::db::threads::is_a_ticket_channel; use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::{CommandError, ModmailError, ModmailResult}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; -use serenity::all::{ - ChannelId, CommandInteraction, Context, CreateCommand, EditChannel, ResolvedOption, -}; use serenity::FutureExt; +use serenity::all::{ChannelId, CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; pub struct TakeCommand; @@ -90,12 +89,15 @@ impl RegistrableCommand for TakeCommand { return Err(ModmailError::Command(CommandError::TicketAlreadyTaken)); } - let _ = thread_id - .edit( - &ctx.http, - EditChannel::new().name(format!("🔵-{}", command.user.name)), - ) - .await; + rename_channel_with_timeout( + &ctx, + &config, + thread_id, + command.user.name.clone(), + None, + Some(&command), + ) + .await?; let mut params = std::collections::HashMap::new(); params.insert("staff".to_string(), format!("<@{}>", command.user.id)); diff --git a/rustmail/src/commands/take/text_command/take.rs b/rustmail/src/commands/take/text_command/take.rs index 1015e9f..c16d324 100644 --- a/rustmail/src/commands/take/text_command/take.rs +++ b/rustmail/src/commands/take/text_command/take.rs @@ -1,13 +1,13 @@ +use crate::commands::take::common::rename_channel_with_timeout; use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; -use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::{CommandError, ModmailError, ModmailResult}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ChannelId, Context, Message}; -use serenity::builder::EditChannel; use std::sync::Arc; pub async fn take( @@ -40,12 +40,15 @@ pub async fn take( return Err(ModmailError::Command(CommandError::TicketAlreadyTaken)); } - let _ = thread_id - .edit( - &ctx.http, - EditChannel::new().name(format!("🔵-{}", msg.author.name)), - ) - .await; + rename_channel_with_timeout( + &ctx, + config, + thread_id, + msg.author.name.clone(), + Some(&msg), + None, + ) + .await?; let mut params = std::collections::HashMap::new(); params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 6381f28..e61a621 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -895,6 +895,13 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "take.confirmation".to_string(), DictionaryMessage::new("The ticket is now taken by {staff}."), ); + dict.messages.insert( + "take.timeout".to_string(), + DictionaryMessage::new( + "⚠️ **Discord’s API** enforces a limit of **2** channel updates every **10 minutes**. + The action will be **__automatically__** applied once the cooldown expires.", + ), + ); dict.messages.insert( "slash_command.take_command_description".to_string(), DictionaryMessage::new("Take ownership of the current ticket."), diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 8229847..db2192a 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -917,6 +917,10 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "take.confirmation".to_string(), DictionaryMessage::new("Le ticket est maintenant pris en charge par {staff}."), ); + dict.messages.insert( + "take.timeout".to_string(), + DictionaryMessage::new("⚠️ **L’API de Discord** impose une limite de **2** changements de salon toutes les **10 minutes**. L’action sera appliquée **__automatiquement__** dès que le délai sera écoulé."), + ); dict.messages.insert( "slash_command.take_command_description".to_string(), DictionaryMessage::new("Prendre en charge le ticket actuel"), From 05dda65dd27a7bd0e461ff6fec619ba4125e1f13 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 28 Oct 2025 09:31:54 +0100 Subject: [PATCH 078/100] refactor(bot): reformat code --- rustmail/src/api/handler/auth/login.rs | 4 ++-- rustmail/src/bot.rs | 6 +++--- rustmail/src/commands/add_reminder/common.rs | 2 +- .../add_reminder/slash_command/add_reminder.rs | 6 +++--- .../add_reminder/text_command/add_reminder.rs | 4 ++-- .../add_staff/slash_command/add_staff.rs | 4 ++-- .../commands/add_staff/text_command/add_staff.rs | 2 +- rustmail/src/commands/alert/common.rs | 2 +- .../src/commands/alert/slash_command/alert.rs | 9 ++++----- .../src/commands/alert/text_command/alert.rs | 2 +- .../commands/anonreply/text_command/anonreply.rs | 4 ++-- .../src/commands/close/slash_command/close.rs | 4 ++-- .../src/commands/close/text_command/close.rs | 2 +- rustmail/src/commands/delete/common.rs | 2 +- .../src/commands/delete/slash_command/delete.rs | 9 ++++----- .../src/commands/delete/text_command/delete.rs | 2 +- rustmail/src/commands/edit/slash_command/edit.rs | 9 ++++----- rustmail/src/commands/edit/text_command/edit.rs | 4 ++-- .../force_close/slash_command/force_close.rs | 4 ++-- .../force_close/text_command/force_close.rs | 2 +- rustmail/src/commands/help/slash_command/help.rs | 16 ++++++++-------- rustmail/src/commands/id/slash_command/id.rs | 7 +++---- rustmail/src/commands/id/text_command/id.rs | 2 +- rustmail/src/commands/logs/slash_command/logs.rs | 7 +++---- .../move_thread/slash_command/move_thread.rs | 7 +++---- .../move_thread/text_command/move_thread.rs | 2 +- .../new_thread/slash_command/new_thread.rs | 4 ++-- .../new_thread/text_command/new_thread.rs | 2 +- .../commands/recover/slash_command/recover.rs | 9 ++++----- .../src/commands/recover/text_command/recover.rs | 2 +- .../slash_command/remove_reminder.rs | 7 ++++--- .../text_command/remove_reminder.rs | 2 +- .../remove_staff/slash_command/remove_staff.rs | 4 ++-- .../remove_staff/text_command/remove_staff.rs | 2 +- .../src/commands/reply/slash_command/reply.rs | 11 +++++------ .../src/commands/reply/text_command/reply.rs | 4 ++-- rustmail/src/handlers/guild_messages_handler.rs | 6 +++--- rustmail/src/modules/scheduled_closures.rs | 2 +- rustmail_panel/src/main.rs | 4 ++-- rustmail_panel/src/utils/markdown.rs | 2 +- 40 files changed, 89 insertions(+), 96 deletions(-) diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index 55e481b..8c2d9c0 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -1,9 +1,9 @@ -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; use crate::BotState; +use crate::api::utils::get_user_id_from_session::get_user_id_from_session; use axum::extract::{Query, State}; use axum::response::Redirect; use axum_extra::extract::CookieJar; -use sqlx::{query, Row}; +use sqlx::{Row, query}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 69a8bb5..2cf3965 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -1,3 +1,4 @@ +use crate::commands::CommandRegistry; use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; use crate::commands::alert::slash_command::alert::AlertCommand; @@ -16,10 +17,9 @@ use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveRemi use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; use crate::commands::reply::slash_command::reply::ReplyCommand; use crate::commands::take::slash_command::take::TakeCommand; -use crate::commands::CommandRegistry; use crate::config::load_config; -use crate::errors::types::ConfigError; use crate::errors::ModmailError; +use crate::errors::types::ConfigError; use crate::handlers::guild_handler::GuildHandler; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::handlers::guild_members_handler::GuildMembersHandler; @@ -30,7 +30,7 @@ use crate::handlers::ready_handler::ReadyHandler; use crate::handlers::typing_proxy_handler::TypingProxyHandler; use crate::panel_commands::user::is_member::is_member; use crate::types::logs::PaginationContext; -use crate::{db, BotCommand, BotState, BotStatus}; +use crate::{BotCommand, BotState, BotStatus, db}; use base64::Engine; use rand::RngCore; use serenity::all::{ClientBuilder, GatewayIntents}; diff --git a/rustmail/src/commands/add_reminder/common.rs b/rustmail/src/commands/add_reminder/common.rs index ea3f3c5..fe4bae9 100644 --- a/rustmail/src/commands/add_reminder/common.rs +++ b/rustmail/src/commands/add_reminder/common.rs @@ -1,5 +1,5 @@ use crate::config::Config; -use crate::db::reminders::{is_reminder_active, update_reminder_status, Reminder}; +use crate::db::reminders::{Reminder, is_reminder_active, update_reminder_status}; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; use chrono::Local; diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index 2b17b88..913fcba 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -3,19 +3,19 @@ use crate::commands::add_reminder::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::db::reminders::{insert_reminder, Reminder}; +use crate::db::reminders::{Reminder, insert_reminder}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use chrono::{Local, NaiveTime, TimeZone}; use regex::Regex; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; -use serenity::FutureExt; use std::sync::Arc; pub struct AddReminderCommand; diff --git a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs index ae8332c..f5fd48c 100644 --- a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs @@ -2,9 +2,9 @@ use crate::commands::add_reminder::common::{ send_register_confirmation_from_message, spawn_reminder, }; use crate::config::Config; -use crate::db::reminders::{insert_reminder, Reminder}; +use crate::db::reminders::{Reminder, insert_reminder}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::command::extract_reply_content::extract_reply_content; use chrono::{Local, NaiveTime, TimeZone}; diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index eed8177..58d8470 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -4,18 +4,18 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct AddStaffCommand; diff --git a/rustmail/src/commands/add_staff/text_command/add_staff.rs b/rustmail/src/commands/add_staff/text_command/add_staff.rs index 4920952..8db61f9 100644 --- a/rustmail/src/commands/add_staff/text_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/text_command/add_staff.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; diff --git a/rustmail/src/commands/alert/common.rs b/rustmail/src/commands/alert/common.rs index a29de6e..e25cbeb 100644 --- a/rustmail/src/commands/alert/common.rs +++ b/rustmail/src/commands/alert/common.rs @@ -3,7 +3,7 @@ use crate::db::{cancel_alert_for_staff, get_user_id_from_channel_id, set_alert_f use crate::errors::CommandError::AlertDoesNotExist; use crate::errors::DatabaseError::QueryFailed; use crate::errors::DiscordError::ApiError; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::colours::branding::GREEN; use serenity::all::{CommandInteraction, Context, CreateInteractionResponse, Message}; diff --git a/rustmail/src/commands/alert/slash_command/alert.rs b/rustmail/src/commands/alert/slash_command/alert.rs index b53e2db..c0767c6 100644 --- a/rustmail/src/commands/alert/slash_command/alert.rs +++ b/rustmail/src/commands/alert/slash_command/alert.rs @@ -4,16 +4,16 @@ use crate::commands::alert::common::{ }; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; use std::sync::Arc; -use serenity::FutureExt; pub struct AlertCommand; @@ -24,9 +24,8 @@ impl RegistrableCommand for AlertCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.alert", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.alert", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/alert/text_command/alert.rs b/rustmail/src/commands/alert/text_command/alert.rs index ff28262..a78a46c 100644 --- a/rustmail/src/commands/alert/text_command/alert.rs +++ b/rustmail/src/commands/alert/text_command/alert.rs @@ -3,7 +3,7 @@ use crate::commands::alert::common::{ handle_set_alert_from_msg, }; use crate::config::Config; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/anonreply/text_command/anonreply.rs b/rustmail/src/commands/anonreply/text_command/anonreply.rs index 66d38d9..6ca6a5c 100644 --- a/rustmail/src/commands/anonreply/text_command/anonreply.rs +++ b/rustmail/src/commands/anonreply/text_command/anonreply.rs @@ -1,10 +1,10 @@ use crate::config::Config; use crate::db::operations::allocate_next_message_number; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; +use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index 8f7a6f5..f5007b0 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::category::{ @@ -15,11 +15,11 @@ use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::fetch_thread::fetch_thread; use chrono::Utc; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, GuildId, ResolvedOption, UserId, }; -use serenity::FutureExt; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; diff --git a/rustmail/src/commands/close/text_command/close.rs b/rustmail/src/commands/close/text_command/close.rs index 19aca47..0417ff7 100644 --- a/rustmail/src/commands/close/text_command/close.rs +++ b/rustmail/src/commands/close/text_command/close.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::db::{ close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, }; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::category::{ get_category_id_from_message, get_category_name_from_message, diff --git a/rustmail/src/commands/delete/common.rs b/rustmail/src/commands/delete/common.rs index c55af1a..cad6074 100644 --- a/rustmail/src/commands/delete/common.rs +++ b/rustmail/src/commands/delete/common.rs @@ -5,7 +5,7 @@ use crate::db::{ delete_message, get_message_ids_by_number, get_thread_by_channel_id, get_user_id_from_channel_id, update_message_numbers_after_deletion, }; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{ChannelId, Context, Message, MessageId, UserId}; use std::collections::HashMap; diff --git a/rustmail/src/commands/delete/slash_command/delete.rs b/rustmail/src/commands/delete/slash_command/delete.rs index 9621eb3..f4eeece 100644 --- a/rustmail/src/commands/delete/slash_command/delete.rs +++ b/rustmail/src/commands/delete/slash_command/delete.rs @@ -5,18 +5,18 @@ use crate::commands::delete::common::{ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::messages::get_thread_message_by_message_id; -use crate::errors::{common, MessageError, ModmailError, ModmailResult}; +use crate::errors::{MessageError, ModmailError, ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response_ephemeral; use crate::utils::message::message_builder::MessageBuilder; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct DeleteCommand; @@ -27,9 +27,8 @@ impl RegistrableCommand for DeleteCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.delete", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.delete", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/delete/text_command/delete.rs b/rustmail/src/commands/delete/text_command/delete.rs index bfa9b0f..82a4936 100644 --- a/rustmail/src/commands/delete/text_command/delete.rs +++ b/rustmail/src/commands/delete/text_command/delete.rs @@ -3,7 +3,7 @@ use crate::commands::delete::common::{ get_thread_info, send_delete_message, update_message_numbers, }; use crate::config::Config; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/edit/slash_command/edit.rs b/rustmail/src/commands/edit/slash_command/edit.rs index a5a87dc..1bbeae9 100644 --- a/rustmail/src/commands/edit/slash_command/edit.rs +++ b/rustmail/src/commands/edit/slash_command/edit.rs @@ -4,19 +4,19 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::{get_thread_message_by_inbox_message_id, update_message_content}; use crate::errors::common::message_not_found; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct EditCommand; @@ -27,9 +27,8 @@ impl RegistrableCommand for EditCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.edit", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.edit", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/edit/text_command/edit.rs b/rustmail/src/commands/edit/text_command/edit.rs index 47d31eb..f8a82a7 100644 --- a/rustmail/src/commands/edit/text_command/edit.rs +++ b/rustmail/src/commands/edit/text_command/edit.rs @@ -3,13 +3,13 @@ use crate::commands::edit::message_ops::{ cleanup_command_message, edit_messages, format_new_message, get_message_ids, }; use crate::commands::edit::validation::{ - parse_edit_command, validate_edit_permissions, EditCommandInput, + EditCommandInput, parse_edit_command, validate_edit_permissions, }; use crate::config::Config; use crate::db::get_thread_message_by_inbox_message_id; use crate::db::update_message_content; use crate::errors::common::message_not_found; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::conversion::hex_string_to_int::hex_string_to_int; use crate::utils::message::message_builder::MessageBuilder; diff --git a/rustmail/src/commands/force_close/slash_command/force_close.rs b/rustmail/src/commands/force_close/slash_command/force_close.rs index 7f068fb..466c2da 100644 --- a/rustmail/src/commands/force_close/slash_command/force_close.rs +++ b/rustmail/src/commands/force_close/slash_command/force_close.rs @@ -4,12 +4,12 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; +use serenity::FutureExt; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; -use serenity::FutureExt; pub struct ForceCloseCommand; diff --git a/rustmail/src/commands/force_close/text_command/force_close.rs b/rustmail/src/commands/force_close/text_command/force_close.rs index 50c3ee9..fc5bccb 100644 --- a/rustmail/src/commands/force_close/text_command/force_close.rs +++ b/rustmail/src/commands/force_close/text_command/force_close.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; use crate::errors::DatabaseError::QueryFailed; use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/help/slash_command/help.rs b/rustmail/src/commands/help/slash_command/help.rs index 8d1b52d..c0702a1 100644 --- a/rustmail/src/commands/help/slash_command/help.rs +++ b/rustmail/src/commands/help/slash_command/help.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; -use std::error::Error; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::errors::ModmailResult; @@ -7,10 +5,12 @@ use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; -use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; -use std::sync::Arc; use serenity::FutureExt; +use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use serenity::futures::future::join_all; +use std::collections::HashMap; +use std::error::Error; +use std::sync::Arc; pub struct HelpCommand; @@ -21,9 +21,8 @@ impl RegistrableCommand for HelpCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.help", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.help", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { @@ -59,7 +58,8 @@ impl RegistrableCommand for HelpCommand { Box::pin(async move { let mut docs_message = String::new(); - let welcome_msg = get_translated_message(&config, "help.message", None, None, None, None).await; + let welcome_msg = + get_translated_message(&config, "help.message", None, None, None, None).await; docs_message.push_str(&welcome_msg); let futures = handler.registry.commands.iter().map(|(name, command)| { diff --git a/rustmail/src/commands/id/slash_command/id.rs b/rustmail/src/commands/id/slash_command/id.rs index ee5ee2b..e232c8b 100644 --- a/rustmail/src/commands/id/slash_command/id.rs +++ b/rustmail/src/commands/id/slash_command/id.rs @@ -8,10 +8,10 @@ use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; +use serenity::FutureExt; use serenity::all::{CommandInteraction, Context, ResolvedOption}; use serenity::builder::CreateCommand; use std::sync::Arc; -use serenity::FutureExt; pub struct IdCommand; @@ -22,9 +22,8 @@ impl RegistrableCommand for IdCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.id", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.id", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/id/text_command/id.rs b/rustmail/src/commands/id/text_command/id.rs index 61273be..034c79a 100644 --- a/rustmail/src/commands/id/text_command/id.rs +++ b/rustmail/src/commands/id/text_command/id.rs @@ -1,8 +1,8 @@ use crate::config::Config; use crate::db::get_thread_by_channel_id; use crate::db::threads::is_a_ticket_channel; -use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::ThreadError::NotAThreadChannel; +use crate::errors::common::{database_connection_failed, thread_not_found}; use crate::errors::{ModmailError, ModmailResult}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; diff --git a/rustmail/src/commands/logs/slash_command/logs.rs b/rustmail/src/commands/logs/slash_command/logs.rs index 53383f0..beea161 100644 --- a/rustmail/src/commands/logs/slash_command/logs.rs +++ b/rustmail/src/commands/logs/slash_command/logs.rs @@ -5,12 +5,12 @@ use crate::errors::{DatabaseError, ModmailError, ModmailResult}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, UserId, }; use std::sync::Arc; -use serenity::FutureExt; pub struct LogsCommand; @@ -20,9 +20,8 @@ impl RegistrableCommand for LogsCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.logs", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.logs", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/move_thread/slash_command/move_thread.rs b/rustmail/src/commands/move_thread/slash_command/move_thread.rs index 690ed43..4309839 100644 --- a/rustmail/src/commands/move_thread/slash_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/slash_command/move_thread.rs @@ -11,13 +11,13 @@ use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct MoveCommand; @@ -28,9 +28,8 @@ impl RegistrableCommand for MoveCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.move", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.move", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/move_thread/text_command/move_thread.rs b/rustmail/src/commands/move_thread/text_command/move_thread.rs index 253b2e5..6811c0a 100644 --- a/rustmail/src/commands/move_thread/text_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/text_command/move_thread.rs @@ -5,7 +5,7 @@ use crate::commands::move_thread::common::{ use crate::config::Config; use crate::errors::CommandError::NotInThread; use crate::errors::ThreadError::CategoryNotFound; -use crate::errors::{common, CommandError, DiscordError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, DiscordError, ModmailError, ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index d0d3150..b6132d2 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -4,7 +4,7 @@ use crate::config::Config; use crate::db::logs::get_logs_from_user_id; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; use crate::errors::{ - common, CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, + CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, common, }; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; @@ -12,13 +12,13 @@ use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; use crate::utils::thread::user_recap::get_user_recap; use crate::utils::time::get_member_join_date::get_member_join_date_for_user; +use serenity::FutureExt; use serenity::all::{ ChannelId, CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Context, CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup, GuildId, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct NewThreadCommand; diff --git a/rustmail/src/commands/new_thread/text_command/new_thread.rs b/rustmail/src/commands/new_thread/text_command/new_thread.rs index 6c6cb42..1e86e78 100644 --- a/rustmail/src/commands/new_thread/text_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/text_command/new_thread.rs @@ -5,7 +5,7 @@ use crate::commands::new_thread::common::{ use crate::config::Config; use crate::db::logs::get_logs_from_user_id; use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; -use crate::errors::{common, DiscordError, ModmailError, ModmailResult}; +use crate::errors::{DiscordError, ModmailError, ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::i18n::get_translated_message; use crate::utils::message::message_builder::MessageBuilder; diff --git a/rustmail/src/commands/recover/slash_command/recover.rs b/rustmail/src/commands/recover/slash_command/recover.rs index 340015c..5435de2 100644 --- a/rustmail/src/commands/recover/slash_command/recover.rs +++ b/rustmail/src/commands/recover/slash_command/recover.rs @@ -1,15 +1,15 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; +use serenity::FutureExt; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct RecoverCommand; @@ -20,9 +20,8 @@ impl RegistrableCommand for RecoverCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.recover", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.recover", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/recover/text_command/recover.rs b/rustmail/src/commands/recover/text_command/recover.rs index a83ad01..54e2dac 100644 --- a/rustmail/src/commands/recover/text_command/recover.rs +++ b/rustmail/src/commands/recover/text_command/recover.rs @@ -1,5 +1,5 @@ use crate::config::Config; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::i18n::get_translated_message; use crate::modules::message_recovery::recover_missing_messages; diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index da54c81..9d83f2a 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -1,18 +1,18 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct RemoveReminderCommand; @@ -24,7 +24,8 @@ impl RegistrableCommand for RemoveReminderCommand { fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { async move { get_translated_message(config, "help.remove_reminder", None, None, None, None).await - }.boxed() + } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs index 89d70da..98915a1 100644 --- a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{common, CommandError, DatabaseError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index c38e434..b1b6b23 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -4,18 +4,18 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{common, CommandError, ModmailError, ModmailResult}; +use crate::errors::{CommandError, ModmailError, ModmailResult, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Context, CreateCommand, CreateCommandOption, CreateInteractionResponseFollowup, ResolvedOption, }; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct RemoveStaffCommand; diff --git a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs index aa954a7..b46d68a 100644 --- a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs @@ -3,7 +3,7 @@ use crate::config::Config; use crate::db::thread_exists; use crate::errors::CommandError::InvalidFormat; use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{common, ModmailError, ModmailResult}; +use crate::errors::{ModmailError, ModmailResult, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message, UserId}; diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index 414e8ad..e7de5ab 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -2,20 +2,20 @@ use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; +use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; use crate::utils::thread::fetch_thread::fetch_thread; +use serenity::FutureExt; use serenity::all::{ Attachment, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, GuildId, ResolvedOption, UserId, }; use std::collections::HashMap; use std::sync::Arc; -use serenity::FutureExt; pub struct ReplyCommand; @@ -26,9 +26,8 @@ impl RegistrableCommand for ReplyCommand { } fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { - async move { - get_translated_message(config, "help.reply", None, None, None, None).await - }.boxed() + async move { get_translated_message(config, "help.reply", None, None, None, None).await } + .boxed() } fn register(&self, config: &Config) -> BoxFuture<'_, Vec> { diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index 5348d78..d36e2fa 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -1,11 +1,11 @@ use crate::config::Config; use crate::db::allocate_next_message_number; use crate::errors::MessageError::MessageEmpty; -use crate::errors::{common, CommandError, ModmailError, ModmailResult, ThreadError}; +use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; use crate::handlers::guild_messages_handler::GuildMessagesHandler; use crate::utils::command::extract_reply_content::extract_reply_content; use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{extract_intent, ReplyIntent}; +use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; use crate::utils::thread::fetch_thread::fetch_thread; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index a66b740..9061448 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -1,3 +1,4 @@ +use crate::commands::CommandRegistry; use crate::commands::add_reminder::text_command::add_reminder::add_reminder; use crate::commands::add_staff::text_command::add_staff::add_staff; use crate::commands::alert::text_command::alert::alert; @@ -18,7 +19,6 @@ use crate::commands::remove_reminder::text_command::remove_reminder::remove_remi use crate::commands::remove_staff::text_command::remove_staff::remove_staff; use crate::commands::reply::text_command::reply::reply; use crate::commands::take::text_command::take::take; -use crate::commands::CommandRegistry; use crate::config::Config; use crate::db::messages::get_thread_message_by_dm_message_id; use crate::db::operations::messages::get_thread_message_by_message_id; @@ -27,7 +27,7 @@ use crate::db::operations::{ }; use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; use crate::db::threads::get_thread_by_user_id; -use crate::errors::{common, ModmailResult}; +use crate::errors::{ModmailResult, common}; use crate::i18n::get_translated_message; use crate::types::logs::PaginationStore; use crate::utils::message::message_builder::MessageBuilder; @@ -42,8 +42,8 @@ use serenity::{ use std::collections::HashSet; use std::sync::{LazyLock, Mutex}; use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; -use tokio::sync::watch::Receiver; use tokio::sync::Mutex as AsyncMutex; +use tokio::sync::watch::Receiver; static SUPPRESSED_DELETES: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); diff --git a/rustmail/src/modules/scheduled_closures.rs b/rustmail/src/modules/scheduled_closures.rs index 0a9ead5..b672020 100644 --- a/rustmail/src/modules/scheduled_closures.rs +++ b/rustmail/src/modules/scheduled_closures.rs @@ -6,7 +6,7 @@ use crate::db::{ use crate::utils::message::message_builder::MessageBuilder; use chrono::Utc; use serenity::all::{ChannelId, Context, UserId}; -use tokio::time::{sleep, Duration}; +use tokio::time::{Duration, sleep}; fn schedule_one(ctx: &Context, config: &Config, thread_id: String, close_at: i64) { let now = Utc::now().timestamp(); diff --git a/rustmail_panel/src/main.rs b/rustmail_panel/src/main.rs index f2fd5d8..325e30e 100644 --- a/rustmail_panel/src/main.rs +++ b/rustmail_panel/src/main.rs @@ -3,9 +3,9 @@ mod pages; mod router; mod utils; -use crate::router::{switch, Route}; -use i18nrs::yew::I18nProvider; +use crate::router::{Route, switch}; use i18nrs::StorageType; +use i18nrs::yew::I18nProvider; use std::collections::HashMap; use yew::prelude::*; use yew_router::prelude::*; diff --git a/rustmail_panel/src/utils/markdown.rs b/rustmail_panel/src/utils/markdown.rs index 58179d7..8935294 100644 --- a/rustmail_panel/src/utils/markdown.rs +++ b/rustmail_panel/src/utils/markdown.rs @@ -1,5 +1,5 @@ use ammonia::clean; -use pulldown_cmark::{html, Parser}; +use pulldown_cmark::{Parser, html}; use yew::{AttrValue, Html}; pub fn markdown_to_html_safe(markdown_input: &str) -> Html { From a315045e7d563ce53ce127082aa3a0b2c2905f10 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 28 Oct 2025 11:04:49 +0100 Subject: [PATCH 079/100] refactor(commands): add 'command' argument to help command --- rustmail/src/commands/help/common.rs | 93 +++++++++++++++++++ rustmail/src/commands/help/mod.rs | 1 + .../src/commands/help/slash_command/help.rs | 82 ++++++++++------ .../src/commands/help/text_command/help.rs | 31 ++++--- rustmail/src/i18n/language/en.rs | 8 +- rustmail/src/i18n/language/fr.rs | 8 +- 6 files changed, 177 insertions(+), 46 deletions(-) create mode 100644 rustmail/src/commands/help/common.rs diff --git a/rustmail/src/commands/help/common.rs b/rustmail/src/commands/help/common.rs new file mode 100644 index 0000000..012d425 --- /dev/null +++ b/rustmail/src/commands/help/common.rs @@ -0,0 +1,93 @@ +use crate::commands::CommandRegistry; +use crate::config::Config; +use crate::errors::{CommandError, ModmailError, ModmailResult}; +use crate::i18n::get_translated_message; +use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{CommandInteraction, Context, Message}; +use std::sync::Arc; + +pub async fn display_commands_list( + ctx: &Context, + config: &Config, + registry: Arc, + msg: Option<&Message>, + command: Option<&CommandInteraction>, +) -> ModmailResult<()> { + let mut docs_message = String::new(); + + let welcome_msg = get_translated_message(&config, "help.message", None, None, None, None).await; + docs_message.push_str(&welcome_msg); + + for (name, _) in ®istry.commands { + docs_message.push_str(&format!("- **{}**\n", name)) + } + + if let Some(msg) = msg { + let _ = MessageBuilder::system_message(&ctx, config) + .content(docs_message) + .to_channel(msg.channel_id) + .send(true) + .await; + + return Ok(()); + } + + if let Some(command) = command { + let response = MessageBuilder::system_message(&ctx, config) + .content(docs_message) + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + command.create_followup(&ctx.http, response).await?; + + return Ok(()); + } + + println!("No valid message or command interaction provided."); + Ok(()) +} + +pub async fn display_command_help( + ctx: &Context, + config: &Config, + registry: Arc, + msg: Option<&Message>, + command: Option<&CommandInteraction>, + command_name: &str, +) -> ModmailResult<()> { + if let Some(cmd) = registry.commands.get(command_name) { + let command_doc = cmd.doc(config).await; + let mut docs_message = String::new(); + docs_message.push_str(&format!("**{}**\n\n", command_name)); + docs_message.push_str(&command_doc); + + if let Some(msg) = msg { + let _ = MessageBuilder::system_message(&ctx, config) + .content(docs_message) + .to_channel(msg.channel_id) + .send(true) + .await; + + return Ok(()); + } + + if let Some(command) = command { + let response = MessageBuilder::system_message(&ctx, config) + .content(docs_message) + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + command.create_followup(&ctx.http, response).await?; + return Ok(()); + } + + println!("No valid message or command interaction provided."); + Ok(()) + } else { + Err(ModmailError::Command(CommandError::UnknownCommand( + format!("{}", command_name), + ))) + } +} diff --git a/rustmail/src/commands/help/mod.rs b/rustmail/src/commands/help/mod.rs index 571237c..de6da7a 100644 --- a/rustmail/src/commands/help/mod.rs +++ b/rustmail/src/commands/help/mod.rs @@ -1,2 +1,3 @@ +pub mod common; pub mod slash_command; pub mod text_command; diff --git a/rustmail/src/commands/help/slash_command/help.rs b/rustmail/src/commands/help/slash_command/help.rs index c0702a1..016b5f4 100644 --- a/rustmail/src/commands/help/slash_command/help.rs +++ b/rustmail/src/commands/help/slash_command/help.rs @@ -1,15 +1,15 @@ +use crate::commands::help::common::{display_command_help, display_commands_list}; use crate::commands::{BoxFuture, RegistrableCommand}; use crate::config::Config; use crate::errors::ModmailResult; use crate::handlers::guild_interaction_handler::InteractionHandler; use crate::i18n::get_translated_message; use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use serenity::all::{ + CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, + CreateCommandOption, ResolvedOption, +}; use serenity::FutureExt; -use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; -use serenity::futures::future::join_all; -use std::collections::HashMap; -use std::error::Error; use std::sync::Arc; pub struct HelpCommand; @@ -38,8 +38,22 @@ impl RegistrableCommand for HelpCommand { None, ) .await; + let cmd_arg_desc = get_translated_message( + &config, + "slash_command.help_command_argument_desc", + None, + None, + None, + None, + ) + .await; - vec![CreateCommand::new("help").description(cmd_desc)] + vec![ + CreateCommand::new("help").description(cmd_desc).add_option( + CreateCommandOption::new(CommandOptionType::String, "command", cmd_arg_desc) + .required(false), + ), + ] }) } @@ -56,33 +70,41 @@ impl RegistrableCommand for HelpCommand { let config = config.clone(); Box::pin(async move { - let mut docs_message = String::new(); - - let welcome_msg = - get_translated_message(&config, "help.message", None, None, None, None).await; - docs_message.push_str(&welcome_msg); - - let futures = handler.registry.commands.iter().map(|(name, command)| { - let config = config.clone(); - - async move { - let doc = command.doc(&config).await; - format!("**{}** — {}\n\n", name, doc) - } - }); - - let results = join_all(futures).await; - docs_message.push_str(&results.join("")); - defer_response(&ctx, &command).await?; - let response = MessageBuilder::system_message(&ctx, &config) - .content(docs_message) - .to_channel(command.channel_id) - .build_interaction_message_followup() - .await; + let mut command_name: Option = None; - let _ = command.create_followup(&ctx.http, response).await; + for option in &command.data.options { + match option.name.as_str() { + "command" => { + if let CommandDataOptionValue::String(val) = &option.value { + command_name.replace(val.clone()); + } + } + _ => {} + } + } + + if let Some(cmd_name) = command_name { + display_command_help( + &ctx, + &config, + handler.registry.clone(), + None, + Some(&command), + &cmd_name, + ) + .await?; + } else { + display_commands_list( + &ctx, + &config, + handler.registry.clone(), + None, + Some(&command), + ) + .await?; + } Ok(()) }) diff --git a/rustmail/src/commands/help/text_command/help.rs b/rustmail/src/commands/help/text_command/help.rs index c5ed08a..388ab41 100644 --- a/rustmail/src/commands/help/text_command/help.rs +++ b/rustmail/src/commands/help/text_command/help.rs @@ -1,29 +1,36 @@ +use crate::commands::help::common::{display_command_help, display_commands_list}; use crate::config::Config; use crate::errors::ModmailResult; use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::message::message_builder::MessageBuilder; use serenity::all::{Context, Message}; use std::sync::Arc; +fn extract_request_command_name(command: &str) -> &str { + let parts: Vec<&str> = command.trim().split_whitespace().collect(); + if parts.len() > 1 { parts[1] } else { "" } +} + pub async fn help( ctx: Context, msg: Message, config: &Config, handler: Arc, ) -> ModmailResult<()> { - let mut docs_message = String::new(); - - for (name, command) in &handler.registry.commands { - let doc = command.doc(&config).await; + let command_name = extract_request_command_name(&msg.content); - docs_message.push_str(&format!("**{}** — {}\n\n", name, doc)) - } - - MessageBuilder::system_message(&ctx, config) - .content(docs_message) - .to_channel(msg.channel_id) - .send(true) + if command_name.is_empty() { + display_commands_list(&ctx, config, handler.registry.clone(), Some(&msg), None).await?; + } else { + display_command_help( + &ctx, + config, + handler.registry.clone(), + Some(&msg), + None, + command_name, + ) .await?; + } Ok(()) } diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index e61a621..57af912 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -835,7 +835,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "help.help".to_string(), - DictionaryMessage::new("Displays a list of all available commands with a short description. To view the help message, use `!help`."), + DictionaryMessage::new("Displays a list of all available commands with a short description. To view the help message, use `!help`. If you want help with a specific command, type `!help `."), ); dict.messages.insert( "help.id".to_string(), @@ -871,7 +871,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "help.message".to_string(), - DictionaryMessage::new("## Commands:\n\n**All commands** are also available as **__slash commands__** with the **__same name__**.\n\n"), + DictionaryMessage::new("## Commands:\n\n**All commands** are also available as **__slash commands__** with the **__same name__**.\n\nIf you want help with a specific command, type `!help `.\n\n"), ); dict.messages.insert( "help.take".to_string(), @@ -922,4 +922,8 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "release.confirmation".to_string(), DictionaryMessage::new("The ticket has been released by {staff}."), ); + dict.messages.insert( + "slash_command.help_command_argument_desc".to_string(), + DictionaryMessage::new("The command to get help with"), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index db2192a..250bae6 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -859,7 +859,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "help.help".to_string(), - DictionaryMessage::new("Affiche une liste de toutes les commandes disponibles avec une brève description. Pour afficher le message d'aide, faites `!help`."), + DictionaryMessage::new("Affiche une liste de toutes les commandes disponibles avec une brève description. Pour afficher le message d'aide, faites `!help`. Si vous souhaitez obtenir de l'aide sur une commande spécifique, faites `!help `."), ); dict.messages.insert( "help.id".to_string(), @@ -895,7 +895,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "help.message".to_string(), - DictionaryMessage::new("## Commandes :\n\n**Toutes les commandes** disponibles sont également utilisables via des **__commandes slash__** portant le __même nom__.\n\n"), + DictionaryMessage::new("## Commandes :\n\n**Toutes les commandes** disponibles sont également utilisables via des **__commandes slash__** portant le __même nom__.\n\nSi vous souhaitez obtenir de l'aide sur une commande spécifique, faites `!help `.\n\n") ); dict.messages.insert( "help.take".to_string(), @@ -937,4 +937,8 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "release.confirmation".to_string(), DictionaryMessage::new("Le ticket n'est plus pris en charge par {staff}."), ); + dict.messages.insert( + "slash_command.help_command_argument_desc".to_string(), + DictionaryMessage::new("Le nom de la commande pour laquelle vous souhaitez de l'aide"), + ); } From b02ee8723d9f1fd271603a662f40f42dd4d4b1e8 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 28 Oct 2025 11:56:36 +0100 Subject: [PATCH 080/100] fix(modules): defered all modals interactions --- rustmail/src/modules/threads.rs | 107 ++++++++++++++------------------ 1 file changed, 48 insertions(+), 59 deletions(-) diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index 8e30da6..b3d80c7 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -10,8 +10,8 @@ use crate::utils::thread::send_to_thread::send_to_thread; use crate::utils::thread::user_recap::get_user_recap; use crate::utils::time::get_member_join_date::get_member_join_date_for_user; use serenity::all::{ - ActionRowComponent, Channel, ChannelId, ComponentInteraction, Context, CreateChannel, GuildId, - Message, ModalInteraction, UserId, + ActionRowComponent, Channel, ChannelId, ComponentInteraction, Context, CreateChannel, + CreateInteractionResponseFollowup, GuildId, Message, ModalInteraction, UserId, }; use serenity::builder::{CreateInteractionResponse, EditChannel, EditMessage}; use std::collections::HashMap; @@ -175,21 +175,6 @@ pub async fn handle_thread_modal_interaction( let parts = match parse_thread_interaction(&interaction.data.custom_id) { Some(parts) => parts, None => { - let response = CreateInteractionResponse::Message( - MessageBuilder::system_message(&ctx, &config) - .translated_content( - "feature.not_implemented", - None, - Some(interaction.user.id), - interaction.guild_id.map(|g| g.get()), - ) - .await - .to_channel(interaction.channel_id) - .build_interaction_message() - .await - .ephemeral(true), - ); - let _ = interaction.create_response(&ctx.http, response).await; return Ok(()); } }; @@ -197,6 +182,10 @@ pub async fn handle_thread_modal_interaction( let key = interaction.channel_id.get(); let lock = get_thread_lock(config, key); + interaction + .create_response(&ctx.http, CreateInteractionResponse::Acknowledge) + .await?; + let guard = match lock.try_lock() { Ok(guard) => guard, Err(_) => { @@ -240,9 +229,9 @@ pub async fn handle_thread_modal_interaction( Err(_) => { eprintln!("Invalid user ID provided in modal interaction: {}", id); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content( "thread.modal_invalid_user_id", @@ -252,7 +241,7 @@ pub async fn handle_thread_modal_interaction( ) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -271,9 +260,9 @@ pub async fn handle_thread_modal_interaction( Err(_) => { eprintln!("Failed to fetch user by ID: {}", user_id); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content( "thread.modal_user_not_found", @@ -283,7 +272,7 @@ pub async fn handle_thread_modal_interaction( ) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -299,14 +288,14 @@ pub async fn handle_thread_modal_interaction( user_id ); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.modal_bot_user", None, None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -330,9 +319,9 @@ pub async fn handle_thread_modal_interaction( format!("<#{}>", existing_channel_str), ); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content( "thread.already_exists", @@ -342,7 +331,7 @@ pub async fn handle_thread_modal_interaction( ) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -356,9 +345,9 @@ pub async fn handle_thread_modal_interaction( Ok(Channel::Guild(gc)) => gc, _ => { let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content( "thread.not_a_thread_channel", @@ -368,7 +357,7 @@ pub async fn handle_thread_modal_interaction( ) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -382,9 +371,9 @@ pub async fn handle_thread_modal_interaction( if let Some(parent_id) = guild_channel.parent_id { if parent_id.get() != config.thread.inbox_category_id { let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content( "thread.not_a_thread_channel", @@ -394,7 +383,7 @@ pub async fn handle_thread_modal_interaction( ) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -405,14 +394,14 @@ pub async fn handle_thread_modal_interaction( } } else { let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.not_a_thread_channel", None, None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -429,14 +418,14 @@ pub async fn handle_thread_modal_interaction( { eprintln!("Failed to create thread record: {}", e); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.creation_failed", None, None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -474,14 +463,14 @@ pub async fn handle_thread_modal_interaction( format!("<#{}>", interaction.channel_id), ); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.created", Some(¶ms), None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -500,14 +489,14 @@ pub async fn handle_thread_modal_interaction( _ => { eprintln!("Unknown thread modal interaction action: {}", parts); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.unknown_action", None, None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await, ), ) @@ -536,14 +525,14 @@ pub async fn handle_thread_component_interaction( Ok(guard) => guard, Err(_) => { let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.action_in_progress", None, None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await .ephemeral(true), ), @@ -563,14 +552,14 @@ pub async fn handle_thread_component_interaction( params.insert("user".to_string(), format!("<@{}>", interaction.user.id)); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.thread_closing", Some(¶ms), None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await, ), ) @@ -582,14 +571,14 @@ pub async fn handle_thread_component_interaction( } "keep" => { let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.will_remain_open", None, None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await, ), ) @@ -630,14 +619,14 @@ pub async fn handle_thread_component_interaction( _ => { eprintln!("Unknown thread component interaction action: {}", parts); let _ = interaction - .create_response( + .create_followup( &ctx.http, - CreateInteractionResponse::Message( + CreateInteractionResponseFollowup::from( MessageBuilder::system_message(ctx, config) .translated_content("thread.unknown_action", None, None, None) .await .to_channel(interaction.channel_id) - .build_interaction_message() + .build_interaction_message_followup() .await, ), ) From 699511d607137e55d3373418387f97610a2cd267 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 28 Oct 2025 15:02:08 +0100 Subject: [PATCH 081/100] refactor(imports): add prelude for easiest imports --- rustmail/src/api/handler/auth/callback.rs | 2 +- rustmail/src/api/handler/auth/login.rs | 4 +- rustmail/src/api/handler/auth/logout.rs | 2 +- rustmail/src/api/handler/bot/restart.rs | 4 +- rustmail/src/api/handler/bot/start.rs | 4 +- rustmail/src/api/handler/bot/status.rs | 8 ++-- rustmail/src/api/handler/bot/stop.rs | 4 +- rustmail/src/api/handler/bot/tickets.rs | 4 +- rustmail/src/api/handler/panel/panel.rs | 2 +- rustmail/src/api/handler/user/avatar.rs | 2 +- rustmail/src/api/middleware/auth.rs | 4 +- rustmail/src/api/router.rs | 2 +- rustmail/src/api/routes/auth.rs | 4 +- rustmail/src/api/routes/bot.rs | 4 +- rustmail/src/api/routes/panel.rs | 4 +- rustmail/src/api/routes/user.rs | 4 +- rustmail/src/bot.rs | 40 +++------------- rustmail/src/commands/add_reminder/mod.rs | 4 ++ .../add_reminder/slash_command/mod.rs | 2 + .../commands/add_reminder/text_command/mod.rs | 2 + rustmail/src/commands/add_staff/mod.rs | 3 ++ .../commands/add_staff/slash_command/mod.rs | 2 + .../commands/add_staff/text_command/mod.rs | 2 + rustmail/src/commands/alert/mod.rs | 3 ++ .../src/commands/alert/slash_command/mod.rs | 2 + .../src/commands/alert/text_command/mod.rs | 2 + rustmail/src/commands/anonreply/mod.rs | 2 + .../commands/anonreply/text_command/mod.rs | 2 + rustmail/src/commands/close/mod.rs | 3 ++ .../src/commands/close/slash_command/mod.rs | 2 + .../src/commands/close/text_command/mod.rs | 2 + rustmail/src/commands/delete/mod.rs | 3 ++ .../src/commands/delete/slash_command/mod.rs | 2 + .../src/commands/delete/text_command/mod.rs | 2 + rustmail/src/commands/edit/mod.rs | 4 ++ .../src/commands/edit/slash_command/mod.rs | 2 + .../src/commands/edit/text_command/mod.rs | 2 + rustmail/src/commands/force_close/mod.rs | 3 ++ .../commands/force_close/slash_command/mod.rs | 2 + .../commands/force_close/text_command/mod.rs | 2 + rustmail/src/commands/help/mod.rs | 3 ++ .../src/commands/help/slash_command/mod.rs | 2 + .../src/commands/help/text_command/mod.rs | 2 + rustmail/src/commands/id/mod.rs | 3 ++ rustmail/src/commands/id/slash_command/mod.rs | 2 + rustmail/src/commands/id/text_command/mod.rs | 2 + rustmail/src/commands/logs/mod.rs | 3 ++ .../src/commands/logs/slash_command/mod.rs | 2 + .../src/commands/logs/text_command/mod.rs | 2 + rustmail/src/commands/mod.rs | 20 ++++++++ rustmail/src/commands/move_thread/mod.rs | 3 ++ .../commands/move_thread/slash_command/mod.rs | 2 + .../commands/move_thread/text_command/mod.rs | 2 + rustmail/src/commands/new_thread/mod.rs | 3 ++ .../commands/new_thread/slash_command/mod.rs | 2 + .../commands/new_thread/text_command/mod.rs | 2 + rustmail/src/commands/recover/mod.rs | 3 ++ .../src/commands/recover/slash_command/mod.rs | 2 + .../src/commands/recover/text_command/mod.rs | 2 + rustmail/src/commands/release/mod.rs | 3 ++ .../src/commands/release/slash_command/mod.rs | 2 + .../src/commands/release/text_command/mod.rs | 2 + rustmail/src/commands/remove_reminder/mod.rs | 3 ++ .../remove_reminder/slash_command/mod.rs | 2 + .../remove_reminder/text_command/mod.rs | 2 + rustmail/src/commands/remove_staff/mod.rs | 3 ++ .../remove_staff/slash_command/mod.rs | 2 + .../commands/remove_staff/text_command/mod.rs | 2 + rustmail/src/commands/reply/mod.rs | 3 ++ .../src/commands/reply/slash_command/mod.rs | 2 + .../src/commands/reply/text_command/mod.rs | 2 + rustmail/src/commands/take/mod.rs | 3 ++ .../src/commands/take/slash_command/mod.rs | 2 + .../src/commands/take/text_command/mod.rs | 2 + rustmail/src/config.rs | 4 +- rustmail/src/db/operations/mod.rs | 24 +++------- .../src/handlers/guild_messages_handler.rs | 46 ++++--------------- rustmail/src/main.rs | 38 +-------------- rustmail/src/modules/mod.rs | 2 + rustmail/src/modules/reminders.rs | 6 +-- rustmail/src/modules/scheduled_closures.rs | 11 ++--- rustmail/src/modules/threads.rs | 16 ++----- rustmail/src/prelude/commands.rs | 1 + rustmail/src/prelude/config.rs | 2 + rustmail/src/prelude/db.rs | 6 +++ rustmail/src/prelude/errors.rs | 4 ++ rustmail/src/prelude/features.rs | 0 rustmail/src/prelude/handlers.rs | 8 ++++ rustmail/src/prelude/i18n.rs | 2 + rustmail/src/prelude/mod.rs | 11 +++++ rustmail/src/prelude/modules.rs | 1 + rustmail/src/prelude/panel_commands.rs | 1 + rustmail/src/prelude/types.rs | 1 + rustmail/src/prelude/utils.rs | 1 + rustmail/src/types/bot.rs | 35 ++++++++++++++ rustmail/src/types/mod.rs | 4 ++ rustmail/src/utils/command/mod.rs | 2 + rustmail/src/utils/conversion/mod.rs | 1 + rustmail/src/utils/message/mod.rs | 2 + rustmail/src/utils/mod.rs | 5 +- rustmail/src/utils/thread/mod.rs | 4 ++ rustmail/src/utils/time/mod.rs | 2 + 102 files changed, 314 insertions(+), 176 deletions(-) create mode 100644 rustmail/src/prelude/commands.rs create mode 100644 rustmail/src/prelude/config.rs create mode 100644 rustmail/src/prelude/db.rs create mode 100644 rustmail/src/prelude/errors.rs create mode 100644 rustmail/src/prelude/features.rs create mode 100644 rustmail/src/prelude/handlers.rs create mode 100644 rustmail/src/prelude/i18n.rs create mode 100644 rustmail/src/prelude/mod.rs create mode 100644 rustmail/src/prelude/modules.rs create mode 100644 rustmail/src/prelude/panel_commands.rs create mode 100644 rustmail/src/prelude/types.rs create mode 100644 rustmail/src/prelude/utils.rs create mode 100644 rustmail/src/types/bot.rs diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index 1a2f411..ec52555 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -1,4 +1,4 @@ -use crate::BotState; +use crate::types::bot::BotState; use axum::extract::{Query, State}; use axum::response::Redirect; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index 8c2d9c0..46faf21 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -1,9 +1,9 @@ -use crate::BotState; use crate::api::utils::get_user_id_from_session::get_user_id_from_session; +use crate::types::bot::BotState; use axum::extract::{Query, State}; use axum::response::Redirect; use axum_extra::extract::CookieJar; -use sqlx::{Row, query}; +use sqlx::{query, Row}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/auth/logout.rs b/rustmail/src/api/handler/auth/logout.rs index 8d73c17..0501d72 100644 --- a/rustmail/src/api/handler/auth/logout.rs +++ b/rustmail/src/api/handler/auth/logout.rs @@ -1,5 +1,5 @@ -use crate::BotState; use crate::api::utils::get_user_id_from_session::get_user_id_from_session; +use crate::types::bot::BotState; use axum::extract::State; use axum::response::Redirect; use axum_extra::extract::CookieJar; diff --git a/rustmail/src/api/handler/bot/restart.rs b/rustmail/src/api/handler/bot/restart.rs index a91c787..3f91690 100644 --- a/rustmail/src/api/handler/bot/restart.rs +++ b/rustmail/src/api/handler/bot/restart.rs @@ -1,8 +1,8 @@ use crate::api::utils::ping_internal::ping_internal; -use crate::{BotState, BotStatus}; -use axum::Json; +use crate::types::bot::{BotState, BotStatus}; use axum::extract::State; use axum::http::StatusCode; +use axum::Json; use sqlx::__rt::sleep; use std::sync::Arc; use std::time::Duration; diff --git a/rustmail/src/api/handler/bot/start.rs b/rustmail/src/api/handler/bot/start.rs index e5d92c0..0da2abc 100644 --- a/rustmail/src/api/handler/bot/start.rs +++ b/rustmail/src/api/handler/bot/start.rs @@ -1,9 +1,9 @@ use crate::bot::run_bot; use crate::config::load_config; -use crate::{BotState, BotStatus}; -use axum::Json; +use crate::types::bot::{BotState, BotStatus}; use axum::extract::State; use axum::http::StatusCode; +use axum::Json; use std::sync::Arc; use tokio::spawn; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/bot/status.rs b/rustmail/src/api/handler/bot/status.rs index 3868b58..656c881 100644 --- a/rustmail/src/api/handler/bot/status.rs +++ b/rustmail/src/api/handler/bot/status.rs @@ -1,8 +1,8 @@ -use crate::BotState; -use axum::Json; +use crate::types::bot::{BotState, BotStatus}; use axum::extract::State; use axum::http::StatusCode; use axum::response::IntoResponse; +use axum::Json; use std::sync::Arc; use tokio::sync::Mutex; @@ -10,11 +10,11 @@ pub async fn handle_status_bot(State(bot_state): State>>) -> let state_lock = bot_state.lock().await; match state_lock.status { - crate::BotStatus::Running { .. } => ( + BotStatus::Running { .. } => ( StatusCode::OK, Json(serde_json::json!({"status": "running"})), ), - crate::BotStatus::Stopped => ( + BotStatus::Stopped => ( StatusCode::OK, Json(serde_json::json!({"status": "stopped"})), ), diff --git a/rustmail/src/api/handler/bot/stop.rs b/rustmail/src/api/handler/bot/stop.rs index 1f9e4b1..d6490d0 100644 --- a/rustmail/src/api/handler/bot/stop.rs +++ b/rustmail/src/api/handler/bot/stop.rs @@ -1,7 +1,7 @@ -use crate::{BotState, BotStatus}; -use axum::Json; +use crate::types::bot::{BotState, BotStatus}; use axum::extract::State; use axum::http::StatusCode; +use axum::Json; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/bot/tickets.rs b/rustmail/src/api/handler/bot/tickets.rs index 713845a..28ae401 100644 --- a/rustmail/src/api/handler/bot/tickets.rs +++ b/rustmail/src/api/handler/bot/tickets.rs @@ -1,8 +1,8 @@ -use crate::BotState; +use crate::types::bot::BotState; use axum::{ - Json, extract::{Query, State}, http::StatusCode, + Json, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; diff --git a/rustmail/src/api/handler/panel/panel.rs b/rustmail/src/api/handler/panel/panel.rs index ad61332..cf3a24b 100644 --- a/rustmail/src/api/handler/panel/panel.rs +++ b/rustmail/src/api/handler/panel/panel.rs @@ -1,4 +1,4 @@ -use crate::BotState; +use crate::types::bot::BotState; use axum::extract::State; use axum::response::IntoResponse; use std::sync::Arc; diff --git a/rustmail/src/api/handler/user/avatar.rs b/rustmail/src/api/handler/user/avatar.rs index e0df027..e7c8976 100644 --- a/rustmail/src/api/handler/user/avatar.rs +++ b/rustmail/src/api/handler/user/avatar.rs @@ -1,5 +1,5 @@ -use crate::BotState; use crate::api::utils::get_user_id_from_session::get_user_id_from_session; +use crate::types::bot::BotState; use axum::extract::State; use axum::response::IntoResponse; use axum_extra::extract::CookieJar; diff --git a/rustmail/src/api/middleware/auth.rs b/rustmail/src/api/middleware/auth.rs index 995aea8..31a2d67 100644 --- a/rustmail/src/api/middleware/auth.rs +++ b/rustmail/src/api/middleware/auth.rs @@ -1,5 +1,5 @@ use crate::api::utils::get_user_id_from_session::get_user_id_from_session; -use crate::{BotCommand, BotState, BotStatus}; +use crate::types::bot::{BotCommand, BotState, BotStatus}; use axum::extract::State; use axum::extract::{ConnectInfo, Request}; use axum::middleware::Next; @@ -8,7 +8,7 @@ use axum_extra::extract::CookieJar; use chrono::Utc; use hyper::StatusCode; use serenity::all::{GuildId, UserId}; -use sqlx::{Row, query}; +use sqlx::{query, Row}; use std::net::SocketAddr; use std::sync::Arc; use subtle::ConstantTimeEq; diff --git a/rustmail/src/api/router.rs b/rustmail/src/api/router.rs index 3a667ee..b038384 100644 --- a/rustmail/src/api/router.rs +++ b/rustmail/src/api/router.rs @@ -1,8 +1,8 @@ -use crate::BotState; use crate::api::routes::auth::create_auth_router; use crate::api::routes::bot::create_bot_router; use crate::api::routes::panel::create_panel_router; use crate::api::routes::user::create_user_router; +use crate::types::bot::BotState; use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/auth.rs b/rustmail/src/api/routes/auth.rs index 249f03f..ee68e2b 100644 --- a/rustmail/src/api/routes/auth.rs +++ b/rustmail/src/api/routes/auth.rs @@ -1,9 +1,9 @@ -use crate::BotState; use crate::api::handler::auth::callback::handle_callback; use crate::api::handler::auth::login::handle_login; use crate::api::handler::auth::logout::handle_logout; -use axum::Router; +use crate::types::bot::BotState; use axum::routing::get; +use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index e982c89..37b4904 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -1,12 +1,12 @@ -use crate::BotState; use crate::api::handler::bot::restart::handle_restart_bot; use crate::api::handler::bot::start::handle_start_bot; use crate::api::handler::bot::status::handle_status_bot; use crate::api::handler::bot::stop::handle_stop_bot; use crate::api::handler::bot::tickets::handle_tickets_bot; use crate::api::middleware::auth::auth_middleware; -use axum::Router; +use crate::types::bot::BotState; use axum::routing::{get, post}; +use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/panel.rs b/rustmail/src/api/routes/panel.rs index f35c181..93eaa23 100644 --- a/rustmail/src/api/routes/panel.rs +++ b/rustmail/src/api/routes/panel.rs @@ -1,8 +1,8 @@ -use crate::BotState; use crate::api::handler::panel::panel::handle_panel_check; use crate::api::middleware::auth::auth_middleware; -use axum::Router; +use crate::types::bot::BotState; use axum::routing::get; +use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/user.rs b/rustmail/src/api/routes/user.rs index 6af1d61..9bd4e5c 100644 --- a/rustmail/src/api/routes/user.rs +++ b/rustmail/src/api/routes/user.rs @@ -1,8 +1,8 @@ -use crate::BotState; use crate::api::handler::user::avatar::handle_get_user_avatar; use crate::api::middleware::auth::auth_middleware; -use axum::Router; +use crate::types::bot::BotState; use axum::routing::get; +use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 2cf3965..6a4bfa3 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -1,36 +1,10 @@ -use crate::commands::CommandRegistry; -use crate::commands::add_reminder::slash_command::add_reminder::AddReminderCommand; -use crate::commands::add_staff::slash_command::add_staff::AddStaffCommand; -use crate::commands::alert::slash_command::alert::AlertCommand; -use crate::commands::close::slash_command::close::CloseCommand; -use crate::commands::delete::slash_command::delete::DeleteCommand; -use crate::commands::edit::slash_command::edit::EditCommand; -use crate::commands::force_close::slash_command::force_close::ForceCloseCommand; -use crate::commands::help::slash_command::help::HelpCommand; -use crate::commands::id::slash_command::id::IdCommand; -use crate::commands::logs::slash_command::logs::LogsCommand; -use crate::commands::move_thread::slash_command::move_thread::MoveCommand; -use crate::commands::new_thread::slash_command::new_thread::NewThreadCommand; -use crate::commands::recover::slash_command::recover::RecoverCommand; -use crate::commands::release::slash_command::release::ReleaseCommand; -use crate::commands::remove_reminder::slash_command::remove_reminder::RemoveReminderCommand; -use crate::commands::remove_staff::slash_command::remove_staff::RemoveStaffCommand; -use crate::commands::reply::slash_command::reply::ReplyCommand; -use crate::commands::take::slash_command::take::TakeCommand; -use crate::config::load_config; -use crate::errors::ModmailError; -use crate::errors::types::ConfigError; -use crate::handlers::guild_handler::GuildHandler; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::handlers::guild_members_handler::GuildMembersHandler; -use crate::handlers::guild_message_reactions_handler::GuildMessageReactionsHandler; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::handlers::guild_moderation_handler::GuildModerationHandler; -use crate::handlers::ready_handler::ReadyHandler; -use crate::handlers::typing_proxy_handler::TypingProxyHandler; -use crate::panel_commands::user::is_member::is_member; -use crate::types::logs::PaginationContext; -use crate::{BotCommand, BotState, BotStatus, db}; +use crate::db; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::panel_commands::*; +use crate::prelude::types::*; use base64::Engine; use rand::RngCore; use serenity::all::{ClientBuilder, GatewayIntents}; diff --git a/rustmail/src/commands/add_reminder/mod.rs b/rustmail/src/commands/add_reminder/mod.rs index de6da7a..e08ac47 100644 --- a/rustmail/src/commands/add_reminder/mod.rs +++ b/rustmail/src/commands/add_reminder/mod.rs @@ -1,3 +1,7 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use common::*; +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/add_reminder/slash_command/mod.rs b/rustmail/src/commands/add_reminder/slash_command/mod.rs index 19dd0ee..7f4f3fa 100644 --- a/rustmail/src/commands/add_reminder/slash_command/mod.rs +++ b/rustmail/src/commands/add_reminder/slash_command/mod.rs @@ -1 +1,3 @@ pub mod add_reminder; + +pub use add_reminder::*; diff --git a/rustmail/src/commands/add_reminder/text_command/mod.rs b/rustmail/src/commands/add_reminder/text_command/mod.rs index 19dd0ee..7f4f3fa 100644 --- a/rustmail/src/commands/add_reminder/text_command/mod.rs +++ b/rustmail/src/commands/add_reminder/text_command/mod.rs @@ -1 +1,3 @@ pub mod add_reminder; + +pub use add_reminder::*; diff --git a/rustmail/src/commands/add_staff/mod.rs b/rustmail/src/commands/add_staff/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/add_staff/mod.rs +++ b/rustmail/src/commands/add_staff/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/add_staff/slash_command/mod.rs b/rustmail/src/commands/add_staff/slash_command/mod.rs index 3d1dc65..b8b7f74 100644 --- a/rustmail/src/commands/add_staff/slash_command/mod.rs +++ b/rustmail/src/commands/add_staff/slash_command/mod.rs @@ -1 +1,3 @@ pub mod add_staff; + +pub use add_staff::*; diff --git a/rustmail/src/commands/add_staff/text_command/mod.rs b/rustmail/src/commands/add_staff/text_command/mod.rs index 3d1dc65..b8b7f74 100644 --- a/rustmail/src/commands/add_staff/text_command/mod.rs +++ b/rustmail/src/commands/add_staff/text_command/mod.rs @@ -1 +1,3 @@ pub mod add_staff; + +pub use add_staff::*; diff --git a/rustmail/src/commands/alert/mod.rs b/rustmail/src/commands/alert/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/alert/mod.rs +++ b/rustmail/src/commands/alert/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/alert/slash_command/mod.rs b/rustmail/src/commands/alert/slash_command/mod.rs index 811b2ab..18e9847 100644 --- a/rustmail/src/commands/alert/slash_command/mod.rs +++ b/rustmail/src/commands/alert/slash_command/mod.rs @@ -1 +1,3 @@ pub mod alert; + +pub use alert::*; diff --git a/rustmail/src/commands/alert/text_command/mod.rs b/rustmail/src/commands/alert/text_command/mod.rs index 811b2ab..18e9847 100644 --- a/rustmail/src/commands/alert/text_command/mod.rs +++ b/rustmail/src/commands/alert/text_command/mod.rs @@ -1 +1,3 @@ pub mod alert; + +pub use alert::*; diff --git a/rustmail/src/commands/anonreply/mod.rs b/rustmail/src/commands/anonreply/mod.rs index 9e9ef4d..51e29f4 100644 --- a/rustmail/src/commands/anonreply/mod.rs +++ b/rustmail/src/commands/anonreply/mod.rs @@ -1 +1,3 @@ pub mod text_command; + +pub use text_command::*; diff --git a/rustmail/src/commands/anonreply/text_command/mod.rs b/rustmail/src/commands/anonreply/text_command/mod.rs index 8f15f19..248f1fa 100644 --- a/rustmail/src/commands/anonreply/text_command/mod.rs +++ b/rustmail/src/commands/anonreply/text_command/mod.rs @@ -1 +1,3 @@ pub mod anonreply; + +pub use anonreply::*; diff --git a/rustmail/src/commands/close/mod.rs b/rustmail/src/commands/close/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/close/mod.rs +++ b/rustmail/src/commands/close/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/close/slash_command/mod.rs b/rustmail/src/commands/close/slash_command/mod.rs index 85b4013..fc1f9eb 100644 --- a/rustmail/src/commands/close/slash_command/mod.rs +++ b/rustmail/src/commands/close/slash_command/mod.rs @@ -1 +1,3 @@ pub mod close; + +pub use close::*; diff --git a/rustmail/src/commands/close/text_command/mod.rs b/rustmail/src/commands/close/text_command/mod.rs index 85b4013..fc1f9eb 100644 --- a/rustmail/src/commands/close/text_command/mod.rs +++ b/rustmail/src/commands/close/text_command/mod.rs @@ -1 +1,3 @@ pub mod close; + +pub use close::*; diff --git a/rustmail/src/commands/delete/mod.rs b/rustmail/src/commands/delete/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/delete/mod.rs +++ b/rustmail/src/commands/delete/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/delete/slash_command/mod.rs b/rustmail/src/commands/delete/slash_command/mod.rs index 0cbafdf..cbc0911 100644 --- a/rustmail/src/commands/delete/slash_command/mod.rs +++ b/rustmail/src/commands/delete/slash_command/mod.rs @@ -1 +1,3 @@ pub mod delete; + +pub use delete::*; diff --git a/rustmail/src/commands/delete/text_command/mod.rs b/rustmail/src/commands/delete/text_command/mod.rs index 0cbafdf..cbc0911 100644 --- a/rustmail/src/commands/delete/text_command/mod.rs +++ b/rustmail/src/commands/delete/text_command/mod.rs @@ -1 +1,3 @@ pub mod delete; + +pub use delete::*; diff --git a/rustmail/src/commands/edit/mod.rs b/rustmail/src/commands/edit/mod.rs index d7911e6..a731dd7 100644 --- a/rustmail/src/commands/edit/mod.rs +++ b/rustmail/src/commands/edit/mod.rs @@ -3,3 +3,7 @@ pub mod message_ops; pub mod slash_command; pub mod text_command; pub mod validation; + +pub use message_ops::*; +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/edit/slash_command/mod.rs b/rustmail/src/commands/edit/slash_command/mod.rs index 99946a8..810909e 100644 --- a/rustmail/src/commands/edit/slash_command/mod.rs +++ b/rustmail/src/commands/edit/slash_command/mod.rs @@ -1 +1,3 @@ pub mod edit; + +pub use edit::*; diff --git a/rustmail/src/commands/edit/text_command/mod.rs b/rustmail/src/commands/edit/text_command/mod.rs index 99946a8..810909e 100644 --- a/rustmail/src/commands/edit/text_command/mod.rs +++ b/rustmail/src/commands/edit/text_command/mod.rs @@ -1 +1,3 @@ pub mod edit; + +pub use edit::*; diff --git a/rustmail/src/commands/force_close/mod.rs b/rustmail/src/commands/force_close/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/force_close/mod.rs +++ b/rustmail/src/commands/force_close/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/force_close/slash_command/mod.rs b/rustmail/src/commands/force_close/slash_command/mod.rs index 9b22ca6..8eb0f4c 100644 --- a/rustmail/src/commands/force_close/slash_command/mod.rs +++ b/rustmail/src/commands/force_close/slash_command/mod.rs @@ -1 +1,3 @@ pub mod force_close; + +pub use force_close::*; diff --git a/rustmail/src/commands/force_close/text_command/mod.rs b/rustmail/src/commands/force_close/text_command/mod.rs index 9b22ca6..8eb0f4c 100644 --- a/rustmail/src/commands/force_close/text_command/mod.rs +++ b/rustmail/src/commands/force_close/text_command/mod.rs @@ -1 +1,3 @@ pub mod force_close; + +pub use force_close::*; diff --git a/rustmail/src/commands/help/mod.rs b/rustmail/src/commands/help/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/help/mod.rs +++ b/rustmail/src/commands/help/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/help/slash_command/mod.rs b/rustmail/src/commands/help/slash_command/mod.rs index 364f76c..5249255 100644 --- a/rustmail/src/commands/help/slash_command/mod.rs +++ b/rustmail/src/commands/help/slash_command/mod.rs @@ -1 +1,3 @@ pub mod help; + +pub use help::*; diff --git a/rustmail/src/commands/help/text_command/mod.rs b/rustmail/src/commands/help/text_command/mod.rs index 364f76c..5249255 100644 --- a/rustmail/src/commands/help/text_command/mod.rs +++ b/rustmail/src/commands/help/text_command/mod.rs @@ -1 +1,3 @@ pub mod help; + +pub use help::*; diff --git a/rustmail/src/commands/id/mod.rs b/rustmail/src/commands/id/mod.rs index 571237c..b852419 100644 --- a/rustmail/src/commands/id/mod.rs +++ b/rustmail/src/commands/id/mod.rs @@ -1,2 +1,5 @@ pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/id/slash_command/mod.rs b/rustmail/src/commands/id/slash_command/mod.rs index fd6bb6c..4d3f020 100644 --- a/rustmail/src/commands/id/slash_command/mod.rs +++ b/rustmail/src/commands/id/slash_command/mod.rs @@ -1 +1,3 @@ pub mod id; + +pub use id::*; diff --git a/rustmail/src/commands/id/text_command/mod.rs b/rustmail/src/commands/id/text_command/mod.rs index fd6bb6c..4d3f020 100644 --- a/rustmail/src/commands/id/text_command/mod.rs +++ b/rustmail/src/commands/id/text_command/mod.rs @@ -1 +1,3 @@ pub mod id; + +pub use id::*; diff --git a/rustmail/src/commands/logs/mod.rs b/rustmail/src/commands/logs/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/logs/mod.rs +++ b/rustmail/src/commands/logs/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/logs/slash_command/mod.rs b/rustmail/src/commands/logs/slash_command/mod.rs index af2c2c3..da94856 100644 --- a/rustmail/src/commands/logs/slash_command/mod.rs +++ b/rustmail/src/commands/logs/slash_command/mod.rs @@ -1 +1,3 @@ pub mod logs; + +pub use logs::*; diff --git a/rustmail/src/commands/logs/text_command/mod.rs b/rustmail/src/commands/logs/text_command/mod.rs index af2c2c3..da94856 100644 --- a/rustmail/src/commands/logs/text_command/mod.rs +++ b/rustmail/src/commands/logs/text_command/mod.rs @@ -1 +1,3 @@ pub mod logs; + +pub use logs::*; diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index b3b3545..7f067b3 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -29,6 +29,26 @@ pub mod remove_staff; pub mod reply; pub mod take; +pub use add_reminder::*; +pub use add_staff::*; +pub use alert::*; +pub use anonreply::*; +pub use close::*; +pub use delete::*; +pub use edit::*; +pub use force_close::*; +pub use help::*; +pub use id::*; +pub use logs::*; +pub use move_thread::*; +pub use new_thread::*; +pub use recover::*; +pub use release::*; +pub use remove_reminder::*; +pub use remove_staff::*; +pub use reply::*; +pub use take::*; + pub type BoxFuture<'a, T> = Pin + Send + 'a>>; pub trait RegistrableCommand: Any + Send + Sync { diff --git a/rustmail/src/commands/move_thread/mod.rs b/rustmail/src/commands/move_thread/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/move_thread/mod.rs +++ b/rustmail/src/commands/move_thread/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/move_thread/slash_command/mod.rs b/rustmail/src/commands/move_thread/slash_command/mod.rs index ae16a43..7a8a59e 100644 --- a/rustmail/src/commands/move_thread/slash_command/mod.rs +++ b/rustmail/src/commands/move_thread/slash_command/mod.rs @@ -1 +1,3 @@ pub mod move_thread; + +pub use move_thread::*; diff --git a/rustmail/src/commands/move_thread/text_command/mod.rs b/rustmail/src/commands/move_thread/text_command/mod.rs index ae16a43..7a8a59e 100644 --- a/rustmail/src/commands/move_thread/text_command/mod.rs +++ b/rustmail/src/commands/move_thread/text_command/mod.rs @@ -1 +1,3 @@ pub mod move_thread; + +pub use move_thread::*; diff --git a/rustmail/src/commands/new_thread/mod.rs b/rustmail/src/commands/new_thread/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/new_thread/mod.rs +++ b/rustmail/src/commands/new_thread/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/new_thread/slash_command/mod.rs b/rustmail/src/commands/new_thread/slash_command/mod.rs index f286241..e92a3fc 100644 --- a/rustmail/src/commands/new_thread/slash_command/mod.rs +++ b/rustmail/src/commands/new_thread/slash_command/mod.rs @@ -1 +1,3 @@ pub mod new_thread; + +pub use new_thread::*; diff --git a/rustmail/src/commands/new_thread/text_command/mod.rs b/rustmail/src/commands/new_thread/text_command/mod.rs index f286241..e92a3fc 100644 --- a/rustmail/src/commands/new_thread/text_command/mod.rs +++ b/rustmail/src/commands/new_thread/text_command/mod.rs @@ -1 +1,3 @@ pub mod new_thread; + +pub use new_thread::*; diff --git a/rustmail/src/commands/recover/mod.rs b/rustmail/src/commands/recover/mod.rs index 571237c..b852419 100644 --- a/rustmail/src/commands/recover/mod.rs +++ b/rustmail/src/commands/recover/mod.rs @@ -1,2 +1,5 @@ pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/recover/slash_command/mod.rs b/rustmail/src/commands/recover/slash_command/mod.rs index 583fc1c..c5032b7 100644 --- a/rustmail/src/commands/recover/slash_command/mod.rs +++ b/rustmail/src/commands/recover/slash_command/mod.rs @@ -1 +1,3 @@ pub mod recover; + +pub use recover::*; diff --git a/rustmail/src/commands/recover/text_command/mod.rs b/rustmail/src/commands/recover/text_command/mod.rs index 583fc1c..c5032b7 100644 --- a/rustmail/src/commands/recover/text_command/mod.rs +++ b/rustmail/src/commands/recover/text_command/mod.rs @@ -1 +1,3 @@ pub mod recover; + +pub use recover::*; diff --git a/rustmail/src/commands/release/mod.rs b/rustmail/src/commands/release/mod.rs index 571237c..b852419 100644 --- a/rustmail/src/commands/release/mod.rs +++ b/rustmail/src/commands/release/mod.rs @@ -1,2 +1,5 @@ pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/release/slash_command/mod.rs b/rustmail/src/commands/release/slash_command/mod.rs index f35143a..84bceb5 100644 --- a/rustmail/src/commands/release/slash_command/mod.rs +++ b/rustmail/src/commands/release/slash_command/mod.rs @@ -1 +1,3 @@ pub mod release; + +pub use release::*; diff --git a/rustmail/src/commands/release/text_command/mod.rs b/rustmail/src/commands/release/text_command/mod.rs index f35143a..84bceb5 100644 --- a/rustmail/src/commands/release/text_command/mod.rs +++ b/rustmail/src/commands/release/text_command/mod.rs @@ -1 +1,3 @@ pub mod release; + +pub use release::*; diff --git a/rustmail/src/commands/remove_reminder/mod.rs b/rustmail/src/commands/remove_reminder/mod.rs index 571237c..b852419 100644 --- a/rustmail/src/commands/remove_reminder/mod.rs +++ b/rustmail/src/commands/remove_reminder/mod.rs @@ -1,2 +1,5 @@ pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/remove_reminder/slash_command/mod.rs b/rustmail/src/commands/remove_reminder/slash_command/mod.rs index 1d8a3aa..a2db9f4 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/mod.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/mod.rs @@ -1 +1,3 @@ pub mod remove_reminder; + +pub use remove_reminder::*; diff --git a/rustmail/src/commands/remove_reminder/text_command/mod.rs b/rustmail/src/commands/remove_reminder/text_command/mod.rs index 1d8a3aa..a2db9f4 100644 --- a/rustmail/src/commands/remove_reminder/text_command/mod.rs +++ b/rustmail/src/commands/remove_reminder/text_command/mod.rs @@ -1 +1,3 @@ pub mod remove_reminder; + +pub use remove_reminder::*; diff --git a/rustmail/src/commands/remove_staff/mod.rs b/rustmail/src/commands/remove_staff/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/remove_staff/mod.rs +++ b/rustmail/src/commands/remove_staff/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/remove_staff/slash_command/mod.rs b/rustmail/src/commands/remove_staff/slash_command/mod.rs index b729249..d94ee3b 100644 --- a/rustmail/src/commands/remove_staff/slash_command/mod.rs +++ b/rustmail/src/commands/remove_staff/slash_command/mod.rs @@ -1 +1,3 @@ pub mod remove_staff; + +pub use remove_staff::*; diff --git a/rustmail/src/commands/remove_staff/text_command/mod.rs b/rustmail/src/commands/remove_staff/text_command/mod.rs index b729249..d94ee3b 100644 --- a/rustmail/src/commands/remove_staff/text_command/mod.rs +++ b/rustmail/src/commands/remove_staff/text_command/mod.rs @@ -1 +1,3 @@ pub mod remove_staff; + +pub use remove_staff::*; diff --git a/rustmail/src/commands/reply/mod.rs b/rustmail/src/commands/reply/mod.rs index 571237c..b852419 100644 --- a/rustmail/src/commands/reply/mod.rs +++ b/rustmail/src/commands/reply/mod.rs @@ -1,2 +1,5 @@ pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/reply/slash_command/mod.rs b/rustmail/src/commands/reply/slash_command/mod.rs index 1b2be17..b052552 100644 --- a/rustmail/src/commands/reply/slash_command/mod.rs +++ b/rustmail/src/commands/reply/slash_command/mod.rs @@ -1 +1,3 @@ pub mod reply; + +pub use reply::*; diff --git a/rustmail/src/commands/reply/text_command/mod.rs b/rustmail/src/commands/reply/text_command/mod.rs index 1b2be17..b052552 100644 --- a/rustmail/src/commands/reply/text_command/mod.rs +++ b/rustmail/src/commands/reply/text_command/mod.rs @@ -1 +1,3 @@ pub mod reply; + +pub use reply::*; diff --git a/rustmail/src/commands/take/mod.rs b/rustmail/src/commands/take/mod.rs index de6da7a..a983339 100644 --- a/rustmail/src/commands/take/mod.rs +++ b/rustmail/src/commands/take/mod.rs @@ -1,3 +1,6 @@ pub mod common; pub mod slash_command; pub mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/take/slash_command/mod.rs b/rustmail/src/commands/take/slash_command/mod.rs index f1cf257..f7094af 100644 --- a/rustmail/src/commands/take/slash_command/mod.rs +++ b/rustmail/src/commands/take/slash_command/mod.rs @@ -1 +1,3 @@ pub mod take; + +pub use take::*; diff --git a/rustmail/src/commands/take/text_command/mod.rs b/rustmail/src/commands/take/text_command/mod.rs index f1cf257..f7094af 100644 --- a/rustmail/src/commands/take/text_command/mod.rs +++ b/rustmail/src/commands/take/text_command/mod.rs @@ -1 +1,3 @@ pub mod take; + +pub use take::*; diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index 403b139..ffabe13 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -1,5 +1,5 @@ -use crate::errors::handler::ErrorHandler; -use crate::i18n::languages::Language; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; use chrono_tz::Tz; use serde::Deserialize; use serenity::all::GuildId; diff --git a/rustmail/src/db/operations/mod.rs b/rustmail/src/db/operations/mod.rs index ee64212..bd4d2d8 100644 --- a/rustmail/src/db/operations/mod.rs +++ b/rustmail/src/db/operations/mod.rs @@ -6,20 +6,10 @@ pub mod reminders; pub mod scheduled; pub mod threads; -pub use features::{get_feature_message, upsert_feature_message}; -pub use init::{get_last_recovery_timestamp, init_database, update_last_recovery_timestamp}; -pub use messages::{ - delete_message, get_latest_thread_message, get_message_ids_by_message_id, - get_message_ids_by_number, get_thread_message_by_inbox_message_id, insert_staff_message, - insert_user_message_with_ids, update_message_content, update_message_numbers_after_deletion, -}; -pub use scheduled::{ - delete_scheduled_closure, get_all_scheduled_closures, get_scheduled_closure, - upsert_scheduled_closure, -}; -pub use threads::{ - allocate_next_message_number, cancel_alert_for_staff, close_thread, create_thread_for_user, - get_all_opened_threads, get_staff_alerts_for_user, get_thread_by_channel_id, get_thread_by_id, - get_thread_channel_by_user_id, get_thread_id_by_user_id, get_user_id_from_channel_id, - is_user_left, mark_alert_as_used, set_alert_for_staff, thread_exists, update_thread_user_left, -}; +pub use features::*; +pub use init::*; +pub use logs::*; +pub use messages::*; +pub use reminders::*; +pub use scheduled::*; +pub use threads::*; diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 9061448..8e75ae9 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -1,39 +1,11 @@ -use crate::commands::CommandRegistry; -use crate::commands::add_reminder::text_command::add_reminder::add_reminder; -use crate::commands::add_staff::text_command::add_staff::add_staff; -use crate::commands::alert::text_command::alert::alert; -use crate::commands::anonreply::text_command::anonreply::anonreply; -use crate::commands::close::text_command::close::close; -use crate::commands::delete::text_command::delete::delete; -use crate::commands::edit::message_ops::edit_inbox_message; -use crate::commands::edit::text_command::edit::edit; -use crate::commands::force_close::text_command::force_close::force_close; -use crate::commands::help::text_command::help::help; -use crate::commands::id::text_command::id::id; -use crate::commands::logs::text_command::logs::logs; -use crate::commands::move_thread::text_command::move_thread::move_thread; -use crate::commands::new_thread::text_command::new_thread::new_thread; -use crate::commands::recover::text_command::recover::recover; -use crate::commands::release::text_command::release::release; -use crate::commands::remove_reminder::text_command::remove_reminder::remove_reminder; -use crate::commands::remove_staff::text_command::remove_staff::remove_staff; -use crate::commands::reply::text_command::reply::reply; -use crate::commands::take::text_command::take::take; use crate::config::Config; -use crate::db::messages::get_thread_message_by_dm_message_id; -use crate::db::operations::messages::get_thread_message_by_message_id; -use crate::db::operations::{ - delete_message as db_delete_message, update_message_numbers_after_deletion, -}; -use crate::db::operations::{get_thread_channel_by_user_id, thread_exists, update_message_content}; -use crate::db::threads::get_thread_by_user_id; -use crate::errors::{ModmailResult, common}; -use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::thread::get_thread_lock::get_thread_lock; -use crate::utils::thread::send_to_thread::send_to_thread; -use crate::{modules::threads::create_channel, utils::wrap_command}; +use crate::modules::create_channel; +use crate::prelude::commands::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; +use crate::prelude::types::*; +use crate::prelude::utils::*; use serenity::all::{GuildId, MessageId, UserId}; use serenity::{ all::{ChannelId, Context, EventHandler, Message, MessageUpdateEvent}, @@ -42,8 +14,9 @@ use serenity::{ use std::collections::HashSet; use std::sync::{LazyLock, Mutex}; use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; -use tokio::sync::Mutex as AsyncMutex; use tokio::sync::watch::Receiver; +use tokio::sync::Mutex as AsyncMutex; +use crate::wrap_command; static SUPPRESSED_DELETES: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); @@ -82,6 +55,7 @@ impl GuildMessagesHandler { shutdown: Arc::new(shutdown), pagination, }; + extract_reply_content() let mut lock = h.commands.lock().await; diff --git a/rustmail/src/main.rs b/rustmail/src/main.rs index d0f5b63..7322241 100644 --- a/rustmail/src/main.rs +++ b/rustmail/src/main.rs @@ -1,16 +1,11 @@ use crate::api::router::create_api_router; use crate::bot::{init_bot_state, start_bot_if_config_valid}; -use crate::config::Config; use axum::extract::Path; use axum::response::Response; use rust_embed::RustEmbed; -use serenity::http::Http; use std::borrow::Cow; use std::net::SocketAddr; -use std::sync::Arc; use tokio::signal; -use tokio::sync::watch::Sender; -use tokio::task::JoinHandle; mod api; mod bot; @@ -23,41 +18,10 @@ mod handlers; mod i18n; mod modules; mod panel_commands; +mod prelude; mod types; mod utils; -pub struct Database; - -pub enum BotStatus { - Stopped, - Running { - handle: JoinHandle<()>, - shutdown: Sender, - }, -} - -enum BotCommand { - CheckUserRole { - user_id: u64, - role_id: u64, - resp: tokio::sync::oneshot::Sender, - }, - CheckUserIsMember { - user_id: u64, - resp: tokio::sync::oneshot::Sender, - }, - Test, -} - -struct BotState { - config: Option, - status: BotStatus, - db_pool: Option, - command_tx: tokio::sync::mpsc::Sender, - bot_http: Option>, - internal_token: String, -} - #[derive(RustEmbed)] #[folder = "static/"] struct Assets; diff --git a/rustmail/src/modules/mod.rs b/rustmail/src/modules/mod.rs index ad3b788..a89d31d 100644 --- a/rustmail/src/modules/mod.rs +++ b/rustmail/src/modules/mod.rs @@ -3,3 +3,5 @@ pub mod message_recovery; pub mod reminders; pub mod scheduled_closures; pub mod threads; + +pub use threads::*; diff --git a/rustmail/src/modules/reminders.rs b/rustmail/src/modules/reminders.rs index ad9ed6d..a28f853 100644 --- a/rustmail/src/modules/reminders.rs +++ b/rustmail/src/modules/reminders.rs @@ -1,6 +1,6 @@ -use crate::commands::add_reminder::common::spawn_reminder; -use crate::config::Config; -use crate::db::reminders::get_all_pending_reminders; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; use serenity::all::Context; use std::sync::Arc; use tokio::sync::watch::Receiver; diff --git a/rustmail/src/modules/scheduled_closures.rs b/rustmail/src/modules/scheduled_closures.rs index b672020..8b11902 100644 --- a/rustmail/src/modules/scheduled_closures.rs +++ b/rustmail/src/modules/scheduled_closures.rs @@ -1,12 +1,9 @@ -use crate::config::Config; -use crate::db::{ - close_thread, delete_scheduled_closure, get_all_scheduled_closures, get_scheduled_closure, - get_thread_by_id, -}; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::utils::*; use chrono::Utc; use serenity::all::{ChannelId, Context, UserId}; -use tokio::time::{Duration, sleep}; +use tokio::time::{sleep, Duration}; fn schedule_one(ctx: &Context, config: &Config, thread_id: String, close_at: i64) { let now = Utc::now().timestamp(); diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index b3d80c7..b780190 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -1,14 +1,8 @@ -use crate::config::Config; -use crate::db::logs::get_logs_from_user_id; -use crate::db::operations::{create_thread_for_user, get_thread_channel_by_user_id}; -use crate::errors::common::validation_failed; -use crate::i18n::get_translated_message; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::ui; -use crate::utils::thread::get_thread_lock::get_thread_lock; -use crate::utils::thread::send_to_thread::send_to_thread; -use crate::utils::thread::user_recap::get_user_recap; -use crate::utils::time::get_member_join_date::get_member_join_date_for_user; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::{ ActionRowComponent, Channel, ChannelId, ComponentInteraction, Context, CreateChannel, CreateInteractionResponseFollowup, GuildId, Message, ModalInteraction, UserId, diff --git a/rustmail/src/prelude/commands.rs b/rustmail/src/prelude/commands.rs new file mode 100644 index 0000000..280a8e8 --- /dev/null +++ b/rustmail/src/prelude/commands.rs @@ -0,0 +1 @@ +pub use crate::commands::*; diff --git a/rustmail/src/prelude/config.rs b/rustmail/src/prelude/config.rs new file mode 100644 index 0000000..bf55a47 --- /dev/null +++ b/rustmail/src/prelude/config.rs @@ -0,0 +1,2 @@ +pub use crate::config::load_config; +pub use crate::config::Config; diff --git a/rustmail/src/prelude/db.rs b/rustmail/src/prelude/db.rs new file mode 100644 index 0000000..d4174e1 --- /dev/null +++ b/rustmail/src/prelude/db.rs @@ -0,0 +1,6 @@ +pub use crate::db::logs::*; +pub use crate::db::messages::*; +pub use crate::db::operations::*; +pub use crate::db::reminders::*; +pub use crate::db::threads::*; +pub use crate::db::*; diff --git a/rustmail/src/prelude/errors.rs b/rustmail/src/prelude/errors.rs new file mode 100644 index 0000000..31695e7 --- /dev/null +++ b/rustmail/src/prelude/errors.rs @@ -0,0 +1,4 @@ +pub use crate::errors::common::*; +pub use crate::errors::handler::*; +pub use crate::errors::types::*; +pub use crate::errors::*; diff --git a/rustmail/src/prelude/features.rs b/rustmail/src/prelude/features.rs new file mode 100644 index 0000000..e69de29 diff --git a/rustmail/src/prelude/handlers.rs b/rustmail/src/prelude/handlers.rs new file mode 100644 index 0000000..b362c54 --- /dev/null +++ b/rustmail/src/prelude/handlers.rs @@ -0,0 +1,8 @@ +pub use crate::handlers::guild_handler::GuildHandler; +pub use crate::handlers::guild_interaction_handler::InteractionHandler; +pub use crate::handlers::guild_members_handler::GuildMembersHandler; +pub use crate::handlers::guild_message_reactions_handler::GuildMessageReactionsHandler; +pub use crate::handlers::guild_messages_handler::GuildMessagesHandler; +pub use crate::handlers::guild_moderation_handler::GuildModerationHandler; +pub use crate::handlers::ready_handler::ReadyHandler; +pub use crate::handlers::typing_proxy_handler::TypingProxyHandler; diff --git a/rustmail/src/prelude/i18n.rs b/rustmail/src/prelude/i18n.rs new file mode 100644 index 0000000..ad16993 --- /dev/null +++ b/rustmail/src/prelude/i18n.rs @@ -0,0 +1,2 @@ +pub use crate::i18n::get_translated_message; +pub use crate::i18n::languages::*; diff --git a/rustmail/src/prelude/mod.rs b/rustmail/src/prelude/mod.rs new file mode 100644 index 0000000..b39feb5 --- /dev/null +++ b/rustmail/src/prelude/mod.rs @@ -0,0 +1,11 @@ +pub mod commands; +pub mod config; +pub mod db; +pub mod errors; +pub mod features; +pub mod handlers; +pub mod i18n; +pub mod modules; +pub mod panel_commands; +pub mod types; +pub mod utils; diff --git a/rustmail/src/prelude/modules.rs b/rustmail/src/prelude/modules.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/rustmail/src/prelude/modules.rs @@ -0,0 +1 @@ + diff --git a/rustmail/src/prelude/panel_commands.rs b/rustmail/src/prelude/panel_commands.rs new file mode 100644 index 0000000..fe5c21d --- /dev/null +++ b/rustmail/src/prelude/panel_commands.rs @@ -0,0 +1 @@ +pub use crate::panel_commands::user::is_member::is_member; diff --git a/rustmail/src/prelude/types.rs b/rustmail/src/prelude/types.rs new file mode 100644 index 0000000..1b6a658 --- /dev/null +++ b/rustmail/src/prelude/types.rs @@ -0,0 +1 @@ +pub use crate::types::*; diff --git a/rustmail/src/prelude/utils.rs b/rustmail/src/prelude/utils.rs new file mode 100644 index 0000000..7af11e7 --- /dev/null +++ b/rustmail/src/prelude/utils.rs @@ -0,0 +1 @@ +pub use crate::utils::*; diff --git a/rustmail/src/types/bot.rs b/rustmail/src/types/bot.rs new file mode 100644 index 0000000..b15c16c --- /dev/null +++ b/rustmail/src/types/bot.rs @@ -0,0 +1,35 @@ +use crate::config::Config; +use serenity::all::Http; +use std::sync::Arc; +use tokio::sync::watch::Sender; +use tokio::task::JoinHandle; + +pub enum BotStatus { + Stopped, + Running { + handle: JoinHandle<()>, + shutdown: Sender, + }, +} + +pub enum BotCommand { + CheckUserRole { + user_id: u64, + role_id: u64, + resp: tokio::sync::oneshot::Sender, + }, + CheckUserIsMember { + user_id: u64, + resp: tokio::sync::oneshot::Sender, + }, + Test, +} + +pub struct BotState { + pub config: Option, + pub status: BotStatus, + pub db_pool: Option, + pub command_tx: tokio::sync::mpsc::Sender, + pub bot_http: Option>, + pub internal_token: String, +} diff --git a/rustmail/src/types/mod.rs b/rustmail/src/types/mod.rs index af2c2c3..31b7717 100644 --- a/rustmail/src/types/mod.rs +++ b/rustmail/src/types/mod.rs @@ -1 +1,5 @@ +pub mod bot; pub mod logs; + +pub use bot::*; +pub use logs::*; diff --git a/rustmail/src/utils/command/mod.rs b/rustmail/src/utils/command/mod.rs index 64f622f..084a867 100644 --- a/rustmail/src/utils/command/mod.rs +++ b/rustmail/src/utils/command/mod.rs @@ -2,3 +2,5 @@ pub mod category; pub mod defer_response; pub mod extract_reply_content; pub mod wrap_command; + +pub use extract_reply_content::*; diff --git a/rustmail/src/utils/conversion/mod.rs b/rustmail/src/utils/conversion/mod.rs index e215dc6..c77e677 100644 --- a/rustmail/src/utils/conversion/mod.rs +++ b/rustmail/src/utils/conversion/mod.rs @@ -1 +1,2 @@ pub mod hex_string_to_int; + diff --git a/rustmail/src/utils/message/mod.rs b/rustmail/src/utils/message/mod.rs index 2cd920b..8b0e900 100644 --- a/rustmail/src/utils/message/mod.rs +++ b/rustmail/src/utils/message/mod.rs @@ -4,6 +4,8 @@ pub mod reply_intent; pub mod category; pub mod ui_components; +pub use message_builder::*; + pub mod ui { use super::{ui_components::ButtonsBuilder, ui_components::ModalBuilder}; pub fn modal(id: impl Into, title: impl Into) -> ModalBuilder { diff --git a/rustmail/src/utils/mod.rs b/rustmail/src/utils/mod.rs index 1e051de..046ef8f 100644 --- a/rustmail/src/utils/mod.rs +++ b/rustmail/src/utils/mod.rs @@ -4,4 +4,7 @@ pub mod message; pub mod thread; pub mod time; -pub use crate::wrap_command; +pub use command::*; +pub use message::*; +pub use thread::*; +pub use time::*; diff --git a/rustmail/src/utils/thread/mod.rs b/rustmail/src/utils/thread/mod.rs index 2ec3a7a..16e431a 100644 --- a/rustmail/src/utils/thread/mod.rs +++ b/rustmail/src/utils/thread/mod.rs @@ -3,3 +3,7 @@ pub mod fetch_thread; pub mod get_thread_lock; pub mod send_to_thread; pub mod user_recap; + +pub use get_thread_lock::*; +pub use send_to_thread::*; +pub use user_recap::*; diff --git a/rustmail/src/utils/time/mod.rs b/rustmail/src/utils/time/mod.rs index 4f689c2..0c43e24 100644 --- a/rustmail/src/utils/time/mod.rs +++ b/rustmail/src/utils/time/mod.rs @@ -1,2 +1,4 @@ pub mod format_duration_since; pub mod get_member_join_date; + +pub use get_member_join_date::*; From b6accb09cfbba52b59841eda2aa1626bfcb5062a Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 28 Oct 2025 18:06:45 +0100 Subject: [PATCH 082/100] fix(commands): incorect channel's name format --- rustmail/src/commands/take/common.rs | 4 ++-- rustmail/src/commands/take/slash_command/take.rs | 2 +- rustmail/src/commands/take/text_command/take.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rustmail/src/commands/take/common.rs b/rustmail/src/commands/take/common.rs index d9bdbd9..f8da4eb 100644 --- a/rustmail/src/commands/take/common.rs +++ b/rustmail/src/commands/take/common.rs @@ -15,7 +15,7 @@ pub async fn rename_channel_with_timeout( ) -> ModmailResult<()> { let rename_future = channel_id.edit( &ctx.http, - EditChannel::new().name(format!("🔵-{}", new_name)), + EditChannel::new().name(new_name.clone()), ); let timeout = sleep(Duration::from_secs(2)); @@ -57,7 +57,7 @@ pub async fn rename_channel_with_timeout( None }; - if let Err(e) = channel_id.edit(&ctx.http, EditChannel::new().name(format!("🔵-{}", new_name))).await { + if let Err(e) = channel_id.edit(&ctx.http, EditChannel::new().name(new_name)).await { return Err(ModmailError::Discord(DiscordError::ApiError(e.to_string()))); } diff --git a/rustmail/src/commands/take/slash_command/take.rs b/rustmail/src/commands/take/slash_command/take.rs index a919d92..16fe6f2 100644 --- a/rustmail/src/commands/take/slash_command/take.rs +++ b/rustmail/src/commands/take/slash_command/take.rs @@ -93,7 +93,7 @@ impl RegistrableCommand for TakeCommand { &ctx, &config, thread_id, - command.user.name.clone(), + format!("🔵-{}", command.user.name.clone()), None, Some(&command), ) diff --git a/rustmail/src/commands/take/text_command/take.rs b/rustmail/src/commands/take/text_command/take.rs index c16d324..6152ba1 100644 --- a/rustmail/src/commands/take/text_command/take.rs +++ b/rustmail/src/commands/take/text_command/take.rs @@ -44,7 +44,7 @@ pub async fn take( &ctx, config, thread_id, - msg.author.name.clone(), + format!("🔵-{}", msg.author.name.clone()), Some(&msg), None, ) From 00a1d6e7b68cb1652a2a0b375635001884cd4032 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Tue, 28 Oct 2025 20:56:18 +0100 Subject: [PATCH 083/100] refactor(commands): put take/release command logic into tokio task for handle discord api limits --- .../commands/release/slash_command/release.rs | 46 ++++++++++--------- .../commands/release/text_command/release.rs | 38 ++++++++------- rustmail/src/commands/take/common.rs | 13 ++---- .../src/commands/take/slash_command/take.rs | 44 +++++++++--------- .../src/commands/take/text_command/take.rs | 40 ++++++++-------- rustmail/src/config.rs | 1 - 6 files changed, 95 insertions(+), 87 deletions(-) diff --git a/rustmail/src/commands/release/slash_command/release.rs b/rustmail/src/commands/release/slash_command/release.rs index c61ab27..45f4a42 100644 --- a/rustmail/src/commands/release/slash_command/release.rs +++ b/rustmail/src/commands/release/slash_command/release.rs @@ -88,28 +88,30 @@ impl RegistrableCommand for ReleaseCommand { if thread_name == thread.user_name { return Err(ModmailError::Command(CommandError::TicketAlreadyReleased)); } - - rename_channel_with_timeout( - &ctx, - &config, - thread_id, - thread.user_name.clone(), - None, - Some(&command), - ) - .await?; - - let mut params = std::collections::HashMap::new(); - params.insert("staff".to_string(), format!("<@{}>", command.user.id)); - - let response = MessageBuilder::system_message(&ctx, &config) - .translated_content("release.confirmation", Some(¶ms), None, None) - .await - .to_channel(command.channel_id) - .build_interaction_message_followup() - .await; - - let _ = command.create_followup(ctx.clone(), response).await; + + tokio::spawn(async move { + let _ = rename_channel_with_timeout( + &ctx, + &config, + thread_id, + thread.user_name.clone(), + None, + Some(&command), + ) + .await; + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", command.user.id)); + + let response = MessageBuilder::system_message(&ctx, &config) + .translated_content("release.confirmation", Some(¶ms), None, None) + .await + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + let _ = command.create_followup(ctx.clone(), response).await; + }); Ok(()) } else { diff --git a/rustmail/src/commands/release/text_command/release.rs b/rustmail/src/commands/release/text_command/release.rs index 05b1b51..fe8ef8e 100644 --- a/rustmail/src/commands/release/text_command/release.rs +++ b/rustmail/src/commands/release/text_command/release.rs @@ -40,25 +40,29 @@ pub async fn release( return Err(ModmailError::Command(CommandError::TicketAlreadyReleased)); } - rename_channel_with_timeout( - &ctx, - &config, - thread_id, - thread.user_name.clone(), - Some(&msg), - None, - ) - .await?; + let config_clone = config.clone(); - let mut params = std::collections::HashMap::new(); - params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); + tokio::spawn(async move { + let _ = rename_channel_with_timeout( + &ctx, + &config_clone, + thread_id, + thread.user_name.clone(), + Some(&msg), + None, + ) + .await; - let _ = MessageBuilder::system_message(&ctx, config) - .translated_content("release.confirmation", Some(¶ms), None, None) - .await - .to_channel(msg.channel_id) - .send(true) - .await?; + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); + + let _ = MessageBuilder::system_message(&ctx, &config_clone) + .translated_content("release.confirmation", Some(¶ms), None, None) + .await + .to_channel(msg.channel_id) + .send(true) + .await; + }); Ok(()) } else { diff --git a/rustmail/src/commands/take/common.rs b/rustmail/src/commands/take/common.rs index f8da4eb..bc46225 100644 --- a/rustmail/src/commands/take/common.rs +++ b/rustmail/src/commands/take/common.rs @@ -57,16 +57,13 @@ pub async fn rename_channel_with_timeout( None }; - if let Err(e) = channel_id.edit(&ctx.http, EditChannel::new().name(new_name)).await { - return Err(ModmailError::Discord(DiscordError::ApiError(e.to_string()))); - } + let _ = channel_id.edit(&ctx.http, EditChannel::new().name(new_name)).await; - if let Some(msg) = message_response { - let _ = msg.delete(&ctx.http).await; + if let Some(m) = message_response { + let _ = m.delete(&ctx.http).await; } - - if let Some(command) = command_response { - let _ = command.delete(&ctx.http).await; + if let Some(m) = command_response { + let _ = m.delete(&ctx.http).await; } return Ok(()); diff --git a/rustmail/src/commands/take/slash_command/take.rs b/rustmail/src/commands/take/slash_command/take.rs index 16fe6f2..6cab6ff 100644 --- a/rustmail/src/commands/take/slash_command/take.rs +++ b/rustmail/src/commands/take/slash_command/take.rs @@ -89,27 +89,29 @@ impl RegistrableCommand for TakeCommand { return Err(ModmailError::Command(CommandError::TicketAlreadyTaken)); } - rename_channel_with_timeout( - &ctx, - &config, - thread_id, - format!("🔵-{}", command.user.name.clone()), - None, - Some(&command), - ) - .await?; - - let mut params = std::collections::HashMap::new(); - params.insert("staff".to_string(), format!("<@{}>", command.user.id)); - - let response = MessageBuilder::system_message(&ctx, &config) - .translated_content("take.confirmation", Some(¶ms), None, None) - .await - .to_channel(command.channel_id) - .build_interaction_message_followup() - .await; - - let _ = command.create_followup(ctx.clone(), response).await; + tokio::spawn(async move { + let _ = rename_channel_with_timeout( + &ctx, + &config, + thread_id, + format!("🔵-{}", command.user.name.clone()), + None, + Some(&command), + ) + .await; + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", command.user.id)); + + let response = MessageBuilder::system_message(&ctx, &config) + .translated_content("take.confirmation", Some(¶ms), None, None) + .await + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + let _ = command.create_followup(ctx.clone(), response).await; + }); Ok(()) } else { diff --git a/rustmail/src/commands/take/text_command/take.rs b/rustmail/src/commands/take/text_command/take.rs index 6152ba1..47cbc12 100644 --- a/rustmail/src/commands/take/text_command/take.rs +++ b/rustmail/src/commands/take/text_command/take.rs @@ -39,26 +39,30 @@ pub async fn take( if thread_name == format!("🔵-{}", msg.author.name) { return Err(ModmailError::Command(CommandError::TicketAlreadyTaken)); } + + let config_clone = config.clone(); + + tokio::spawn(async move { + let _ = rename_channel_with_timeout( + &ctx, + &config_clone, + thread_id, + format!("🔵-{}", msg.author.name.clone()), + Some(&msg), + None, + ) + .await; - rename_channel_with_timeout( - &ctx, - config, - thread_id, - format!("🔵-{}", msg.author.name.clone()), - Some(&msg), - None, - ) - .await?; + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); - let mut params = std::collections::HashMap::new(); - params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); - - let _ = MessageBuilder::system_message(&ctx, config) - .translated_content("take.confirmation", Some(¶ms), None, None) - .await - .to_channel(msg.channel_id) - .send(true) - .await?; + let _ = MessageBuilder::system_message(&ctx, &config_clone) + .translated_content("take.confirmation", Some(¶ms), None, None) + .await + .to_channel(msg.channel_id) + .send(true) + .await; + }); Ok(()) } else { diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index 403b139..ca78c6b 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -65,7 +65,6 @@ where } fn default_timezone() -> Tz { - println!("yes"); chrono_tz::UTC } From b70943acd4d4f0dfd27b3372c2c64a35d858a452 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Wed, 29 Oct 2025 15:00:15 +0100 Subject: [PATCH 084/100] refactor(imports): add complete prelude --- rustmail/src/api/handler/auth/callback.rs | 2 +- rustmail/src/api/handler/auth/login.rs | 6 +-- rustmail/src/api/handler/auth/logout.rs | 4 +- rustmail/src/api/handler/auth/mod.rs | 4 ++ rustmail/src/api/handler/bot/mod.rs | 6 +++ rustmail/src/api/handler/bot/restart.rs | 6 +-- rustmail/src/api/handler/bot/start.rs | 6 +-- rustmail/src/api/handler/bot/status.rs | 4 +- rustmail/src/api/handler/bot/stop.rs | 4 +- rustmail/src/api/handler/bot/tickets.rs | 4 +- rustmail/src/api/handler/mod.rs | 5 ++ rustmail/src/api/handler/panel/mod.rs | 2 + rustmail/src/api/handler/panel/panel.rs | 2 +- rustmail/src/api/handler/user/avatar.rs | 4 +- rustmail/src/api/handler/user/mod.rs | 2 + rustmail/src/api/middleware/auth.rs | 6 +-- rustmail/src/api/middleware/mod.rs | 2 + rustmail/src/api/mod.rs | 6 +++ rustmail/src/api/router.rs | 7 +-- rustmail/src/api/routes/auth.rs | 8 ++-- rustmail/src/api/routes/bot.rs | 11 ++--- rustmail/src/api/routes/mod.rs | 5 ++ rustmail/src/api/routes/panel.rs | 7 ++- rustmail/src/api/routes/user.rs | 7 ++- rustmail/src/api/utils/mod.rs | 3 ++ rustmail/src/bot.rs | 6 +-- rustmail/src/commands/add_reminder/common.rs | 7 ++- .../slash_command/add_reminder.rs | 20 ++++---- .../add_reminder/text_command/add_reminder.rs | 17 +++---- rustmail/src/commands/add_staff/common.rs | 6 +-- rustmail/src/commands/add_staff/mod.rs | 1 + .../add_staff/slash_command/add_staff.rs | 24 ++++------ .../add_staff/text_command/add_staff.rs | 27 +++++------ rustmail/src/commands/alert/common.rs | 19 ++++---- rustmail/src/commands/alert/mod.rs | 1 + .../src/commands/alert/slash_command/alert.rs | 18 +++----- .../src/commands/alert/text_command/alert.rs | 13 ++---- .../anonreply/text_command/anonreply.rs | 21 ++++----- rustmail/src/commands/close/mod.rs | 1 + .../src/commands/close/slash_command/close.rs | 25 ++++------ .../src/commands/close/text_command/close.rs | 21 +++------ rustmail/src/commands/delete/common.rs | 24 ++++------ rustmail/src/commands/delete/mod.rs | 1 + .../commands/delete/slash_command/delete.rs | 24 ++++------ .../commands/delete/text_command/delete.rs | 15 +++--- rustmail/src/commands/edit/common.rs | 13 ++---- rustmail/src/commands/edit/message_ops.rs | 35 +++++++------- rustmail/src/commands/edit/mod.rs | 2 + .../src/commands/edit/slash_command/edit.rs | 21 ++++----- .../src/commands/edit/text_command/edit.rs | 23 +++------- rustmail/src/commands/edit/validation.rs | 18 ++++---- rustmail/src/commands/force_close/common.rs | 5 +- rustmail/src/commands/force_close/mod.rs | 1 + .../force_close/slash_command/force_close.rs | 27 +++++------ .../force_close/text_command/force_close.rs | 20 ++++---- rustmail/src/commands/help/common.rs | 10 ++-- rustmail/src/commands/help/mod.rs | 1 + .../src/commands/help/slash_command/help.rs | 15 +++--- .../src/commands/help/text_command/help.rs | 8 ++-- rustmail/src/commands/id/slash_command/id.rs | 21 ++++----- rustmail/src/commands/id/text_command/id.rs | 15 +++--- rustmail/src/commands/logs/common.rs | 12 ++--- rustmail/src/commands/logs/mod.rs | 1 + .../src/commands/logs/slash_command/logs.rs | 13 +++--- .../src/commands/logs/text_command/logs.rs | 21 ++++----- rustmail/src/commands/mod.rs | 8 ++-- rustmail/src/commands/move_thread/common.rs | 37 ++------------- rustmail/src/commands/move_thread/mod.rs | 1 + .../move_thread/slash_command/move_thread.rs | 20 +++----- .../move_thread/text_command/move_thread.rs | 46 +++++++++++++------ rustmail/src/commands/new_thread/common.rs | 6 +-- rustmail/src/commands/new_thread/mod.rs | 1 + .../new_thread/slash_command/new_thread.rs | 23 ++++------ .../new_thread/text_command/new_thread.rs | 22 ++++----- .../commands/recover/slash_command/recover.rs | 17 ++++--- .../commands/recover/text_command/recover.rs | 14 +++--- .../commands/release/slash_command/release.rs | 21 ++++----- .../commands/release/text_command/release.rs | 17 +++---- .../slash_command/remove_reminder.rs | 17 ++++--- .../text_command/remove_reminder.rs | 13 +++--- rustmail/src/commands/remove_staff/common.rs | 6 +-- rustmail/src/commands/remove_staff/mod.rs | 1 + .../slash_command/remove_staff.rs | 24 ++++------ .../remove_staff/text_command/remove_staff.rs | 26 +++++------ .../src/commands/reply/slash_command/reply.rs | 26 +++++------ .../src/commands/reply/text_command/reply.rs | 22 ++++----- rustmail/src/commands/take/common.rs | 6 +-- rustmail/src/commands/take/mod.rs | 1 + .../src/commands/take/slash_command/take.rs | 21 ++++----- .../src/commands/take/text_command/take.rs | 17 +++---- rustmail/src/db/mod.rs | 1 + rustmail/src/db/operations/logs.rs | 4 +- rustmail/src/db/operations/messages.rs | 7 ++- rustmail/src/db/operations/reminders.rs | 7 +-- rustmail/src/db/operations/scheduled.rs | 2 +- rustmail/src/db/operations/threads.rs | 7 ++- rustmail/src/errors/dictionary.rs | 15 +----- rustmail/src/errors/handler.rs | 6 +-- rustmail/src/errors/mod.rs | 25 +++++----- rustmail/src/errors/types.rs | 6 ++- rustmail/src/features/mod.rs | 8 ++-- rustmail/src/features/poll.rs | 6 +-- rustmail/src/handlers/guild_handler.rs | 11 ++--- .../src/handlers/guild_interaction_handler.rs | 13 ++---- .../src/handlers/guild_members_handler.rs | 12 ++--- .../guild_message_reactions_handler.rs | 29 ++++++------ .../src/handlers/guild_messages_handler.rs | 21 ++++----- .../src/handlers/guild_moderation_handler.rs | 10 ++-- rustmail/src/handlers/mod.rs | 9 ++++ rustmail/src/handlers/ready_handler.rs | 10 ++-- rustmail/src/handlers/typing_proxy_handler.rs | 4 +- rustmail/src/i18n/language/cn.rs | 2 +- rustmail/src/i18n/language/dt.rs | 2 +- rustmail/src/i18n/language/en.rs | 3 +- rustmail/src/i18n/language/fr.rs | 3 +- rustmail/src/i18n/language/gr.rs | 2 +- rustmail/src/i18n/language/it.rs | 2 +- rustmail/src/i18n/language/jp.rs | 2 +- rustmail/src/i18n/language/kr.rs | 2 +- rustmail/src/i18n/language/mod.rs | 12 +++++ rustmail/src/i18n/language/pr.rs | 2 +- rustmail/src/i18n/language/ru.rs | 2 +- rustmail/src/i18n/language/sp.rs | 2 +- rustmail/src/i18n/mod.rs | 5 +- rustmail/src/i18n/utils.rs | 2 +- rustmail/src/main.rs | 2 +- rustmail/src/modules/commands.rs | 12 ++--- rustmail/src/modules/message_recovery.rs | 12 ++--- rustmail/src/modules/mod.rs | 4 ++ rustmail/src/modules/scheduled_closures.rs | 2 +- rustmail/src/panel_commands/mod.rs | 2 + rustmail/src/panel_commands/user/mod.rs | 2 + rustmail/src/prelude/api.rs | 1 + rustmail/src/prelude/config.rs | 3 +- rustmail/src/prelude/db.rs | 5 -- rustmail/src/prelude/errors.rs | 12 +++-- rustmail/src/prelude/features.rs | 1 + rustmail/src/prelude/handlers.rs | 9 +--- rustmail/src/prelude/i18n.rs | 3 +- rustmail/src/prelude/mod.rs | 1 + rustmail/src/prelude/modules.rs | 2 +- rustmail/src/prelude/panel_commands.rs | 2 +- rustmail/src/types/bot.rs | 2 +- rustmail/src/utils/command/defer_response.rs | 2 +- rustmail/src/utils/command/mod.rs | 3 ++ rustmail/src/utils/command/wrap_command.rs | 2 +- rustmail/src/utils/conversion/mod.rs | 1 + rustmail/src/utils/message/message_builder.rs | 12 ++--- rustmail/src/utils/message/mod.rs | 8 +++- rustmail/src/utils/mod.rs | 1 + rustmail/src/utils/thread/fetch_thread.rs | 8 ++-- rustmail/src/utils/thread/get_thread_lock.rs | 2 +- rustmail/src/utils/thread/mod.rs | 2 + rustmail/src/utils/thread/send_to_thread.rs | 12 ++--- rustmail/src/utils/thread/user_recap.rs | 2 +- .../src/utils/time/get_member_join_date.rs | 2 +- rustmail/src/utils/time/mod.rs | 4 ++ 157 files changed, 694 insertions(+), 809 deletions(-) create mode 100644 rustmail/src/prelude/api.rs diff --git a/rustmail/src/api/handler/auth/callback.rs b/rustmail/src/api/handler/auth/callback.rs index ec52555..6249aaa 100644 --- a/rustmail/src/api/handler/auth/callback.rs +++ b/rustmail/src/api/handler/auth/callback.rs @@ -1,4 +1,4 @@ -use crate::types::bot::BotState; +use crate::prelude::types::*; use axum::extract::{Query, State}; use axum::response::Redirect; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; diff --git a/rustmail/src/api/handler/auth/login.rs b/rustmail/src/api/handler/auth/login.rs index 46faf21..ca5e926 100644 --- a/rustmail/src/api/handler/auth/login.rs +++ b/rustmail/src/api/handler/auth/login.rs @@ -1,9 +1,9 @@ -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; -use crate::types::bot::BotState; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::extract::{Query, State}; use axum::response::Redirect; use axum_extra::extract::CookieJar; -use sqlx::{query, Row}; +use sqlx::{Row, query}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/auth/logout.rs b/rustmail/src/api/handler/auth/logout.rs index 0501d72..70496a2 100644 --- a/rustmail/src/api/handler/auth/logout.rs +++ b/rustmail/src/api/handler/auth/logout.rs @@ -1,5 +1,5 @@ -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; -use crate::types::bot::BotState; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::extract::State; use axum::response::Redirect; use axum_extra::extract::CookieJar; diff --git a/rustmail/src/api/handler/auth/mod.rs b/rustmail/src/api/handler/auth/mod.rs index 47c9997..e1da44e 100644 --- a/rustmail/src/api/handler/auth/mod.rs +++ b/rustmail/src/api/handler/auth/mod.rs @@ -1,3 +1,7 @@ pub mod callback; pub mod login; pub mod logout; + +pub use callback::*; +pub use login::*; +pub use logout::*; diff --git a/rustmail/src/api/handler/bot/mod.rs b/rustmail/src/api/handler/bot/mod.rs index d85da0f..0c63b22 100644 --- a/rustmail/src/api/handler/bot/mod.rs +++ b/rustmail/src/api/handler/bot/mod.rs @@ -3,3 +3,9 @@ pub mod start; pub mod status; pub mod stop; pub mod tickets; + +pub use restart::*; +pub use start::*; +pub use status::*; +pub use stop::*; +pub use tickets::*; diff --git a/rustmail/src/api/handler/bot/restart.rs b/rustmail/src/api/handler/bot/restart.rs index 3f91690..fc04199 100644 --- a/rustmail/src/api/handler/bot/restart.rs +++ b/rustmail/src/api/handler/bot/restart.rs @@ -1,8 +1,8 @@ -use crate::api::utils::ping_internal::ping_internal; -use crate::types::bot::{BotState, BotStatus}; +use crate::prelude::api::*; +use crate::prelude::types::*; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use axum::Json; use sqlx::__rt::sleep; use std::sync::Arc; use std::time::Duration; diff --git a/rustmail/src/api/handler/bot/start.rs b/rustmail/src/api/handler/bot/start.rs index 0da2abc..e01edd7 100644 --- a/rustmail/src/api/handler/bot/start.rs +++ b/rustmail/src/api/handler/bot/start.rs @@ -1,9 +1,9 @@ use crate::bot::run_bot; -use crate::config::load_config; -use crate::types::bot::{BotState, BotStatus}; +use crate::prelude::config::*; +use crate::prelude::types::*; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use axum::Json; use std::sync::Arc; use tokio::spawn; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/bot/status.rs b/rustmail/src/api/handler/bot/status.rs index 656c881..d2b3063 100644 --- a/rustmail/src/api/handler/bot/status.rs +++ b/rustmail/src/api/handler/bot/status.rs @@ -1,8 +1,8 @@ -use crate::types::bot::{BotState, BotStatus}; +use crate::prelude::types::*; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; use axum::response::IntoResponse; -use axum::Json; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/bot/stop.rs b/rustmail/src/api/handler/bot/stop.rs index d6490d0..f58f56c 100644 --- a/rustmail/src/api/handler/bot/stop.rs +++ b/rustmail/src/api/handler/bot/stop.rs @@ -1,7 +1,7 @@ -use crate::types::bot::{BotState, BotStatus}; +use crate::prelude::types::*; +use axum::Json; use axum::extract::State; use axum::http::StatusCode; -use axum::Json; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/handler/bot/tickets.rs b/rustmail/src/api/handler/bot/tickets.rs index 28ae401..370722b 100644 --- a/rustmail/src/api/handler/bot/tickets.rs +++ b/rustmail/src/api/handler/bot/tickets.rs @@ -1,8 +1,8 @@ -use crate::types::bot::BotState; +use crate::prelude::types::*; use axum::{ + Json, extract::{Query, State}, http::StatusCode, - Json, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; diff --git a/rustmail/src/api/handler/mod.rs b/rustmail/src/api/handler/mod.rs index 6c84d29..ca20cbd 100644 --- a/rustmail/src/api/handler/mod.rs +++ b/rustmail/src/api/handler/mod.rs @@ -2,3 +2,8 @@ pub mod auth; pub mod bot; pub mod panel; pub mod user; + +pub use auth::*; +pub use bot::*; +pub use panel::*; +pub use user::*; diff --git a/rustmail/src/api/handler/panel/mod.rs b/rustmail/src/api/handler/panel/mod.rs index 7a6b958..0e1b226 100644 --- a/rustmail/src/api/handler/panel/mod.rs +++ b/rustmail/src/api/handler/panel/mod.rs @@ -1 +1,3 @@ pub mod panel; + +pub use panel::*; diff --git a/rustmail/src/api/handler/panel/panel.rs b/rustmail/src/api/handler/panel/panel.rs index cf3a24b..bfef707 100644 --- a/rustmail/src/api/handler/panel/panel.rs +++ b/rustmail/src/api/handler/panel/panel.rs @@ -1,4 +1,4 @@ -use crate::types::bot::BotState; +use crate::prelude::types::*; use axum::extract::State; use axum::response::IntoResponse; use std::sync::Arc; diff --git a/rustmail/src/api/handler/user/avatar.rs b/rustmail/src/api/handler/user/avatar.rs index e7c8976..1ff659e 100644 --- a/rustmail/src/api/handler/user/avatar.rs +++ b/rustmail/src/api/handler/user/avatar.rs @@ -1,5 +1,5 @@ -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; -use crate::types::bot::BotState; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::extract::State; use axum::response::IntoResponse; use axum_extra::extract::CookieJar; diff --git a/rustmail/src/api/handler/user/mod.rs b/rustmail/src/api/handler/user/mod.rs index 124369c..1e966d1 100644 --- a/rustmail/src/api/handler/user/mod.rs +++ b/rustmail/src/api/handler/user/mod.rs @@ -1 +1,3 @@ pub mod avatar; + +pub use avatar::*; diff --git a/rustmail/src/api/middleware/auth.rs b/rustmail/src/api/middleware/auth.rs index 31a2d67..d977ab2 100644 --- a/rustmail/src/api/middleware/auth.rs +++ b/rustmail/src/api/middleware/auth.rs @@ -1,5 +1,5 @@ -use crate::api::utils::get_user_id_from_session::get_user_id_from_session; -use crate::types::bot::{BotCommand, BotState, BotStatus}; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::extract::State; use axum::extract::{ConnectInfo, Request}; use axum::middleware::Next; @@ -8,7 +8,7 @@ use axum_extra::extract::CookieJar; use chrono::Utc; use hyper::StatusCode; use serenity::all::{GuildId, UserId}; -use sqlx::{query, Row}; +use sqlx::{Row, query}; use std::net::SocketAddr; use std::sync::Arc; use subtle::ConstantTimeEq; diff --git a/rustmail/src/api/middleware/mod.rs b/rustmail/src/api/middleware/mod.rs index 0e4a05d..efadf86 100644 --- a/rustmail/src/api/middleware/mod.rs +++ b/rustmail/src/api/middleware/mod.rs @@ -1 +1,3 @@ pub mod auth; + +pub use auth::*; diff --git a/rustmail/src/api/mod.rs b/rustmail/src/api/mod.rs index ffd27fd..af72813 100644 --- a/rustmail/src/api/mod.rs +++ b/rustmail/src/api/mod.rs @@ -3,3 +3,9 @@ pub mod middleware; pub mod router; pub mod routes; pub mod utils; + +pub use handler::*; +pub use middleware::*; +pub use router::*; +pub use routes::*; +pub use utils::*; diff --git a/rustmail/src/api/router.rs b/rustmail/src/api/router.rs index b038384..272993c 100644 --- a/rustmail/src/api/router.rs +++ b/rustmail/src/api/router.rs @@ -1,8 +1,5 @@ -use crate::api::routes::auth::create_auth_router; -use crate::api::routes::bot::create_bot_router; -use crate::api::routes::panel::create_panel_router; -use crate::api::routes::user::create_user_router; -use crate::types::bot::BotState; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::Router; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/auth.rs b/rustmail/src/api/routes/auth.rs index ee68e2b..2debcaf 100644 --- a/rustmail/src/api/routes/auth.rs +++ b/rustmail/src/api/routes/auth.rs @@ -1,9 +1,7 @@ -use crate::api::handler::auth::callback::handle_callback; -use crate::api::handler::auth::login::handle_login; -use crate::api::handler::auth::logout::handle_logout; -use crate::types::bot::BotState; -use axum::routing::get; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::Router; +use axum::routing::get; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index 37b4904..58ccefc 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -1,12 +1,7 @@ -use crate::api::handler::bot::restart::handle_restart_bot; -use crate::api::handler::bot::start::handle_start_bot; -use crate::api::handler::bot::status::handle_status_bot; -use crate::api::handler::bot::stop::handle_stop_bot; -use crate::api::handler::bot::tickets::handle_tickets_bot; -use crate::api::middleware::auth::auth_middleware; -use crate::types::bot::BotState; -use axum::routing::{get, post}; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::Router; +use axum::routing::{get, post}; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/mod.rs b/rustmail/src/api/routes/mod.rs index 6c84d29..ca20cbd 100644 --- a/rustmail/src/api/routes/mod.rs +++ b/rustmail/src/api/routes/mod.rs @@ -2,3 +2,8 @@ pub mod auth; pub mod bot; pub mod panel; pub mod user; + +pub use auth::*; +pub use bot::*; +pub use panel::*; +pub use user::*; diff --git a/rustmail/src/api/routes/panel.rs b/rustmail/src/api/routes/panel.rs index 93eaa23..ae3a7ab 100644 --- a/rustmail/src/api/routes/panel.rs +++ b/rustmail/src/api/routes/panel.rs @@ -1,8 +1,7 @@ -use crate::api::handler::panel::panel::handle_panel_check; -use crate::api::middleware::auth::auth_middleware; -use crate::types::bot::BotState; -use axum::routing::get; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::Router; +use axum::routing::get; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/routes/user.rs b/rustmail/src/api/routes/user.rs index 9bd4e5c..43aef11 100644 --- a/rustmail/src/api/routes/user.rs +++ b/rustmail/src/api/routes/user.rs @@ -1,8 +1,7 @@ -use crate::api::handler::user::avatar::handle_get_user_avatar; -use crate::api::middleware::auth::auth_middleware; -use crate::types::bot::BotState; -use axum::routing::get; +use crate::prelude::api::*; +use crate::prelude::types::*; use axum::Router; +use axum::routing::get; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/rustmail/src/api/utils/mod.rs b/rustmail/src/api/utils/mod.rs index 8f5fd4d..f4d433b 100644 --- a/rustmail/src/api/utils/mod.rs +++ b/rustmail/src/api/utils/mod.rs @@ -1,2 +1,5 @@ pub mod get_user_id_from_session; pub mod ping_internal; + +pub use get_user_id_from_session::*; +pub use ping_internal::*; diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index 6a4bfa3..ce6f18b 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -1,6 +1,6 @@ -use crate::db; use crate::prelude::commands::*; use crate::prelude::config::*; +use crate::prelude::db::*; use crate::prelude::errors::*; use crate::prelude::handlers::*; use crate::prelude::panel_commands::*; @@ -17,9 +17,7 @@ use tokio::sync::Mutex; use tokio::{select, spawn}; pub async fn init_bot_state() -> Arc> { - let pool = db::operations::init_database() - .await - .expect("An error occured!"); + let pool = init_database().await.expect("An error occured!"); println!("Database connected!"); let mut bytes = [0u8; 32]; diff --git a/rustmail/src/commands/add_reminder/common.rs b/rustmail/src/commands/add_reminder/common.rs index fe4bae9..aaba806 100644 --- a/rustmail/src/commands/add_reminder/common.rs +++ b/rustmail/src/commands/add_reminder/common.rs @@ -1,7 +1,6 @@ -use crate::config::Config; -use crate::db::reminders::{Reminder, is_reminder_active, update_reminder_status}; -use crate::utils::conversion::hex_string_to_int::hex_string_to_int; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::utils::*; use chrono::Local; use serenity::all::{ChannelId, CommandInteraction, Context, Message, UserId}; use sqlx::SqlitePool; diff --git a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs index 913fcba..92cd587 100644 --- a/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/slash_command/add_reminder.rs @@ -1,14 +1,10 @@ -use crate::commands::add_reminder::common::{ - send_register_confirmation_from_command, spawn_reminder, -}; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::reminders::{Reminder, insert_reminder}; -use crate::db::threads::get_thread_by_user_id; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use chrono::{Local, NaiveTime, TimeZone}; use regex::Regex; use serenity::FutureExt; @@ -100,7 +96,7 @@ impl RegistrableCommand for AddReminderCommand { let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let _ = defer_response(&ctx, &command).await; diff --git a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs index f5fd48c..6a267a7 100644 --- a/rustmail/src/commands/add_reminder/text_command/add_reminder.rs +++ b/rustmail/src/commands/add_reminder/text_command/add_reminder.rs @@ -1,12 +1,9 @@ -use crate::commands::add_reminder::common::{ - send_register_confirmation_from_message, spawn_reminder, -}; -use crate::config::Config; -use crate::db::reminders::{Reminder, insert_reminder}; -use crate::db::threads::get_thread_by_user_id; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::command::extract_reply_content::extract_reply_content; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use chrono::{Local, NaiveTime, TimeZone}; use regex::Regex; use serenity::all::{Context, Message}; @@ -21,7 +18,7 @@ pub async fn add_reminder( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let content = match extract_reply_content(&msg.content, &config.command.prefix, &["remind", "rem"]) { diff --git a/rustmail/src/commands/add_staff/common.rs b/rustmail/src/commands/add_staff/common.rs index 95aeb25..e2989a6 100644 --- a/rustmail/src/commands/add_staff/common.rs +++ b/rustmail/src/commands/add_staff/common.rs @@ -1,5 +1,5 @@ -use crate::config::Config; -use crate::errors::ModmailResult; +use crate::prelude::config::*; +use crate::prelude::errors::*; use serenity::all::{ ChannelId, Context, Message, PermissionOverwrite, PermissionOverwriteType, UserId, }; @@ -26,7 +26,7 @@ pub async fn add_user_to_channel( Ok(()) } -pub async fn extract_user_id(msg: &Message, config: &Config) -> String { +pub async fn extract_staff_id(msg: &Message, config: &Config) -> String { let content = msg.content.trim(); let prefix = &config.command.prefix; let command_names = ["addmod", "am"]; diff --git a/rustmail/src/commands/add_staff/mod.rs b/rustmail/src/commands/add_staff/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/add_staff/mod.rs +++ b/rustmail/src/commands/add_staff/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/add_staff/slash_command/add_staff.rs b/rustmail/src/commands/add_staff/slash_command/add_staff.rs index 58d8470..025ac37 100644 --- a/rustmail/src/commands/add_staff/slash_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/slash_command/add_staff.rs @@ -1,14 +1,10 @@ -use crate::commands::add_staff::common::add_user_to_channel; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::thread_exists; -use crate::errors::CommandError::InvalidFormat; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, @@ -80,7 +76,7 @@ impl RegistrableCommand for AddStaffCommand { let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response(&ctx, &command).await?; @@ -126,10 +122,10 @@ impl RegistrableCommand for AddStaffCommand { Ok(()) } - Err(..) => Err(ModmailError::Command(InvalidFormat)), + Err(..) => Err(ModmailError::Command(CommandError::InvalidFormat)), } } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } }) } diff --git a/rustmail/src/commands/add_staff/text_command/add_staff.rs b/rustmail/src/commands/add_staff/text_command/add_staff.rs index 8db61f9..bff1ea4 100644 --- a/rustmail/src/commands/add_staff/text_command/add_staff.rs +++ b/rustmail/src/commands/add_staff/text_command/add_staff.rs @@ -1,12 +1,9 @@ -use crate::commands::add_staff::common::add_user_to_channel; -use crate::commands::add_staff::common::extract_user_id; -use crate::config::Config; -use crate::db::thread_exists; -use crate::errors::CommandError::InvalidFormat; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{ModmailError, ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{Context, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; @@ -20,17 +17,17 @@ pub async fn add_staff( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; - let user_id_str = extract_user_id(&msg, config).await; + let user_id_str = extract_staff_id(&msg, config).await; if user_id_str.is_empty() { - return Err(ModmailError::Command(InvalidFormat)); + return Err(ModmailError::Command(CommandError::InvalidFormat)); } let user_id = match user_id_str.parse::() { Ok(id) => UserId::new(id), - Err(_) => return Err(ModmailError::Command(InvalidFormat)), + Err(_) => return Err(ModmailError::Command(CommandError::InvalidFormat)), }; if thread_exists(msg.author.id, pool).await { @@ -48,9 +45,9 @@ pub async fn add_staff( Ok(()) } - Err(..) => Err(ModmailError::Command(InvalidFormat)), + Err(..) => Err(ModmailError::Command(CommandError::InvalidFormat)), } } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } } diff --git a/rustmail/src/commands/alert/common.rs b/rustmail/src/commands/alert/common.rs index e25cbeb..ee4d61c 100644 --- a/rustmail/src/commands/alert/common.rs +++ b/rustmail/src/commands/alert/common.rs @@ -1,10 +1,7 @@ -use crate::config::Config; -use crate::db::{cancel_alert_for_staff, get_user_id_from_channel_id, set_alert_for_staff}; -use crate::errors::CommandError::AlertDoesNotExist; -use crate::errors::DatabaseError::QueryFailed; -use crate::errors::DiscordError::ApiError; -use crate::errors::{ModmailError, ModmailResult, common}; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::utils::*; use serenity::all::colours::branding::GREEN; use serenity::all::{CommandInteraction, Context, CreateInteractionResponse, Message}; use std::collections::HashMap; @@ -39,7 +36,7 @@ pub async fn get_thread_user_id_from_command( None => { let bot_user = match ctx.http.get_current_user().await { Ok(user) => user, - Err(e) => return Err(ModmailError::Discord(ApiError(e.to_string()))), + Err(e) => return Err(ModmailError::Discord(DiscordError::ApiError(e.to_string()))), }; let bot_user_id = ctx.cache.current_user().id; @@ -93,7 +90,7 @@ pub async fn handle_cancel_alert_from_command( pool: &sqlx::SqlitePool, ) -> ModmailResult<()> { if let Err(_) = cancel_alert_for_staff(command.user.id, user_id, pool).await { - Err(ModmailError::Command(AlertDoesNotExist)) + Err(ModmailError::Command(CommandError::AlertDoesNotExist)) } else { let mut params = HashMap::new(); params.insert("user".to_string(), format!("<@{}>", user_id)); @@ -143,7 +140,9 @@ pub async fn handle_set_alert_from_command( pool: &sqlx::SqlitePool, ) -> ModmailResult<()> { if let Err(e) = set_alert_for_staff(command.user.id, user_id, pool).await { - Err(ModmailError::Database(QueryFailed(e.to_string()))) + Err(ModmailError::Database(DatabaseError::QueryFailed( + e.to_string(), + ))) } else { let mut params = HashMap::new(); params.insert("user".to_string(), format!("<@{}>", user_id)); diff --git a/rustmail/src/commands/alert/mod.rs b/rustmail/src/commands/alert/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/alert/mod.rs +++ b/rustmail/src/commands/alert/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/alert/slash_command/alert.rs b/rustmail/src/commands/alert/slash_command/alert.rs index c0767c6..ac0eb4e 100644 --- a/rustmail/src/commands/alert/slash_command/alert.rs +++ b/rustmail/src/commands/alert/slash_command/alert.rs @@ -1,13 +1,9 @@ -use crate::commands::alert::common::{ - get_thread_user_id_from_command, handle_cancel_alert_from_command, - handle_set_alert_from_command, -}; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, @@ -78,7 +74,7 @@ impl RegistrableCommand for AlertCommand { let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response(&ctx, &command).await?; diff --git a/rustmail/src/commands/alert/text_command/alert.rs b/rustmail/src/commands/alert/text_command/alert.rs index a78a46c..e13710f 100644 --- a/rustmail/src/commands/alert/text_command/alert.rs +++ b/rustmail/src/commands/alert/text_command/alert.rs @@ -1,10 +1,7 @@ -use crate::commands::alert::common::{ - extract_alert_action, get_thread_user_id_from_msg, handle_cancel_alert_from_msg, - handle_set_alert_from_msg, -}; -use crate::config::Config; -use crate::errors::{ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; use serenity::all::{Context, Message}; use std::sync::Arc; @@ -17,7 +14,7 @@ pub async fn alert( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let user_id = get_thread_user_id_from_msg(&ctx, &msg, config, pool).await?; let is_cancel = extract_alert_action(&msg, config).await; diff --git a/rustmail/src/commands/anonreply/text_command/anonreply.rs b/rustmail/src/commands/anonreply/text_command/anonreply.rs index 6ca6a5c..c2c604f 100644 --- a/rustmail/src/commands/anonreply/text_command/anonreply.rs +++ b/rustmail/src/commands/anonreply/text_command/anonreply.rs @@ -1,11 +1,8 @@ -use crate::config::Config; -use crate::db::operations::allocate_next_message_number; -use crate::errors::{ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::command::extract_reply_content::extract_reply_content; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; -use crate::utils::thread::fetch_thread::fetch_thread; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; @@ -19,7 +16,7 @@ pub async fn anonreply( let db_pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let content = extract_reply_content(&msg.content, &config.command.prefix, &["anonreply", "ar"]); let intent = extract_intent(content, &msg.attachments).await; @@ -38,7 +35,7 @@ pub async fn anonreply( .send_and_forget() .await; - return Err(common::validation_failed("Missing content")); + return Err(validation_failed("Missing content")); }; let thread = fetch_thread(db_pool, &msg.channel_id.to_string()).await?; @@ -67,7 +64,7 @@ pub async fn anonreply( let next_message_number = allocate_next_message_number(&thread.id, db_pool) .await - .map_err(|_| common::validation_failed("Failed to allocate message number"))?; + .map_err(|_| validation_failed("Failed to allocate message number"))?; let _ = msg.delete(&ctx.http).await; @@ -109,7 +106,7 @@ pub async fn anonreply( .to_channel(msg.channel_id) .send_and_forget() .await; - return Err(common::validation_failed("Failed to send to thread")); + return Err(validation_failed("Failed to send to thread")); } }; diff --git a/rustmail/src/commands/close/mod.rs b/rustmail/src/commands/close/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/close/mod.rs +++ b/rustmail/src/commands/close/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/close/slash_command/close.rs b/rustmail/src/commands/close/slash_command/close.rs index f5007b0..1e78bf2 100644 --- a/rustmail/src/commands/close/slash_command/close.rs +++ b/rustmail/src/commands/close/slash_command/close.rs @@ -1,19 +1,10 @@ -use crate::commands::close::common::parse_duration_spec; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::{ - close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, -}; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::category::{ - get_category_id_from_command, get_category_name_from_command, - get_required_permissions_channel_from_command, -}; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::thread::fetch_thread::fetch_thread; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use chrono::Utc; use serenity::FutureExt; use serenity::all::{ @@ -118,7 +109,7 @@ impl RegistrableCommand for CloseCommand { let db_pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response(&ctx, &command).await?; diff --git a/rustmail/src/commands/close/text_command/close.rs b/rustmail/src/commands/close/text_command/close.rs index 0417ff7..7cd515f 100644 --- a/rustmail/src/commands/close/text_command/close.rs +++ b/rustmail/src/commands/close/text_command/close.rs @@ -1,16 +1,9 @@ -use crate::commands::close::common::parse_duration_spec; -use crate::config::Config; -use crate::db::{ - close_thread, delete_scheduled_closure, get_scheduled_closure, upsert_scheduled_closure, -}; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::message::category::{ - get_category_id_from_message, get_category_name_from_message, - get_required_permissions_channel_from_message, -}; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::thread::fetch_thread::fetch_thread; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use chrono::Utc; use serenity::all::{Channel, Context, GuildId, Message, PermissionOverwriteType, RoleId, UserId}; use std::collections::HashMap; @@ -27,7 +20,7 @@ pub async fn close( let db_pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let content = msg.content.trim(); let prefix = &config.command.prefix; diff --git a/rustmail/src/commands/delete/common.rs b/rustmail/src/commands/delete/common.rs index cad6074..e362db8 100644 --- a/rustmail/src/commands/delete/common.rs +++ b/rustmail/src/commands/delete/common.rs @@ -1,12 +1,8 @@ -use crate::config::Config; -use crate::db::messages::MessageIds; -use crate::db::repr::Thread; -use crate::db::{ - delete_message, get_message_ids_by_number, get_thread_by_channel_id, - get_user_id_from_channel_id, update_message_numbers_after_deletion, -}; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::utils::*; use serenity::all::{ChannelId, Context, Message, MessageId, UserId}; use std::collections::HashMap; @@ -17,21 +13,21 @@ pub async fn get_thread_info( let user_id = match get_user_id_from_channel_id(&channel_id, pool).await { Some(uid) => uid, None => { - return Err(common::validation_failed("Not in a thread")); + return Err(validation_failed("Not in a thread")); } }; let thread = match get_thread_by_channel_id(&channel_id, pool).await { Some(thread) => thread, None => { - return Err(common::validation_failed("Thread not found")); + return Err(validation_failed("Thread not found")); } }; Ok((user_id, thread)) } -pub async fn get_message_ids( +pub async fn get_message_ids_for_delete( user_id: i64, thread: &Thread, message_number: i64, @@ -49,7 +45,7 @@ pub async fn get_message_ids( None => { let mut params = HashMap::new(); params.insert("number".to_string(), message_number.to_string()); - Err(common::message_not_found("Try an other message number.")) + Err(message_not_found("Try an other message number.")) } } } @@ -116,7 +112,7 @@ pub async fn delete_database_message( && let Err(e) = delete_message(dm_msg_id, pool).await { eprintln!("Failed to delete message from database: {}", e); - return Err(common::database_connection_failed()); + return Err(database_connection_failed()); } Ok(()) } diff --git a/rustmail/src/commands/delete/mod.rs b/rustmail/src/commands/delete/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/delete/mod.rs +++ b/rustmail/src/commands/delete/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/delete/slash_command/delete.rs b/rustmail/src/commands/delete/slash_command/delete.rs index f4eeece..8914f04 100644 --- a/rustmail/src/commands/delete/slash_command/delete.rs +++ b/rustmail/src/commands/delete/slash_command/delete.rs @@ -1,15 +1,10 @@ -use crate::commands::delete::common::{ - delete_database_message, delete_discord_messages, get_message_ids, get_thread_info, - update_message_numbers, -}; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::messages::get_thread_message_by_message_id; -use crate::errors::{MessageError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response_ephemeral; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Context, @@ -86,7 +81,7 @@ impl RegistrableCommand for DeleteCommand { let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response_ephemeral(&ctx, &command).await?; @@ -134,7 +129,8 @@ impl RegistrableCommand for DeleteCommand { } } - let message_ids = get_message_ids(user_id, &thread, message_number, pool).await?; + let message_ids = + get_message_ids_for_delete(user_id, &thread, message_number, pool).await?; delete_discord_messages(&ctx, &command.channel_id, user_id, &message_ids).await?; delete_database_message(&message_ids, pool).await?; diff --git a/rustmail/src/commands/delete/text_command/delete.rs b/rustmail/src/commands/delete/text_command/delete.rs index 82a4936..9a6c5e6 100644 --- a/rustmail/src/commands/delete/text_command/delete.rs +++ b/rustmail/src/commands/delete/text_command/delete.rs @@ -1,10 +1,7 @@ -use crate::commands::delete::common::{ - delete_database_message, delete_discord_messages, extract_message_number, get_message_ids, - get_thread_info, send_delete_message, update_message_numbers, -}; -use crate::config::Config; -use crate::errors::{ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; use serenity::all::{Context, Message}; use std::sync::Arc; @@ -17,7 +14,7 @@ pub async fn delete( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let (user_id, thread) = get_thread_info(&msg.channel_id.to_string(), pool).await?; let message_number = extract_message_number(&msg, config).await; @@ -28,7 +25,7 @@ pub async fn delete( } let message_number = message_number.unwrap(); - let message_ids = get_message_ids(user_id, &thread, message_number, pool).await?; + let message_ids = get_message_ids_for_delete(user_id, &thread, message_number, pool).await?; delete_discord_messages(&ctx, &msg.channel_id, user_id, &message_ids).await?; delete_database_message(&message_ids, pool).await?; diff --git a/rustmail/src/commands/edit/common.rs b/rustmail/src/commands/edit/common.rs index 8711b29..16c13bd 100644 --- a/rustmail/src/commands/edit/common.rs +++ b/rustmail/src/commands/edit/common.rs @@ -1,7 +1,6 @@ -use crate::config::Config; -use crate::errors::ModmailResult; -use crate::errors::common::invalid_command; -use crate::utils::command::extract_reply_content::extract_reply_content; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::utils::*; use serenity::all::Message; pub fn extract_command_content(msg: &Message, config: &Config) -> ModmailResult { @@ -12,17 +11,13 @@ pub fn extract_command_content(msg: &Message, config: &Config) -> ModmailResult< #[cfg(test)] mod tests { use super::*; - use crate::config::{ - BotConfig, CommandConfig, Config, ErrorHandlingConfig, LanguageConfig, LogsConfig, - NotificationsConfig, ReminderConfig, ThreadConfig, - }; use std::sync::{Arc, Mutex}; fn create_test_config() -> Config { Config { bot: BotConfig { token: "test".to_string(), - mode: crate::config::ServerMode::Dual { + mode: ServerMode::Dual { community_guild_id: 184848, staff_guild_id: 64456, }, diff --git a/rustmail/src/commands/edit/message_ops.rs b/rustmail/src/commands/edit/message_ops.rs index d3456b4..5e8e4b0 100644 --- a/rustmail/src/commands/edit/message_ops.rs +++ b/rustmail/src/commands/edit/message_ops.rs @@ -1,13 +1,8 @@ -use crate::config::Config; -use crate::db::messages::{MessageIds, get_thread_message_by_inbox_message_id}; -use crate::db::operations::{ - get_message_ids_by_number, get_thread_by_channel_id, get_user_id_from_channel_id, -}; -use crate::errors::MessageError::{DmAccessFailed, EditFailed}; -use crate::errors::common::{incorrect_message_id, not_found, permission_denied, thread_not_found}; -use crate::errors::{ModmailError, ModmailResult}; -use crate::utils::conversion::hex_string_to_int::hex_string_to_int; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::utils::*; use serenity::all::{ ChannelId, CommandInteraction, Context, EditMessage, GuildId, Message, MessageId, User, UserId, }; @@ -137,7 +132,9 @@ pub async fn edit_inbox_message( .await { Ok(_) => Ok(()), - Err(e) => Err(ModmailError::Message(EditFailed(e.to_string()))), + Err(e) => Err(ModmailError::Message(MessageError::EditFailed( + e.to_string(), + ))), } } @@ -173,10 +170,12 @@ pub async fn edit_dm_message<'a>( let dm_channel = match user_id.create_dm_channel(&ctx.http).await { Ok(channel) => channel, Err(e) => { - return Err(ModmailError::Message(DmAccessFailed(format!( - "Unable to access user DM (Maybe the user doesn't allow private messages from bots) : {}", - e - )))); + return Err(ModmailError::Message(MessageError::DmAccessFailed( + format!( + "Unable to access user DM (Maybe the user doesn't allow private messages from bots) : {}", + e + ), + ))); } }; @@ -185,7 +184,11 @@ pub async fn edit_dm_message<'a>( .await { Ok(_) => Ok(()), - Err(e) => return Err(ModmailError::Message(EditFailed(e.to_string()))), + Err(e) => { + return Err(ModmailError::Message(MessageError::EditFailed( + e.to_string(), + ))); + } }; edit_result diff --git a/rustmail/src/commands/edit/mod.rs b/rustmail/src/commands/edit/mod.rs index a731dd7..98d13bd 100644 --- a/rustmail/src/commands/edit/mod.rs +++ b/rustmail/src/commands/edit/mod.rs @@ -4,6 +4,8 @@ pub mod slash_command; pub mod text_command; pub mod validation; +pub use common::*; pub use message_ops::*; pub use slash_command::*; pub use text_command::*; +pub use validation::*; diff --git a/rustmail/src/commands/edit/slash_command/edit.rs b/rustmail/src/commands/edit/slash_command/edit.rs index 1bbeae9..e8ec69d 100644 --- a/rustmail/src/commands/edit/slash_command/edit.rs +++ b/rustmail/src/commands/edit/slash_command/edit.rs @@ -1,15 +1,10 @@ -use crate::commands::edit::message_ops::{edit_messages, format_new_message, get_message_ids}; -use crate::commands::edit::validation::validate_edit_permissions; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::{get_thread_message_by_inbox_message_id, update_message_content}; -use crate::errors::common::message_not_found; -use crate::errors::{ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::conversion::hex_string_to_int::hex_string_to_int; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, @@ -102,7 +97,7 @@ impl RegistrableCommand for EditCommand { let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response(&ctx, &command).await?; diff --git a/rustmail/src/commands/edit/text_command/edit.rs b/rustmail/src/commands/edit/text_command/edit.rs index f8a82a7..95a9e8e 100644 --- a/rustmail/src/commands/edit/text_command/edit.rs +++ b/rustmail/src/commands/edit/text_command/edit.rs @@ -1,18 +1,9 @@ -use crate::commands::edit::common::extract_command_content; -use crate::commands::edit::message_ops::{ - cleanup_command_message, edit_messages, format_new_message, get_message_ids, -}; -use crate::commands::edit::validation::{ - EditCommandInput, parse_edit_command, validate_edit_permissions, -}; -use crate::config::Config; -use crate::db::get_thread_message_by_inbox_message_id; -use crate::db::update_message_content; -use crate::errors::common::message_not_found; -use crate::errors::{ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::conversion::hex_string_to_int::hex_string_to_int; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{Context, Message}; use std::collections::HashMap; use std::sync::Arc; @@ -26,7 +17,7 @@ pub async fn edit( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let raw_content: String = match extract_command_content(&msg, config) { Ok(content) => content, diff --git a/rustmail/src/commands/edit/validation.rs b/rustmail/src/commands/edit/validation.rs index e9ed2b9..27647ae 100644 --- a/rustmail/src/commands/edit/validation.rs +++ b/rustmail/src/commands/edit/validation.rs @@ -1,11 +1,9 @@ -use crate::db::messages::get_thread_message_by_inbox_message_id; -use crate::db::{get_message_ids_by_number, get_thread_by_channel_id}; -use crate::errors::common::{not_found, permission_denied, thread_not_found}; -use crate::errors::{ - CommandError, ModmailError, ModmailResult, ValidationError as ErrorValidationError, - command_error, -}; -use crate::i18n::get_translated_message; +use crate::command_error; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::ValidationError as ErrorValidationError; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; use serenity::all::{ChannelId, Context, Message, UserId}; #[derive(Debug)] @@ -40,7 +38,7 @@ impl From for ModmailError { } impl ValidationError { - pub async fn _error_message(&self, config: &crate::config::Config, msg: &Message) -> String { + pub async fn _error_message(&self, config: &Config, msg: &Message) -> String { let key = match self { ValidationError::InvalidFormat => "edit.validation.invalid_format", ValidationError::MissingMessageNumber => "edit.validation.missing_number", @@ -59,7 +57,7 @@ impl ValidationError { .await } - pub async fn _send_error(&self, ctx: &Context, msg: &Message, config: &crate::config::Config) { + pub async fn _send_error(&self, ctx: &Context, msg: &Message, config: &Config) { let error_msg = self._error_message(config, msg).await; let _ = msg.reply(ctx, error_msg).await; } diff --git a/rustmail/src/commands/force_close/common.rs b/rustmail/src/commands/force_close/common.rs index 0308144..0d47913 100644 --- a/rustmail/src/commands/force_close/common.rs +++ b/rustmail/src/commands/force_close/common.rs @@ -1,5 +1,4 @@ -use crate::errors::DiscordError::ApiError; -use crate::errors::{ModmailError, ModmailResult}; +use crate::prelude::errors::*; use serenity::all::{ChannelId, Context}; pub async fn delete_channel(ctx: &Context, channel_id: ChannelId) -> ModmailResult<()> { @@ -8,6 +7,6 @@ pub async fn delete_channel(ctx: &Context, channel_id: ChannelId) -> ModmailResu println!("Channel {} deleted successfully", channel_id); Ok(()) } - Err(e) => Err(ModmailError::Discord(ApiError(e.to_string()))), + Err(e) => Err(ModmailError::Discord(DiscordError::ApiError(e.to_string()))), } } diff --git a/rustmail/src/commands/force_close/mod.rs b/rustmail/src/commands/force_close/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/force_close/mod.rs +++ b/rustmail/src/commands/force_close/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/force_close/slash_command/force_close.rs b/rustmail/src/commands/force_close/slash_command/force_close.rs index 466c2da..22d8a43 100644 --- a/rustmail/src/commands/force_close/slash_command/force_close.rs +++ b/rustmail/src/commands/force_close/slash_command/force_close.rs @@ -1,12 +1,9 @@ -use crate::commands::force_close::common::delete_channel; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; -use crate::errors::DatabaseError::QueryFailed; -use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{ModmailError, ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; use serenity::FutureExt; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; @@ -59,7 +56,7 @@ impl RegistrableCommand for ForceCloseCommand { let db_pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; if !is_a_ticket_channel(command.channel_id, db_pool).await { match command.channel_id.to_channel(&ctx.http).await { @@ -67,7 +64,7 @@ impl RegistrableCommand for ForceCloseCommand { let guild_channel = match channel.guild() { Some(guild_channel) => guild_channel, None => { - return Err(ModmailError::Thread(NotAThreadChannel)); + return Err(ModmailError::Thread(ThreadError::NotAThreadChannel)); } }; @@ -75,12 +72,12 @@ impl RegistrableCommand for ForceCloseCommand { if category_id == config.thread.inbox_category_id { delete_channel(&ctx, command.channel_id).await?; } else { - return Err(ModmailError::Thread(NotAThreadChannel)); + return Err(ModmailError::Thread(ThreadError::NotAThreadChannel)); } } } Err(_) => { - return Err(ModmailError::Thread(NotAThreadChannel)); + return Err(ModmailError::Thread(ThreadError::NotAThreadChannel)); } } } @@ -88,11 +85,11 @@ impl RegistrableCommand for ForceCloseCommand { match is_orphaned_thread_channel(command.channel_id, db_pool).await { Ok(res) => { if !res { - return Err(ModmailError::Thread(UserStillInServer)); + return Err(ModmailError::Thread(ThreadError::UserStillInServer)); } delete_channel(&ctx, command.channel_id).await } - Err(..) => Err(ModmailError::Database(QueryFailed( + Err(..) => Err(ModmailError::Database(DatabaseError::QueryFailed( "Failed to check if thread channel is orphaned".to_string(), ))), } diff --git a/rustmail/src/commands/force_close/text_command/force_close.rs b/rustmail/src/commands/force_close/text_command/force_close.rs index fc5bccb..47e7624 100644 --- a/rustmail/src/commands/force_close/text_command/force_close.rs +++ b/rustmail/src/commands/force_close/text_command/force_close.rs @@ -1,10 +1,8 @@ -use crate::commands::force_close::common::delete_channel; -use crate::config::Config; -use crate::db::threads::{is_a_ticket_channel, is_orphaned_thread_channel}; -use crate::errors::DatabaseError::QueryFailed; -use crate::errors::ThreadError::{NotAThreadChannel, UserStillInServer}; -use crate::errors::{ModmailError, ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; use serenity::all::{Context, Message}; use std::sync::Arc; @@ -17,25 +15,25 @@ pub async fn force_close( let db_pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; if !is_a_ticket_channel(msg.channel_id, db_pool).await { return match msg.category_id(&ctx.http).await { Some(category_id) if category_id == config.thread.inbox_category_id => { delete_channel(&ctx, msg.channel_id).await } - _ => Err(ModmailError::Thread(NotAThreadChannel)), + _ => Err(ModmailError::Thread(ThreadError::NotAThreadChannel)), }; } match is_orphaned_thread_channel(msg.channel_id, db_pool).await { Ok(res) => { if !res { - return Err(ModmailError::Thread(UserStillInServer)); + return Err(ModmailError::Thread(ThreadError::UserStillInServer)); } delete_channel(&ctx, msg.channel_id).await } - Err(..) => Err(ModmailError::Database(QueryFailed( + Err(..) => Err(ModmailError::Database(DatabaseError::QueryFailed( "Failed to check if thread channel is orphaned".to_string(), ))), } diff --git a/rustmail/src/commands/help/common.rs b/rustmail/src/commands/help/common.rs index 012d425..ecb7004 100644 --- a/rustmail/src/commands/help/common.rs +++ b/rustmail/src/commands/help/common.rs @@ -1,8 +1,8 @@ -use crate::commands::CommandRegistry; -use crate::config::Config; -use crate::errors::{CommandError, ModmailError, ModmailResult}; -use crate::i18n::get_translated_message; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::{CommandInteraction, Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/help/mod.rs b/rustmail/src/commands/help/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/help/mod.rs +++ b/rustmail/src/commands/help/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/help/slash_command/help.rs b/rustmail/src/commands/help/slash_command/help.rs index 016b5f4..4b48b10 100644 --- a/rustmail/src/commands/help/slash_command/help.rs +++ b/rustmail/src/commands/help/slash_command/help.rs @@ -1,15 +1,14 @@ -use crate::commands::help::common::{display_command_help, display_commands_list}; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::errors::ModmailResult; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; +use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, ResolvedOption, }; -use serenity::FutureExt; use std::sync::Arc; pub struct HelpCommand; diff --git a/rustmail/src/commands/help/text_command/help.rs b/rustmail/src/commands/help/text_command/help.rs index 388ab41..3dd72df 100644 --- a/rustmail/src/commands/help/text_command/help.rs +++ b/rustmail/src/commands/help/text_command/help.rs @@ -1,7 +1,7 @@ -use crate::commands::help::common::{display_command_help, display_commands_list}; -use crate::config::Config; -use crate::errors::ModmailResult; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; use serenity::all::{Context, Message}; use std::sync::Arc; diff --git a/rustmail/src/commands/id/slash_command/id.rs b/rustmail/src/commands/id/slash_command/id.rs index e232c8b..b3c4344 100644 --- a/rustmail/src/commands/id/slash_command/id.rs +++ b/rustmail/src/commands/id/slash_command/id.rs @@ -1,13 +1,10 @@ -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::{NotAThreadChannel, ThreadNotFound}; -use crate::errors::{DatabaseError, ModmailError, ModmailResult}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{CommandInteraction, Context, ResolvedOption}; use serenity::builder::CreateCommand; @@ -67,14 +64,14 @@ impl RegistrableCommand for IdCommand { defer_response(&ctx, &command).await?; if !is_a_ticket_channel(command.channel_id, pool).await { - return Err(ModmailError::Thread(NotAThreadChannel)); + return Err(ModmailError::Thread(ThreadError::NotAThreadChannel)); } let thread = match get_thread_by_channel_id(&command.channel_id.to_string(), pool).await { Some(thread) => thread, None => { - return Err(ModmailError::Thread(ThreadNotFound)); + return Err(ModmailError::Thread(ThreadError::ThreadNotFound)); } }; diff --git a/rustmail/src/commands/id/text_command/id.rs b/rustmail/src/commands/id/text_command/id.rs index 034c79a..a1d100e 100644 --- a/rustmail/src/commands/id/text_command/id.rs +++ b/rustmail/src/commands/id/text_command/id.rs @@ -1,11 +1,8 @@ -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::common::{database_connection_failed, thread_not_found}; -use crate::errors::{ModmailError, ModmailResult}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{Context, Message}; use std::sync::Arc; @@ -42,6 +39,6 @@ pub async fn id( Ok(()) } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } } diff --git a/rustmail/src/commands/logs/common.rs b/rustmail/src/commands/logs/common.rs index e431a1a..e86681a 100644 --- a/rustmail/src/commands/logs/common.rs +++ b/rustmail/src/commands/logs/common.rs @@ -1,13 +1,13 @@ -use crate::config::Config; -use crate::errors::ModmailError; -use crate::i18n::get_translated_message; -use crate::types::logs::TicketLog; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; +use crate::prelude::types::*; +use crate::prelude::utils::*; use serenity::all::{ChannelId, CommandInteraction, Message}; use serenity::builder::CreateActionRow; use serenity::client::Context; -pub fn extract_user_id(msg: &Message, config: &Config) -> String { +pub fn extract_user_id_for_logs(msg: &Message, config: &Config) -> String { let content = msg.content.trim(); let prefix = &config.command.prefix; let command_name = "logs"; diff --git a/rustmail/src/commands/logs/mod.rs b/rustmail/src/commands/logs/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/logs/mod.rs +++ b/rustmail/src/commands/logs/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/logs/slash_command/logs.rs b/rustmail/src/commands/logs/slash_command/logs.rs index beea161..efe23e4 100644 --- a/rustmail/src/commands/logs/slash_command/logs.rs +++ b/rustmail/src/commands/logs/slash_command/logs.rs @@ -1,10 +1,9 @@ -use crate::commands::logs::text_command::logs::{handle_logs_from_user_id, handle_logs_in_thread}; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::errors::{DatabaseError, ModmailError, ModmailResult}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, diff --git a/rustmail/src/commands/logs/text_command/logs.rs b/rustmail/src/commands/logs/text_command/logs.rs index af87590..be912d5 100644 --- a/rustmail/src/commands/logs/text_command/logs.rs +++ b/rustmail/src/commands/logs/text_command/logs.rs @@ -1,13 +1,12 @@ -use crate::commands::logs::common::{extract_user_id, get_response, render_logs_page}; -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::db::logs::get_logs_from_user_id; -use crate::errors::{DatabaseError, ModmailError, ModmailResult, ThreadError}; -use crate::features::make_buttons; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::i18n::get_translated_message; -use crate::modules::commands::LOGS_PAGE_SIZE; -use crate::types::logs::{PaginationContext, PaginationStore}; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::features::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::modules::*; +use crate::prelude::types::*; use serenity::all::{ButtonStyle, ChannelId, CommandInteraction, Context, Message}; use sqlx::SqlitePool; use std::sync::Arc; @@ -116,7 +115,7 @@ pub async fn logs( None => return Err(ModmailError::Database(DatabaseError::ConnectionFailed)), }; - let user_id = extract_user_id(&msg, &config); + let user_id = extract_user_id_for_logs(&msg, &config); if user_id.is_empty() { handle_logs_in_thread( diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index 7f067b3..273a77e 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -1,7 +1,7 @@ -use crate::config::Config; -use crate::errors::ModmailResult; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::types::logs::PaginationStore; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::types::*; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::any::Any; use std::collections::HashMap; diff --git a/rustmail/src/commands/move_thread/common.rs b/rustmail/src/commands/move_thread/common.rs index fa7f4b8..683ec46 100644 --- a/rustmail/src/commands/move_thread/common.rs +++ b/rustmail/src/commands/move_thread/common.rs @@ -1,7 +1,7 @@ -use crate::config::Config; -use crate::db::get_user_id_from_channel_id; -use crate::i18n::get_translated_message; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::{ChannelId, CommandInteraction, Context, EditChannel, GuildId, Message}; use std::collections::HashMap; @@ -66,35 +66,6 @@ pub async fn move_channel_to_category_by_command_option( .await } -pub async fn send_success_message( - ctx: &Context, - msg: &Message, - config: &Config, - category_name: &str, -) { - let mut params = HashMap::new(); - params.insert("category".to_string(), category_name.to_string()); - params.insert("staff".to_string(), msg.author.name.clone()); - - let confirmation_msg = get_translated_message( - config, - "move_thread.success", - Some(¶ms), - Some(msg.author.id), - msg.guild_id.map(|g| g.get()), - None, - ) - .await; - - let _ = MessageBuilder::system_message(ctx, config) - .content(confirmation_msg) - .to_channel(msg.channel_id) - .send(true) - .await; - - let _ = msg.delete(&ctx.http).await; -} - pub fn find_best_match_category( target_name: &str, categories: &[(ChannelId, String)], diff --git a/rustmail/src/commands/move_thread/mod.rs b/rustmail/src/commands/move_thread/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/move_thread/mod.rs +++ b/rustmail/src/commands/move_thread/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/move_thread/slash_command/move_thread.rs b/rustmail/src/commands/move_thread/slash_command/move_thread.rs index 4309839..6e09044 100644 --- a/rustmail/src/commands/move_thread/slash_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/slash_command/move_thread.rs @@ -1,16 +1,10 @@ -use crate::commands::move_thread::common::{ - fetch_server_categories, find_best_match_category, move_channel_to_category_by_command_option, -}; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::get_user_id_from_channel_id; -use crate::errors::{ - CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, ThreadError, -}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, diff --git a/rustmail/src/commands/move_thread/text_command/move_thread.rs b/rustmail/src/commands/move_thread/text_command/move_thread.rs index 6811c0a..3369960 100644 --- a/rustmail/src/commands/move_thread/text_command/move_thread.rs +++ b/rustmail/src/commands/move_thread/text_command/move_thread.rs @@ -1,13 +1,11 @@ -use crate::commands::move_thread::common::{ - extract_category_name, fetch_server_categories, find_best_match_category, is_in_thread, - move_channel_to_category_by_msg, send_success_message, -}; -use crate::config::Config; -use crate::errors::CommandError::NotInThread; -use crate::errors::ThreadError::CategoryNotFound; -use crate::errors::{CommandError, DiscordError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::{Context, Message}; +use std::collections::HashMap; use std::sync::Arc; pub async fn move_thread( @@ -19,15 +17,15 @@ pub async fn move_thread( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; if !is_in_thread(&msg, pool).await { - return Err(ModmailError::Command(NotInThread())); + return Err(ModmailError::Command(CommandError::NotInThread())); } let category_name = extract_category_name(&msg, config).await; if category_name.is_empty() { - return Err(ModmailError::Thread(CategoryNotFound)); + return Err(ModmailError::Thread(ThreadError::CategoryNotFound)); } let categories = fetch_server_categories(&ctx, config).await; @@ -45,10 +43,30 @@ pub async fn move_thread( ))); } - send_success_message(&ctx, &msg, config, &category_name).await; + let mut params = HashMap::new(); + params.insert("category".to_string(), category_name.to_string()); + params.insert("staff".to_string(), msg.author.name.clone()); + + let confirmation_msg = get_translated_message( + config, + "move_thread.success", + Some(¶ms), + Some(msg.author.id), + msg.guild_id.map(|g| g.get()), + None, + ) + .await; + + let _ = MessageBuilder::system_message(&ctx.clone(), config) + .content(confirmation_msg) + .to_channel(msg.channel_id) + .send(true) + .await; + + let _ = msg.delete(&ctx.http).await; } None => { - return Err(ModmailError::Thread(CategoryNotFound)); + return Err(ModmailError::Thread(ThreadError::CategoryNotFound)); } } diff --git a/rustmail/src/commands/new_thread/common.rs b/rustmail/src/commands/new_thread/common.rs index 3549f08..d0f5978 100644 --- a/rustmail/src/commands/new_thread/common.rs +++ b/rustmail/src/commands/new_thread/common.rs @@ -1,6 +1,6 @@ -use crate::config::Config; -use crate::errors::ModmailResult; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::utils::*; use serenity::all::{Context, GuildChannel, Message, User, UserId}; use std::collections::HashMap; diff --git a/rustmail/src/commands/new_thread/mod.rs b/rustmail/src/commands/new_thread/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/new_thread/mod.rs +++ b/rustmail/src/commands/new_thread/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index b6132d2..64fc05c 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -1,17 +1,10 @@ -use crate::commands::new_thread::common::send_welcome_message; -use crate::commands::{BoxFuture, CommunityRegistrable, RegistrableCommand}; -use crate::config::Config; -use crate::db::logs::get_logs_from_user_id; -use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; -use crate::errors::{ - CommandError, DatabaseError, DiscordError, ModmailError, ModmailResult, common, -}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::thread::user_recap::get_user_recap; -use crate::utils::time::get_member_join_date::get_member_join_date_for_user; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ ChannelId, CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Context, @@ -90,7 +83,7 @@ impl RegistrableCommand for NewThreadCommand { let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response(&ctx, &command).await?; diff --git a/rustmail/src/commands/new_thread/text_command/new_thread.rs b/rustmail/src/commands/new_thread/text_command/new_thread.rs index 1e86e78..cb7b79b 100644 --- a/rustmail/src/commands/new_thread/text_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/text_command/new_thread.rs @@ -1,16 +1,10 @@ -use crate::commands::new_thread::common::{ - extract_user_id, send_dm_to_user, send_error_message, send_success_message, - send_welcome_message, -}; -use crate::config::Config; -use crate::db::logs::get_logs_from_user_id; -use crate::db::{create_thread_for_user, get_thread_channel_by_user_id, thread_exists}; -use crate::errors::{DiscordError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::i18n::get_translated_message; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::thread::user_recap::get_user_recap; -use crate::utils::time::get_member_join_date::get_member_join_date_for_user; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::{ChannelId, Context, GuildId, Message}; use std::collections::HashMap; use std::sync::Arc; @@ -24,7 +18,7 @@ pub async fn new_thread( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let user_id = extract_user_id(&msg, config).await; if user_id.is_none() { diff --git a/rustmail/src/commands/recover/slash_command/recover.rs b/rustmail/src/commands/recover/slash_command/recover.rs index 5435de2..08e9abd 100644 --- a/rustmail/src/commands/recover/slash_command/recover.rs +++ b/rustmail/src/commands/recover/slash_command/recover.rs @@ -1,11 +1,10 @@ -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::errors::{ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::modules::message_recovery::recover_missing_messages; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::modules::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::collections::HashMap; @@ -58,7 +57,7 @@ impl RegistrableCommand for RecoverCommand { let _ = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response(&ctx, &command).await?; diff --git a/rustmail/src/commands/recover/text_command/recover.rs b/rustmail/src/commands/recover/text_command/recover.rs index 54e2dac..12f7fbc 100644 --- a/rustmail/src/commands/recover/text_command/recover.rs +++ b/rustmail/src/commands/recover/text_command/recover.rs @@ -1,9 +1,9 @@ -use crate::config::Config; -use crate::errors::{ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::i18n::get_translated_message; -use crate::modules::message_recovery::recover_missing_messages; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::modules::*; +use crate::prelude::utils::*; use serenity::all::{Context, Message}; use std::collections::HashMap; use std::sync::Arc; @@ -17,7 +17,7 @@ pub async fn recover( let _ = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let mut params = HashMap::new(); params.insert("user".to_string(), msg.author.name.clone()); diff --git a/rustmail/src/commands/release/slash_command/release.rs b/rustmail/src/commands/release/slash_command/release.rs index c61ab27..9ac0bb8 100644 --- a/rustmail/src/commands/release/slash_command/release.rs +++ b/rustmail/src/commands/release/slash_command/release.rs @@ -1,15 +1,10 @@ -use crate::commands::take::common::rename_channel_with_timeout; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::common::{database_connection_failed, thread_not_found}; -use crate::errors::{CommandError, ModmailError, ModmailResult}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ChannelId, CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; @@ -113,7 +108,7 @@ impl RegistrableCommand for ReleaseCommand { Ok(()) } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } }) } diff --git a/rustmail/src/commands/release/text_command/release.rs b/rustmail/src/commands/release/text_command/release.rs index 05b1b51..0a6d5c2 100644 --- a/rustmail/src/commands/release/text_command/release.rs +++ b/rustmail/src/commands/release/text_command/release.rs @@ -1,12 +1,9 @@ -use crate::commands::take::common::rename_channel_with_timeout; -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::common::{database_connection_failed, thread_not_found}; -use crate::errors::{CommandError, ModmailError, ModmailResult}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{ChannelId, Context, Message}; use std::sync::Arc; @@ -62,6 +59,6 @@ pub async fn release( Ok(()) } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } } diff --git a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs index 9d83f2a..681c4ff 100644 --- a/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/slash_command/remove_reminder.rs @@ -1,11 +1,10 @@ -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, CreateCommand, @@ -74,7 +73,7 @@ impl RegistrableCommand for RemoveReminderCommand { let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let _ = defer_response(&ctx, &command).await; diff --git a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs index 98915a1..26a0c0f 100644 --- a/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs +++ b/rustmail/src/commands/remove_reminder/text_command/remove_reminder.rs @@ -1,9 +1,8 @@ -use crate::config::Config; -use crate::db::reminders::{get_reminder_by_id, update_reminder_status}; -use crate::errors::{CommandError, DatabaseError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::command::extract_reply_content::extract_reply_content; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{Context, Message}; use std::collections::HashMap; use std::sync::Arc; @@ -17,7 +16,7 @@ pub async fn remove_reminder( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let content = match extract_reply_content(&msg.content, &config.command.prefix, &["unremind", "urem"]) { diff --git a/rustmail/src/commands/remove_staff/common.rs b/rustmail/src/commands/remove_staff/common.rs index f280334..c7bdbdc 100644 --- a/rustmail/src/commands/remove_staff/common.rs +++ b/rustmail/src/commands/remove_staff/common.rs @@ -1,5 +1,5 @@ -use crate::config::Config; -use crate::errors::ModmailResult; +use crate::prelude::config::*; +use crate::prelude::errors::*; use serenity::all::{ ChannelId, Context, Message, PermissionOverwrite, PermissionOverwriteType, Permissions, UserId, }; @@ -25,7 +25,7 @@ pub async fn remove_user_from_channel( Ok(()) } -pub async fn extract_user_id(msg: &Message, config: &Config) -> String { +pub async fn extract_remove_staff_id(msg: &Message, config: &Config) -> String { let content = msg.content.trim(); let prefix = &config.command.prefix; let command_names = ["delmod", "dm"]; diff --git a/rustmail/src/commands/remove_staff/mod.rs b/rustmail/src/commands/remove_staff/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/remove_staff/mod.rs +++ b/rustmail/src/commands/remove_staff/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs index b1b6b23..4b5fcdc 100644 --- a/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/slash_command/remove_staff.rs @@ -1,14 +1,10 @@ -use crate::commands::remove_staff::common::remove_user_from_channel; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::thread_exists; -use crate::errors::CommandError::InvalidFormat; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{CommandError, ModmailError, ModmailResult, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Context, @@ -82,7 +78,7 @@ impl RegistrableCommand for RemoveStaffCommand { let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response(&ctx, &command).await?; @@ -138,10 +134,10 @@ impl RegistrableCommand for RemoveStaffCommand { Ok(()) } - Err(..) => Err(ModmailError::Command(InvalidFormat)), + Err(..) => Err(ModmailError::Command(CommandError::InvalidFormat)), } } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } }) } diff --git a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs index b46d68a..2a2f146 100644 --- a/rustmail/src/commands/remove_staff/text_command/remove_staff.rs +++ b/rustmail/src/commands/remove_staff/text_command/remove_staff.rs @@ -1,11 +1,9 @@ -use crate::commands::remove_staff::common::{extract_user_id, remove_user_from_channel}; -use crate::config::Config; -use crate::db::thread_exists; -use crate::errors::CommandError::InvalidFormat; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::{ModmailError, ModmailResult, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{Context, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; @@ -19,17 +17,17 @@ pub async fn remove_staff( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; - let user_id_str = extract_user_id(&msg, config).await; + let user_id_str = extract_remove_staff_id(&msg, config).await; if user_id_str.is_empty() { - return Err(ModmailError::Command(InvalidFormat)); + return Err(ModmailError::Command(CommandError::InvalidFormat)); } let user_id = match user_id_str.parse::() { Ok(id) => UserId::new(id), - Err(_) => return Err(ModmailError::Command(InvalidFormat)), + Err(_) => return Err(ModmailError::Command(CommandError::InvalidFormat)), }; if thread_exists(msg.author.id, pool).await { @@ -47,9 +45,9 @@ pub async fn remove_staff( Ok(()) } - Err(..) => Err(ModmailError::Command(InvalidFormat)), + Err(..) => Err(ModmailError::Command(CommandError::InvalidFormat)), } } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } } diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index e7de5ab..237a2ae 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -1,14 +1,10 @@ -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::allocate_next_message_number; -use crate::errors::MessageError::MessageEmpty; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; -use crate::utils::thread::fetch_thread::fetch_thread; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ Attachment, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, @@ -115,7 +111,7 @@ impl RegistrableCommand for ReplyCommand { let db_pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; defer_response(&ctx, &command).await?; @@ -143,7 +139,7 @@ impl RegistrableCommand for ReplyCommand { let intent = extract_intent(content, &attachments).await; let Some(intent) = intent else { - return Err(ModmailError::Message(MessageEmpty)); + return Err(ModmailError::Message(MessageError::MessageEmpty)); }; let thread = fetch_thread(db_pool, &command.channel_id.to_string()).await?; @@ -157,7 +153,7 @@ impl RegistrableCommand for ReplyCommand { let next_message_number = allocate_next_message_number(&thread.id, db_pool) .await - .map_err(|_| common::validation_failed("Failed to allocate message number"))?; + .map_err(|_| validation_failed("Failed to allocate message number"))?; let mut sr = MessageBuilder::begin_staff_reply( &ctx, @@ -186,7 +182,7 @@ impl RegistrableCommand for ReplyCommand { let (_, dm_msg_opt) = match sr.send_command_and_record(&command, db_pool).await { Ok(tuple) => tuple, Err(_) => { - return Err(common::validation_failed("Failed to send to thread")); + return Err(validation_failed("Failed to send to thread")); } }; diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index d36e2fa..efdf3d9 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -1,12 +1,8 @@ -use crate::config::Config; -use crate::db::allocate_next_message_number; -use crate::errors::MessageError::MessageEmpty; -use crate::errors::{CommandError, ModmailError, ModmailResult, ThreadError, common}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::command::extract_reply_content::extract_reply_content; -use crate::utils::message::message_builder::MessageBuilder; -use crate::utils::message::reply_intent::{ReplyIntent, extract_intent}; -use crate::utils::thread::fetch_thread::fetch_thread; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; @@ -20,13 +16,13 @@ pub async fn reply( let db_pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let content = extract_reply_content(&msg.content, &config.command.prefix, &["reply", "r"]); let intent = extract_intent(content, &msg.attachments).await; let Some(intent) = intent else { - return Err(ModmailError::Message(MessageEmpty)); + return Err(ModmailError::Message(MessageError::MessageEmpty)); }; let thread = fetch_thread(db_pool, &msg.channel_id.to_string()).await?; @@ -40,7 +36,7 @@ pub async fn reply( let next_message_number = allocate_next_message_number(&thread.id, db_pool) .await - .map_err(|_| common::validation_failed("Failed to allocate message number"))?; + .map_err(|_| validation_failed("Failed to allocate message number"))?; let _ = msg.delete(&ctx.http).await; @@ -70,7 +66,7 @@ pub async fn reply( let (_, dm_msg_opt) = match sr.send_msg_and_record(db_pool).await { Ok(tuple) => tuple, Err(_) => { - return Err(common::validation_failed("Failed to send to thread")); + return Err(validation_failed("Failed to send to thread")); } }; diff --git a/rustmail/src/commands/take/common.rs b/rustmail/src/commands/take/common.rs index d9bdbd9..7a0e688 100644 --- a/rustmail/src/commands/take/common.rs +++ b/rustmail/src/commands/take/common.rs @@ -1,6 +1,6 @@ -use crate::config::Config; -use crate::errors::{DiscordError, ModmailError, ModmailResult}; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::utils::*; use serenity::all::{ChannelId, CommandInteraction, Context, EditChannel, Message}; use std::time::Duration; use tokio::time::sleep; diff --git a/rustmail/src/commands/take/mod.rs b/rustmail/src/commands/take/mod.rs index a983339..e08ac47 100644 --- a/rustmail/src/commands/take/mod.rs +++ b/rustmail/src/commands/take/mod.rs @@ -2,5 +2,6 @@ pub mod common; pub mod slash_command; pub mod text_command; +pub use common::*; pub use slash_command::*; pub use text_command::*; diff --git a/rustmail/src/commands/take/slash_command/take.rs b/rustmail/src/commands/take/slash_command/take.rs index a919d92..7f0bc3c 100644 --- a/rustmail/src/commands/take/slash_command/take.rs +++ b/rustmail/src/commands/take/slash_command/take.rs @@ -1,15 +1,10 @@ -use crate::commands::take::common::rename_channel_with_timeout; -use crate::commands::{BoxFuture, RegistrableCommand}; -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::common::{database_connection_failed, thread_not_found}; -use crate::errors::{CommandError, ModmailError, ModmailResult}; -use crate::handlers::guild_interaction_handler::InteractionHandler; -use crate::i18n::get_translated_message; -use crate::utils::command::defer_response::defer_response; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::FutureExt; use serenity::all::{ChannelId, CommandInteraction, Context, CreateCommand, ResolvedOption}; use std::sync::Arc; @@ -113,7 +108,7 @@ impl RegistrableCommand for TakeCommand { Ok(()) } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } }) } diff --git a/rustmail/src/commands/take/text_command/take.rs b/rustmail/src/commands/take/text_command/take.rs index c16d324..a58b21d 100644 --- a/rustmail/src/commands/take/text_command/take.rs +++ b/rustmail/src/commands/take/text_command/take.rs @@ -1,12 +1,9 @@ -use crate::commands::take::common::rename_channel_with_timeout; -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::db::threads::is_a_ticket_channel; -use crate::errors::ThreadError::NotAThreadChannel; -use crate::errors::common::{database_connection_failed, thread_not_found}; -use crate::errors::{CommandError, ModmailError, ModmailResult}; -use crate::handlers::guild_messages_handler::GuildMessagesHandler; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::prelude::utils::*; use serenity::all::{ChannelId, Context, Message}; use std::sync::Arc; @@ -62,6 +59,6 @@ pub async fn take( Ok(()) } else { - Err(ModmailError::Thread(NotAThreadChannel)) + Err(ModmailError::Thread(ThreadError::NotAThreadChannel)) } } diff --git a/rustmail/src/db/mod.rs b/rustmail/src/db/mod.rs index af2d8ad..7c14f81 100644 --- a/rustmail/src/db/mod.rs +++ b/rustmail/src/db/mod.rs @@ -2,3 +2,4 @@ pub mod operations; pub mod repr; pub use operations::*; +pub use repr::*; diff --git a/rustmail/src/db/operations/logs.rs b/rustmail/src/db/operations/logs.rs index 412b4c3..6bba295 100644 --- a/rustmail/src/db/operations/logs.rs +++ b/rustmail/src/db/operations/logs.rs @@ -1,5 +1,5 @@ -use crate::errors::{DatabaseError, ModmailError, ModmailResult}; -use crate::types::logs::TicketLog; +use crate::prelude::errors::*; +use crate::prelude::types::*; use sqlx::SqlitePool; pub async fn get_logs_from_user_id( diff --git a/rustmail/src/db/operations/messages.rs b/rustmail/src/db/operations/messages.rs index 64b5774..c6fe5b4 100644 --- a/rustmail/src/db/operations/messages.rs +++ b/rustmail/src/db/operations/messages.rs @@ -1,7 +1,6 @@ -use crate::config::Config; -use crate::db::operations::threads::get_user_name_from_thread_id; -use crate::errors::ModmailResult; -use crate::errors::common::message_not_found; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; use serenity::all::{Message, MessageId, UserId}; use serenity::client::Context; use sqlx::{Error, SqlitePool}; diff --git a/rustmail/src/db/operations/reminders.rs b/rustmail/src/db/operations/reminders.rs index dc3cebf..b6ee472 100644 --- a/rustmail/src/db/operations/reminders.rs +++ b/rustmail/src/db/operations/reminders.rs @@ -1,7 +1,4 @@ -use crate::errors::CommandError::ReminderAlreadyExists; -use crate::errors::{ModmailError, ModmailResult}; -use sqlx::Error::Database; -use sqlx::error::ErrorKind::UniqueViolation; +use crate::prelude::errors::*; #[derive(Debug, Clone)] pub struct Reminder { @@ -33,7 +30,7 @@ pub async fn insert_reminder(reminder: &Reminder, pool: &sqlx::SqlitePool) -> Mo .await?; if let Some(_existging_id) = existing { - return Err(ModmailError::Command(ReminderAlreadyExists)); + return Err(ModmailError::Command(CommandError::ReminderAlreadyExists)); } let result = sqlx::query!( diff --git a/rustmail/src/db/operations/scheduled.rs b/rustmail/src/db/operations/scheduled.rs index 6fa7575..057202d 100644 --- a/rustmail/src/db/operations/scheduled.rs +++ b/rustmail/src/db/operations/scheduled.rs @@ -1,4 +1,4 @@ -use crate::errors::{ModmailResult, common}; +use crate::prelude::errors::*; use sqlx::{Row, SqlitePool}; #[derive(Debug, Clone)] diff --git a/rustmail/src/db/operations/threads.rs b/rustmail/src/db/operations/threads.rs index 57fe88e..aeafdf9 100644 --- a/rustmail/src/db/operations/threads.rs +++ b/rustmail/src/db/operations/threads.rs @@ -1,6 +1,5 @@ -use crate::db::repr::Thread; -use crate::errors::CommandError::AlertDoesNotExist; -use crate::errors::{DatabaseError, ModmailError, ModmailResult}; +use crate::prelude::db::*; +use crate::prelude::errors::*; use chrono::Utc; use serenity::all::{ChannelId, GuildChannel, UserId}; use sqlx::{Error, SqlitePool}; @@ -264,7 +263,7 @@ pub async fn cancel_alert_for_staff( .await?; if existing.is_none() { - return Err(ModmailError::Command(AlertDoesNotExist)); + return Err(ModmailError::Command(CommandError::AlertDoesNotExist)); } sqlx::query!( diff --git a/rustmail/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs index 33e7ace..54e42c5 100644 --- a/rustmail/src/errors/dictionary.rs +++ b/rustmail/src/errors/dictionary.rs @@ -1,16 +1,5 @@ -use crate::errors::types::*; -use crate::i18n::language::cn::load_chinese_messages; -use crate::i18n::language::dt::load_dutch_messages; -use crate::i18n::language::en::load_english_messages; -use crate::i18n::language::fr::load_french_messages; -use crate::i18n::language::gr::load_german_messages; -use crate::i18n::language::it::load_italian_messages; -use crate::i18n::language::jp::load_japanese_messages; -use crate::i18n::language::kr::load_korean_messages; -use crate::i18n::language::pr::load_portuguese_messages; -use crate::i18n::language::ru::load_russian_messages; -use crate::i18n::language::sp::load_spanish_messages; -use crate::i18n::languages::Language; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; diff --git a/rustmail/src/errors/handler.rs b/rustmail/src/errors/handler.rs index 4f3c71b..c2d4694 100644 --- a/rustmail/src/errors/handler.rs +++ b/rustmail/src/errors/handler.rs @@ -1,7 +1,5 @@ -use crate::errors::DiscordError; -use crate::errors::dictionary::DictionaryManager; -use crate::errors::types::{ModmailError, ModmailResult}; -use crate::i18n::languages::{Language, LanguageDetector, LanguagePreferences}; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; use serenity::all::{ ChannelId, Colour, CommandInteraction, Context, CreateEmbed, CreateInteractionResponseFollowup, CreateMessage, Message, UserId, diff --git a/rustmail/src/errors/mod.rs b/rustmail/src/errors/mod.rs index d617189..d2ccd58 100644 --- a/rustmail/src/errors/mod.rs +++ b/rustmail/src/errors/mod.rs @@ -2,20 +2,16 @@ pub mod dictionary; pub mod handler; pub mod types; -pub use types::{ - CommandError, DatabaseError, DiscordError, MessageError, ModmailError, ModmailResult, - PermissionError, ThreadError, ValidationError, -}; - -pub use dictionary::{DictionaryMessage, ErrorDictionary}; - -pub use crate::{ - command_error, database_error, discord_error, message_error, permission_error, thread_error, - validation_error, -}; +pub use dictionary::*; +pub use handler::*; +pub use types::*; pub mod common { use super::*; + use crate::{ + command_error, database_error, discord_error, message_error, permission_error, + thread_error, validation_error, + }; pub fn not_found(entity: &str) -> ModmailError { database_error!(NotFound, entity) @@ -72,7 +68,7 @@ pub mod common { pub mod results { use super::*; - use crate::db::repr::Thread; + use crate::prelude::db::*; use serenity::all::{Channel, Message}; pub type DatabaseResult = Result; @@ -92,6 +88,7 @@ pub mod results { pub mod conversions { use super::*; + use crate::{database_error, discord_error}; pub fn from_serenity_with_context(err: serenity::Error, context: &str) -> ModmailError { match err { @@ -117,3 +114,7 @@ pub mod conversions { } } } + +pub use common::*; +pub use conversions::*; +pub use results::*; diff --git a/rustmail/src/errors/types.rs b/rustmail/src/errors/types.rs index 2d8e78a..2ccca1d 100644 --- a/rustmail/src/errors/types.rs +++ b/rustmail/src/errors/types.rs @@ -132,7 +132,7 @@ pub enum ValidationError { OutOfRange(String), TooShort(String), TooLong(String), - InvalidFormat(String), + InvalidValidationFormat(String), RequiredFieldMissing(String), InvalidCharacters(String), } @@ -306,7 +306,9 @@ impl fmt::Display for ValidationError { ValidationError::OutOfRange(range) => write!(f, "Out of range: {}", range), ValidationError::TooShort(field) => write!(f, "Too short: {}", field), ValidationError::TooLong(field) => write!(f, "Too long: {}", field), - ValidationError::InvalidFormat(format) => write!(f, "Invalid format: {}", format), + ValidationError::InvalidValidationFormat(format) => { + write!(f, "Invalid format: {}", format) + } ValidationError::RequiredFieldMissing(field) => { write!(f, "Required field missing: {}", field) } diff --git a/rustmail/src/features/mod.rs b/rustmail/src/features/mod.rs index 5654ade..2d03220 100644 --- a/rustmail/src/features/mod.rs +++ b/rustmail/src/features/mod.rs @@ -1,14 +1,14 @@ -use crate::config::Config; -use crate::db::operations::{get_feature_message, upsert_feature_message}; +use crate::prelude::config::*; +use crate::prelude::db::*; use async_trait::async_trait; use serenity::all::ButtonStyle; use serenity::all::{ChannelId, ComponentInteraction, Context, CreateMessage}; use serenity::builder::{CreateActionRow, CreateButton}; use std::sync::Arc; -mod poll; +pub mod poll; -pub use poll::PollFeature; +pub use poll::*; #[async_trait] pub trait Feature<'a>: Send + Sync { diff --git a/rustmail/src/features/poll.rs b/rustmail/src/features/poll.rs index 4842a12..151dd20 100644 --- a/rustmail/src/features/poll.rs +++ b/rustmail/src/features/poll.rs @@ -1,6 +1,6 @@ -use super::{Feature, make_buttons}; -use crate::config::Config; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::features::*; +use crate::prelude::utils::*; use async_trait::async_trait; use serenity::all::{ ButtonStyle, ComponentInteraction, Context, CreateInteractionResponse, CreateMessage, diff --git a/rustmail/src/handlers/guild_handler.rs b/rustmail/src/handlers/guild_handler.rs index 3d84fe3..3f735f8 100644 --- a/rustmail/src/handlers/guild_handler.rs +++ b/rustmail/src/handlers/guild_handler.rs @@ -1,11 +1,6 @@ -use crate::config::Config; -use crate::db::close_thread; -use crate::db::operations::get_thread_by_channel_id; -use crate::db::threads::is_an_opened_ticket_channel; -use crate::utils::thread::category::{ - get_category_id_from_guild_channel, get_category_name_from_guild_channel, - get_required_permissions_channel_from_guild_channel, -}; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::utils::*; use async_trait::async_trait; use serenity::all::{Context, GuildChannel, Message}; use serenity::client::EventHandler; diff --git a/rustmail/src/handlers/guild_interaction_handler.rs b/rustmail/src/handlers/guild_interaction_handler.rs index 66489fc..7f183e5 100644 --- a/rustmail/src/handlers/guild_interaction_handler.rs +++ b/rustmail/src/handlers/guild_interaction_handler.rs @@ -1,11 +1,8 @@ -use crate::commands::CommandRegistry; -use crate::config::Config; -use crate::features::handle_feature_component_interaction; -use crate::modules::commands::handle_command_component_interaction; -use crate::modules::threads::{ - handle_thread_component_interaction, handle_thread_modal_interaction, -}; -use crate::types::logs::PaginationStore; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::features::*; +use crate::prelude::modules::*; +use crate::prelude::types::*; use serenity::all::{Context, EventHandler, Interaction}; use std::sync::Arc; use tokio::sync::watch::Receiver; diff --git a/rustmail/src/handlers/guild_members_handler.rs b/rustmail/src/handlers/guild_members_handler.rs index 2c94d63..e4538dd 100644 --- a/rustmail/src/handlers/guild_members_handler.rs +++ b/rustmail/src/handlers/guild_members_handler.rs @@ -1,10 +1,8 @@ -use crate::config::Config; -use crate::db::close_thread; -use crate::db::operations::update_thread_user_left; -use crate::db::threads::get_thread_by_user_id; -use crate::features::make_buttons; -use crate::i18n::get_translated_message; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::features::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::{ButtonStyle, Channel, Member, PermissionOverwriteType, RoleId}; use serenity::{ all::{Context, EventHandler, GuildId, User}, diff --git a/rustmail/src/handlers/guild_message_reactions_handler.rs b/rustmail/src/handlers/guild_message_reactions_handler.rs index 1711d41..969d229 100644 --- a/rustmail/src/handlers/guild_message_reactions_handler.rs +++ b/rustmail/src/handlers/guild_message_reactions_handler.rs @@ -1,10 +1,6 @@ -use crate::config::Config; -use crate::db::operations::{ - get_message_ids_by_message_id, get_thread_channel_by_user_id, get_user_id_from_channel_id, -}; -use crate::errors::MessageError::{DmAccessFailed, MessageEmpty, MessageNotFound}; -use crate::errors::types::ConfigError::ParseError; -use crate::errors::{ModmailError, ModmailResult}; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; use serenity::all::{ChannelId, Context, EventHandler, MessageId, Reaction, UserId}; use serenity::async_trait; @@ -81,19 +77,20 @@ async fn handle_all_reaction_remove( channel_id: ChannelId, config: &Config, ) -> ModmailResult<()> { - let pool = config - .db_pool - .clone() - .ok_or_else(|| ModmailError::Config(ParseError("Database pool not available".into())))?; + let pool = config.db_pool.clone().ok_or_else(|| { + ModmailError::Config(ConfigError::ParseError( + "Database pool not available".into(), + )) + })?; let dm_message_id_str = match get_message_ids_by_message_id(&removed_from_message_id.to_string(), &pool).await { Some(ids) => match ids.dm_message_id { Some(id) => id, - None => return Err(ModmailError::Message(MessageEmpty)), + None => return Err(ModmailError::Message(MessageError::MessageEmpty)), }, None => { - return Err(ModmailError::Message(MessageNotFound( + return Err(ModmailError::Message(MessageError::MessageNotFound( removed_from_message_id.to_string(), ))); } @@ -101,7 +98,7 @@ async fn handle_all_reaction_remove( let dm_message_id_parsed = dm_message_id_str .parse::() - .map_err(|e| ModmailError::Message(MessageNotFound(e.to_string())))?; + .map_err(|e| ModmailError::Message(MessageError::MessageNotFound(e.to_string())))?; let user_id = match get_user_id_from_channel_id(&channel_id.to_string(), &pool).await { Some(id) if id > 0 => id as u64, @@ -111,12 +108,12 @@ async fn handle_all_reaction_remove( let dm_channel = UserId::new(user_id) .create_dm_channel(&ctx.http) .await - .map_err(|e| ModmailError::Message(DmAccessFailed(e.to_string())))?; + .map_err(|e| ModmailError::Message(MessageError::DmAccessFailed(e.to_string())))?; let dm_message = dm_channel .message(&ctx.http, dm_message_id_parsed) .await - .map_err(|e| ModmailError::Message(DmAccessFailed(e.to_string())))?; + .map_err(|e| ModmailError::Message(MessageError::DmAccessFailed(e.to_string())))?; let bot_user_id = Some(ctx.cache.current_user().id); for reaction in &dm_message.reactions { diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 8e75ae9..67faabc 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -1,11 +1,12 @@ -use crate::config::Config; -use crate::modules::create_channel; use crate::prelude::commands::*; +use crate::prelude::config::*; use crate::prelude::db::*; use crate::prelude::errors::*; use crate::prelude::i18n::*; +use crate::prelude::modules::*; use crate::prelude::types::*; use crate::prelude::utils::*; +use crate::wrap_command; use serenity::all::{GuildId, MessageId, UserId}; use serenity::{ all::{ChannelId, Context, EventHandler, Message, MessageUpdateEvent}, @@ -14,9 +15,8 @@ use serenity::{ use std::collections::HashSet; use std::sync::{LazyLock, Mutex}; use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; -use tokio::sync::watch::Receiver; use tokio::sync::Mutex as AsyncMutex; -use crate::wrap_command; +use tokio::sync::watch::Receiver; static SUPPRESSED_DELETES: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); @@ -55,7 +55,6 @@ impl GuildMessagesHandler { shutdown: Arc::new(shutdown), pagination, }; - extract_reply_content() let mut lock = h.commands.lock().await; @@ -96,12 +95,12 @@ async fn manage_incoming_message( let pool = config .db_pool .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; let error_handler = config .error_handler .as_ref() - .ok_or_else(common::database_connection_failed)?; + .ok_or_else(database_connection_failed)?; if let Some(guild_id) = msg.guild_id { let community_guild_id = config.bot.get_community_guild_id(); @@ -122,7 +121,7 @@ async fn manage_incoming_message( ) .await; - let error = common::validation_failed(&error_msg); + let error = validation_failed(&error_msg); let _ = error_handler .reply_to_msg_with_error(ctx, msg, &error) .await; @@ -138,12 +137,12 @@ async fn manage_incoming_message( if let Some(channel_id_str) = get_thread_channel_by_user_id(msg.author.id, pool).await { let channel_id_num = channel_id_str .parse::() - .map_err(|_| common::validation_failed("Invalid channel ID format"))?; + .map_err(|_| validation_failed("Invalid channel ID format"))?; let channel_id = ChannelId::new(channel_id_num); if let Err(e) = send_to_thread(ctx, channel_id, msg, config, false).await { - let error = common::validation_failed(&format!("Failed to forward message: {}", e)); + let error = validation_failed(&format!("Failed to forward message: {}", e)); let _ = error_handler .reply_to_msg_with_error(ctx, msg, &error) .await; @@ -276,7 +275,7 @@ impl EventHandler for GuildMessagesHandler { let _ = update_message_numbers_after_deletion(&thread.channel_id, num, pool).await; } - let _ = db_delete_message(&deleted_message_id.to_string(), pool).await; + let _ = delete_message(&deleted_message_id.to_string(), pool).await; } async fn message_delete_bulk( diff --git a/rustmail/src/handlers/guild_moderation_handler.rs b/rustmail/src/handlers/guild_moderation_handler.rs index f6b147c..9bc73cc 100644 --- a/rustmail/src/handlers/guild_moderation_handler.rs +++ b/rustmail/src/handlers/guild_moderation_handler.rs @@ -1,8 +1,8 @@ -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::features::make_buttons; -use crate::i18n::get_translated_message; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::features::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::audit_log::Action; use serenity::all::{ AutoModAction, ButtonStyle, ChannelAction, ChannelOverwriteAction, CreatorMonetizationAction, diff --git a/rustmail/src/handlers/mod.rs b/rustmail/src/handlers/mod.rs index 40be298..dcddde7 100644 --- a/rustmail/src/handlers/mod.rs +++ b/rustmail/src/handlers/mod.rs @@ -6,3 +6,12 @@ pub mod guild_messages_handler; pub mod guild_moderation_handler; pub mod ready_handler; pub mod typing_proxy_handler; + +pub use guild_handler::*; +pub use guild_interaction_handler::*; +pub use guild_members_handler::*; +pub use guild_message_reactions_handler::*; +pub use guild_messages_handler::*; +pub use guild_moderation_handler::*; +pub use ready_handler::*; +pub use typing_proxy_handler::*; diff --git a/rustmail/src/handlers/ready_handler.rs b/rustmail/src/handlers/ready_handler.rs index d0f504c..796abd5 100644 --- a/rustmail/src/handlers/ready_handler.rs +++ b/rustmail/src/handlers/ready_handler.rs @@ -1,9 +1,7 @@ -use crate::commands::{CommandRegistry, CommunityRegistrable}; -use crate::config::Config; -use crate::features::sync_features; -use crate::modules::message_recovery::{recover_missing_messages, send_recovery_summary}; -use crate::modules::reminders::load_reminders; -use crate::modules::scheduled_closures::hydrate_scheduled_closures; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::features::*; +use crate::prelude::modules::*; use serenity::all::{ActivityData, CreateCommand, GuildId}; use serenity::{ all::{Context, EventHandler, Ready}, diff --git a/rustmail/src/handlers/typing_proxy_handler.rs b/rustmail/src/handlers/typing_proxy_handler.rs index 3070914..8ae3ebc 100644 --- a/rustmail/src/handlers/typing_proxy_handler.rs +++ b/rustmail/src/handlers/typing_proxy_handler.rs @@ -1,5 +1,5 @@ -use crate::config::Config; -use crate::db::{get_thread_channel_by_user_id, get_user_id_from_channel_id}; +use crate::prelude::config::*; +use crate::prelude::db::*; use serenity::all::{ChannelId, Context, EventHandler, TypingStartEvent, UserId}; use serenity::async_trait; use sqlx::SqlitePool; diff --git a/rustmail/src/i18n/language/cn.rs b/rustmail/src/i18n/language/cn.rs index a7a577d..124dd4d 100644 --- a/rustmail/src/i18n/language/cn.rs +++ b/rustmail/src/i18n/language/cn.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_chinese_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/dt.rs b/rustmail/src/i18n/language/dt.rs index fa84d3a..ec1f7d5 100644 --- a/rustmail/src/i18n/language/dt.rs +++ b/rustmail/src/i18n/language/dt.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_dutch_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 57af912..20a603f 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -1,5 +1,4 @@ -use crate::errors::dictionary::DictionaryMessage; -use crate::errors::dictionary::ErrorDictionary; +use crate::prelude::errors::*; pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 250bae6..68f3597 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -1,5 +1,4 @@ -use crate::errors::dictionary::DictionaryMessage; -use crate::errors::dictionary::ErrorDictionary; +use crate::prelude::errors::*; pub fn load_french_messages(dict: &mut ErrorDictionary) { dict.messages.insert("database.connection_failed".to_string(), diff --git a/rustmail/src/i18n/language/gr.rs b/rustmail/src/i18n/language/gr.rs index 0b02099..0778e65 100644 --- a/rustmail/src/i18n/language/gr.rs +++ b/rustmail/src/i18n/language/gr.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_german_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/it.rs b/rustmail/src/i18n/language/it.rs index 5aaa72c..904448b 100644 --- a/rustmail/src/i18n/language/it.rs +++ b/rustmail/src/i18n/language/it.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_italian_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/jp.rs b/rustmail/src/i18n/language/jp.rs index c5c8961..6622c5f 100644 --- a/rustmail/src/i18n/language/jp.rs +++ b/rustmail/src/i18n/language/jp.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_japanese_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/kr.rs b/rustmail/src/i18n/language/kr.rs index 8627c24..b0f3a3c 100644 --- a/rustmail/src/i18n/language/kr.rs +++ b/rustmail/src/i18n/language/kr.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_korean_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/mod.rs b/rustmail/src/i18n/language/mod.rs index 2c57994..0ba63da 100644 --- a/rustmail/src/i18n/language/mod.rs +++ b/rustmail/src/i18n/language/mod.rs @@ -9,3 +9,15 @@ pub mod kr; pub mod pr; pub mod ru; pub mod sp; + +pub use cn::*; +pub use dt::*; +pub use en::*; +pub use fr::*; +pub use gr::*; +pub use it::*; +pub use jp::*; +pub use kr::*; +pub use pr::*; +pub use ru::*; +pub use sp::*; diff --git a/rustmail/src/i18n/language/pr.rs b/rustmail/src/i18n/language/pr.rs index d8c5f92..ba0fee2 100644 --- a/rustmail/src/i18n/language/pr.rs +++ b/rustmail/src/i18n/language/pr.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_portuguese_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/ru.rs b/rustmail/src/i18n/language/ru.rs index bb1b19c..271827f 100644 --- a/rustmail/src/i18n/language/ru.rs +++ b/rustmail/src/i18n/language/ru.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_russian_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/language/sp.rs b/rustmail/src/i18n/language/sp.rs index 99d7f9b..0add181 100644 --- a/rustmail/src/i18n/language/sp.rs +++ b/rustmail/src/i18n/language/sp.rs @@ -1,4 +1,4 @@ -use crate::errors::{DictionaryMessage, ErrorDictionary}; +use crate::prelude::errors::*; pub fn load_spanish_messages(dict: &mut ErrorDictionary) { dict.messages.insert( diff --git a/rustmail/src/i18n/mod.rs b/rustmail/src/i18n/mod.rs index 08e52f4..1dfb3fd 100644 --- a/rustmail/src/i18n/mod.rs +++ b/rustmail/src/i18n/mod.rs @@ -1,4 +1,7 @@ pub mod language; pub mod languages; pub mod utils; -pub use utils::get_translated_message; + +pub use language::*; +pub use languages::*; +pub use utils::*; diff --git a/rustmail/src/i18n/utils.rs b/rustmail/src/i18n/utils.rs index 02ca225..0937800 100644 --- a/rustmail/src/i18n/utils.rs +++ b/rustmail/src/i18n/utils.rs @@ -1,4 +1,4 @@ -use crate::config::Config; +use crate::prelude::config::*; use serenity::all::UserId; use std::collections::HashMap; diff --git a/rustmail/src/main.rs b/rustmail/src/main.rs index 7322241..43f158c 100644 --- a/rustmail/src/main.rs +++ b/rustmail/src/main.rs @@ -1,5 +1,5 @@ -use crate::api::router::create_api_router; use crate::bot::{init_bot_state, start_bot_if_config_valid}; +use crate::prelude::api::*; use axum::extract::Path; use axum::response::Response; use rust_embed::RustEmbed; diff --git a/rustmail/src/modules/commands.rs b/rustmail/src/modules/commands.rs index 216d4fc..0a0b0c5 100644 --- a/rustmail/src/modules/commands.rs +++ b/rustmail/src/modules/commands.rs @@ -1,9 +1,9 @@ -use crate::commands::logs::common::render_logs_page; -use crate::config::Config; -use crate::features::make_buttons; -use crate::i18n::get_translated_message; -use crate::types::logs::PaginationStore; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::commands::*; +use crate::prelude::config::*; +use crate::prelude::features::*; +use crate::prelude::i18n::*; +use crate::prelude::types::*; +use crate::prelude::utils::*; use serenity::all::{ButtonStyle, ComponentInteraction, Context}; use serenity::builder::CreateInteractionResponse; diff --git a/rustmail/src/modules/message_recovery.rs b/rustmail/src/modules/message_recovery.rs index c391468..f58af80 100644 --- a/rustmail/src/modules/message_recovery.rs +++ b/rustmail/src/modules/message_recovery.rs @@ -1,11 +1,7 @@ -use crate::config::Config; -use crate::db::operations::{ - get_all_opened_threads, get_last_recovery_timestamp, get_latest_thread_message, - update_last_recovery_timestamp, -}; -use crate::db::repr::Thread; -use crate::i18n::get_translated_message; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use chrono::{DateTime, Utc}; use serenity::all::{ChannelId, Context, GetMessages, Message, MessageId, UserId}; use std::collections::HashMap; diff --git a/rustmail/src/modules/mod.rs b/rustmail/src/modules/mod.rs index a89d31d..ad296f4 100644 --- a/rustmail/src/modules/mod.rs +++ b/rustmail/src/modules/mod.rs @@ -4,4 +4,8 @@ pub mod reminders; pub mod scheduled_closures; pub mod threads; +pub use commands::*; +pub use message_recovery::*; +pub use reminders::*; +pub use scheduled_closures::*; pub use threads::*; diff --git a/rustmail/src/modules/scheduled_closures.rs b/rustmail/src/modules/scheduled_closures.rs index 8b11902..0770726 100644 --- a/rustmail/src/modules/scheduled_closures.rs +++ b/rustmail/src/modules/scheduled_closures.rs @@ -3,7 +3,7 @@ use crate::prelude::db::*; use crate::prelude::utils::*; use chrono::Utc; use serenity::all::{ChannelId, Context, UserId}; -use tokio::time::{sleep, Duration}; +use tokio::time::{Duration, sleep}; fn schedule_one(ctx: &Context, config: &Config, thread_id: String, close_at: i64) { let now = Utc::now().timestamp(); diff --git a/rustmail/src/panel_commands/mod.rs b/rustmail/src/panel_commands/mod.rs index 22d12a3..3232c86 100644 --- a/rustmail/src/panel_commands/mod.rs +++ b/rustmail/src/panel_commands/mod.rs @@ -1 +1,3 @@ pub mod user; + +pub use user::*; diff --git a/rustmail/src/panel_commands/user/mod.rs b/rustmail/src/panel_commands/user/mod.rs index 6292c6e..340693a 100644 --- a/rustmail/src/panel_commands/user/mod.rs +++ b/rustmail/src/panel_commands/user/mod.rs @@ -1 +1,3 @@ pub mod is_member; + +pub use is_member::*; diff --git a/rustmail/src/prelude/api.rs b/rustmail/src/prelude/api.rs new file mode 100644 index 0000000..f511548 --- /dev/null +++ b/rustmail/src/prelude/api.rs @@ -0,0 +1 @@ +pub use crate::api::*; diff --git a/rustmail/src/prelude/config.rs b/rustmail/src/prelude/config.rs index bf55a47..2eceedc 100644 --- a/rustmail/src/prelude/config.rs +++ b/rustmail/src/prelude/config.rs @@ -1,2 +1 @@ -pub use crate::config::load_config; -pub use crate::config::Config; +pub use crate::config::*; diff --git a/rustmail/src/prelude/db.rs b/rustmail/src/prelude/db.rs index d4174e1..a67eb0a 100644 --- a/rustmail/src/prelude/db.rs +++ b/rustmail/src/prelude/db.rs @@ -1,6 +1 @@ -pub use crate::db::logs::*; -pub use crate::db::messages::*; -pub use crate::db::operations::*; -pub use crate::db::reminders::*; -pub use crate::db::threads::*; pub use crate::db::*; diff --git a/rustmail/src/prelude/errors.rs b/rustmail/src/prelude/errors.rs index 31695e7..543733e 100644 --- a/rustmail/src/prelude/errors.rs +++ b/rustmail/src/prelude/errors.rs @@ -1,4 +1,10 @@ -pub use crate::errors::common::*; -pub use crate::errors::handler::*; -pub use crate::errors::types::*; pub use crate::errors::*; + +pub use crate::errors::types::CommandError; +pub use crate::errors::types::ConfigError; +pub use crate::errors::types::DatabaseError; +pub use crate::errors::types::DiscordError; +pub use crate::errors::types::MessageError; +pub use crate::errors::types::ModmailError; +pub use crate::errors::types::ThreadError; +pub use crate::errors::types::ValidationError; diff --git a/rustmail/src/prelude/features.rs b/rustmail/src/prelude/features.rs index e69de29..9768c6a 100644 --- a/rustmail/src/prelude/features.rs +++ b/rustmail/src/prelude/features.rs @@ -0,0 +1 @@ +pub use crate::features::*; diff --git a/rustmail/src/prelude/handlers.rs b/rustmail/src/prelude/handlers.rs index b362c54..a0c7c04 100644 --- a/rustmail/src/prelude/handlers.rs +++ b/rustmail/src/prelude/handlers.rs @@ -1,8 +1 @@ -pub use crate::handlers::guild_handler::GuildHandler; -pub use crate::handlers::guild_interaction_handler::InteractionHandler; -pub use crate::handlers::guild_members_handler::GuildMembersHandler; -pub use crate::handlers::guild_message_reactions_handler::GuildMessageReactionsHandler; -pub use crate::handlers::guild_messages_handler::GuildMessagesHandler; -pub use crate::handlers::guild_moderation_handler::GuildModerationHandler; -pub use crate::handlers::ready_handler::ReadyHandler; -pub use crate::handlers::typing_proxy_handler::TypingProxyHandler; +pub use crate::handlers::*; diff --git a/rustmail/src/prelude/i18n.rs b/rustmail/src/prelude/i18n.rs index ad16993..b78d79d 100644 --- a/rustmail/src/prelude/i18n.rs +++ b/rustmail/src/prelude/i18n.rs @@ -1,2 +1 @@ -pub use crate::i18n::get_translated_message; -pub use crate::i18n::languages::*; +pub use crate::i18n::*; diff --git a/rustmail/src/prelude/mod.rs b/rustmail/src/prelude/mod.rs index b39feb5..b46818a 100644 --- a/rustmail/src/prelude/mod.rs +++ b/rustmail/src/prelude/mod.rs @@ -1,3 +1,4 @@ +pub mod api; pub mod commands; pub mod config; pub mod db; diff --git a/rustmail/src/prelude/modules.rs b/rustmail/src/prelude/modules.rs index 8b13789..f98fe38 100644 --- a/rustmail/src/prelude/modules.rs +++ b/rustmail/src/prelude/modules.rs @@ -1 +1 @@ - +pub use crate::modules::*; diff --git a/rustmail/src/prelude/panel_commands.rs b/rustmail/src/prelude/panel_commands.rs index fe5c21d..e3c8b3a 100644 --- a/rustmail/src/prelude/panel_commands.rs +++ b/rustmail/src/prelude/panel_commands.rs @@ -1 +1 @@ -pub use crate::panel_commands::user::is_member::is_member; +pub use crate::panel_commands::*; diff --git a/rustmail/src/types/bot.rs b/rustmail/src/types/bot.rs index b15c16c..5002830 100644 --- a/rustmail/src/types/bot.rs +++ b/rustmail/src/types/bot.rs @@ -1,4 +1,4 @@ -use crate::config::Config; +use crate::prelude::config::*; use serenity::all::Http; use std::sync::Arc; use tokio::sync::watch::Sender; diff --git a/rustmail/src/utils/command/defer_response.rs b/rustmail/src/utils/command/defer_response.rs index 39da1b5..26da84b 100644 --- a/rustmail/src/utils/command/defer_response.rs +++ b/rustmail/src/utils/command/defer_response.rs @@ -1,4 +1,4 @@ -use crate::errors::ModmailResult; +use crate::prelude::errors::*; use serenity::all::{ CommandInteraction, Context, CreateInteractionResponse, CreateInteractionResponseMessage, }; diff --git a/rustmail/src/utils/command/mod.rs b/rustmail/src/utils/command/mod.rs index 084a867..34e0f12 100644 --- a/rustmail/src/utils/command/mod.rs +++ b/rustmail/src/utils/command/mod.rs @@ -3,4 +3,7 @@ pub mod defer_response; pub mod extract_reply_content; pub mod wrap_command; +pub use category::*; +pub use defer_response::*; pub use extract_reply_content::*; +pub use wrap_command::*; diff --git a/rustmail/src/utils/command/wrap_command.rs b/rustmail/src/utils/command/wrap_command.rs index 7307df7..da6714d 100644 --- a/rustmail/src/utils/command/wrap_command.rs +++ b/rustmail/src/utils/command/wrap_command.rs @@ -5,7 +5,7 @@ macro_rules! wrap_command { dyn for<'a> Fn( serenity::prelude::Context, serenity::model::prelude::Message, - &'a $crate::config::Config, + &'a $crate::prelude::config::Config, Arc, ) -> std::pin::Pin< Box< diff --git a/rustmail/src/utils/conversion/mod.rs b/rustmail/src/utils/conversion/mod.rs index c77e677..c3607e4 100644 --- a/rustmail/src/utils/conversion/mod.rs +++ b/rustmail/src/utils/conversion/mod.rs @@ -1,2 +1,3 @@ pub mod hex_string_to_int; +pub use hex_string_to_int::*; diff --git a/rustmail/src/utils/message/message_builder.rs b/rustmail/src/utils/message/message_builder.rs index 83c0d5e..201d87c 100644 --- a/rustmail/src/utils/message/message_builder.rs +++ b/rustmail/src/utils/message/message_builder.rs @@ -1,10 +1,8 @@ -use crate::config::Config; -use crate::db::get_thread_by_channel_id; -use crate::db::operations::{insert_staff_message, insert_user_message_with_ids}; -use crate::db::threads::get_thread_by_user_id; -use crate::errors::{DatabaseError, ModmailError}; -use crate::i18n::get_translated_message; -use crate::utils::conversion::hex_string_to_int::hex_string_to_int; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::{ ChannelId, Colour, CommandInteraction, Context, CreateAttachment, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateInteractionResponseFollowup, CreateMessage, diff --git a/rustmail/src/utils/message/mod.rs b/rustmail/src/utils/message/mod.rs index 8b0e900..ab0d3a0 100644 --- a/rustmail/src/utils/message/mod.rs +++ b/rustmail/src/utils/message/mod.rs @@ -4,8 +4,6 @@ pub mod reply_intent; pub mod category; pub mod ui_components; -pub use message_builder::*; - pub mod ui { use super::{ui_components::ButtonsBuilder, ui_components::ModalBuilder}; pub fn modal(id: impl Into, title: impl Into) -> ModalBuilder { @@ -15,3 +13,9 @@ pub mod ui { ButtonsBuilder::new() } } + +pub use category::*; +pub use message_builder::*; +pub use reply_intent::*; +pub use ui::*; +pub use ui_components::*; diff --git a/rustmail/src/utils/mod.rs b/rustmail/src/utils/mod.rs index 046ef8f..2c41d90 100644 --- a/rustmail/src/utils/mod.rs +++ b/rustmail/src/utils/mod.rs @@ -5,6 +5,7 @@ pub mod thread; pub mod time; pub use command::*; +pub use conversion::*; pub use message::*; pub use thread::*; pub use time::*; diff --git a/rustmail/src/utils/thread/fetch_thread.rs b/rustmail/src/utils/thread/fetch_thread.rs index 2b78e49..122ea8c 100644 --- a/rustmail/src/utils/thread/fetch_thread.rs +++ b/rustmail/src/utils/thread/fetch_thread.rs @@ -1,11 +1,9 @@ -use crate::{ - db::{get_thread_by_channel_id, repr::Thread}, - errors::{ModmailResult, common}, -}; +use crate::prelude::db::*; +use crate::prelude::errors::*; use sqlx::SqlitePool; pub async fn fetch_thread(db_pool: &SqlitePool, channel_id: &str) -> ModmailResult { get_thread_by_channel_id(channel_id, db_pool) .await - .ok_or_else(common::thread_not_found) + .ok_or_else(thread_not_found) } diff --git a/rustmail/src/utils/thread/get_thread_lock.rs b/rustmail/src/utils/thread/get_thread_lock.rs index 7830692..737bdf5 100644 --- a/rustmail/src/utils/thread/get_thread_lock.rs +++ b/rustmail/src/utils/thread/get_thread_lock.rs @@ -1,4 +1,4 @@ -use crate::config::Config; +use crate::prelude::config::*; use std::sync::Arc; pub fn get_thread_lock(config: &Config, key: u64) -> Arc> { diff --git a/rustmail/src/utils/thread/mod.rs b/rustmail/src/utils/thread/mod.rs index 16e431a..487e543 100644 --- a/rustmail/src/utils/thread/mod.rs +++ b/rustmail/src/utils/thread/mod.rs @@ -4,6 +4,8 @@ pub mod get_thread_lock; pub mod send_to_thread; pub mod user_recap; +pub use category::*; +pub use fetch_thread::*; pub use get_thread_lock::*; pub use send_to_thread::*; pub use user_recap::*; diff --git a/rustmail/src/utils/thread/send_to_thread.rs b/rustmail/src/utils/thread/send_to_thread.rs index fbee0d9..7c31fc5 100644 --- a/rustmail/src/utils/thread/send_to_thread.rs +++ b/rustmail/src/utils/thread/send_to_thread.rs @@ -1,10 +1,8 @@ -use crate::config::Config; -use crate::db::operations::{ - get_staff_alerts_for_user, get_thread_id_by_user_id, is_user_left, mark_alert_as_used, -}; -use crate::errors::{DatabaseError, ModmailError, ModmailResult, ThreadError}; -use crate::i18n::get_translated_message; -use crate::utils::message::message_builder::MessageBuilder; +use crate::prelude::config::*; +use crate::prelude::db::*; +use crate::prelude::errors::*; +use crate::prelude::i18n::*; +use crate::prelude::utils::*; use serenity::all::{ChannelId, Context, CreateAttachment, GuildId, Message, UserId}; use std::collections::HashMap; diff --git a/rustmail/src/utils/thread/user_recap.rs b/rustmail/src/utils/thread/user_recap.rs index d2a6dbe..f66c93b 100644 --- a/rustmail/src/utils/thread/user_recap.rs +++ b/rustmail/src/utils/thread/user_recap.rs @@ -1,4 +1,4 @@ -use crate::utils::time::format_duration_since::format_duration_since; +use crate::prelude::utils::*; use serenity::all::UserId; pub fn get_user_recap( diff --git a/rustmail/src/utils/time/get_member_join_date.rs b/rustmail/src/utils/time/get_member_join_date.rs index d23be15..08f2e8f 100644 --- a/rustmail/src/utils/time/get_member_join_date.rs +++ b/rustmail/src/utils/time/get_member_join_date.rs @@ -1,4 +1,4 @@ -use crate::utils::time::format_duration_since::format_duration_since; +use crate::prelude::utils::*; use serenity::all::{Context, GuildId, Message, UserId}; pub async fn get_member_join_date( diff --git a/rustmail/src/utils/time/mod.rs b/rustmail/src/utils/time/mod.rs index 0c43e24..c6c58f6 100644 --- a/rustmail/src/utils/time/mod.rs +++ b/rustmail/src/utils/time/mod.rs @@ -2,3 +2,7 @@ pub mod format_duration_since; pub mod get_member_join_date; pub use get_member_join_date::*; + +pub use format_duration_since::*; +pub use format_duration_since::*; +pub use get_member_join_date::*; From ce9cf89351322644bf317f4651b1c7ddf1421347 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 30 Oct 2025 08:33:53 +0100 Subject: [PATCH 085/100] refactor(prisma): remove prisma from project source tree --- package-lock.json | 430 ---------------------------------------------- package.json | 8 - 2 files changed, 438 deletions(-) delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index c664cd5..0000000 --- a/package-lock.json +++ /dev/null @@ -1,430 +0,0 @@ -{ - "name": "rustmail", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@prisma/client": "^6.18.0" - }, - "devDependencies": { - "prisma": "^6.18.0" - } - }, - "node_modules/@prisma/client": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", - "integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@prisma/config": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz", - "integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "c12": "3.1.0", - "deepmerge-ts": "7.1.5", - "effect": "3.18.4", - "empathic": "2.0.0" - } - }, - "node_modules/@prisma/debug": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", - "integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", - "integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.18.0", - "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "@prisma/fetch-engine": "6.18.0", - "@prisma/get-platform": "6.18.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz", - "integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz", - "integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.18.0", - "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "@prisma/get-platform": "6.18.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz", - "integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.18.0" - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/c12": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.3", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^16.6.1", - "exsolve": "^1.0.7", - "giget": "^2.0.0", - "jiti": "^2.4.2", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.2.0", - "rc9": "^2.1.2" - }, - "peerDependencies": { - "magicast": "^0.3.5" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/effect": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "fast-check": "^3.23.1" - } - }, - "node_modules/empathic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/giget": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", - "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, - "node_modules/prisma": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz", - "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", - "devOptional": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/config": "6.18.0", - "@prisma/engines": "6.18.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" - } - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "devOptional": true, - "license": "MIT" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index edc4055..0000000 --- a/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "devDependencies": { - "prisma": "^6.18.0" - }, - "dependencies": { - "@prisma/client": "^6.18.0" - } -} From 0dce6e8eed764ea00ee48bf57cb9d5a6dc83b59e Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 30 Oct 2025 15:00:11 +0100 Subject: [PATCH 086/100] feat(status): add bases for ticket status updates --- Cargo.lock | 356 +++++++++++++++++- Cargo.toml | 2 +- migrations/20251030075606_thread_status.sql | 11 + rustmail/Cargo.toml | 1 + .../anonreply/text_command/anonreply.rs | 13 + .../src/commands/reply/slash_command/reply.rs | 13 + .../src/commands/reply/text_command/reply.rs | 13 + rustmail/src/db/operations/threads.rs | 139 ++++++- rustmail/src/handlers/ready_handler.rs | 51 ++- rustmail/src/modules/mod.rs | 2 + rustmail/src/modules/threads.rs | 6 +- rustmail/src/modules/threads_status.rs | 60 +++ rustmail/src/types/mod.rs | 2 + rustmail/src/types/threads_status.rs | 23 ++ rustmail/src/utils/thread/send_to_thread.rs | 13 + 15 files changed, 682 insertions(+), 23 deletions(-) create mode 100644 migrations/20251030075606_thread_status.sql create mode 100644 rustmail/src/modules/threads_status.rs create mode 100644 rustmail/src/types/threads_status.rs diff --git a/Cargo.lock b/Cargo.lock index 8a0119a..1ed8f13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "anymap2" version = "0.13.0" @@ -60,6 +66,28 @@ dependencies = [ "serde", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -92,13 +120,40 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.2", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ - "axum-core", + "axum-core 0.5.5", "bytes", "form_urlencoded", "futures-util", @@ -108,7 +163,7 @@ dependencies = [ "hyper 1.7.0", "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", @@ -119,12 +174,32 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-core" version = "0.5.5" @@ -150,8 +225,8 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ - "axum", - "axum-core", + "axum 0.8.6", + "axum-core 0.5.5", "bytes", "cookie", "futures-util", @@ -338,6 +413,45 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console-api" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "hyper-util", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1305,7 +1419,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -1324,13 +1438,19 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -1357,6 +1477,19 @@ dependencies = [ "hashbrown 0.15.3", ] +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "heck" version = "0.5.0" @@ -1490,6 +1623,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "hyper" version = "0.14.32" @@ -1567,6 +1706,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.7.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1758,7 +1910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" dependencies = [ "implicit-clone-derive", - "indexmap", + "indexmap 2.11.4", ] [[package]] @@ -1771,6 +1923,16 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + [[package]] name = "indexmap" version = "2.11.4" @@ -1797,6 +1959,15 @@ dependencies = [ "serde", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1913,6 +2084,21 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matchit" version = "0.8.4" @@ -1966,6 +2152,12 @@ dependencies = [ "triomphe", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.8" @@ -2015,6 +2207,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2412,6 +2614,38 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -2600,7 +2834,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-native-tls", - "tower", + "tower 0.5.2", "tower-http", "tower-service", "url", @@ -2790,11 +3024,12 @@ name = "rustmail" version = "0.1.1" dependencies = [ "async-trait", - "axum", + "axum 0.8.6", "axum-extra", "base64 0.22.1", "chrono", "chrono-tz", + "console-subscriber", "hyper 1.7.0", "mime_guess", "rand", @@ -3087,6 +3322,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3221,7 +3465,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.3", "hashlink", - "indexmap", + "indexmap 2.11.4", "log", "memchr", "once_cell", @@ -3614,6 +3858,15 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -3683,6 +3936,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.0", "tokio-macros", + "tracing", "windows-sys 0.61.1", ] @@ -3784,7 +4038,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap", + "indexmap 2.11.4", "serde_core", "serde_spanned", "toml_datetime 0.7.3", @@ -3814,7 +4068,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.11.4", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -3834,6 +4088,56 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum 0.7.9", + "base64 0.22.1", + "bytes", + "h2 0.4.10", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.7.0", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.9", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -3863,7 +4167,7 @@ dependencies = [ "http-body 1.0.1", "iri-string", "pin-project-lite", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", ] @@ -3910,6 +4214,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "matchers", + "once_cell", + "regex-automata", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", ] [[package]] @@ -4091,6 +4411,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4578,7 +4904,7 @@ dependencies = [ "gloo 0.10.0", "html-escape", "implicit-clone", - "indexmap", + "indexmap 2.11.4", "js-sys", "prokio", "rustversion", diff --git a/Cargo.toml b/Cargo.toml index d03bbdc..1549bee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] members = ["rustmail", "rustmail_panel"] -resolver = "3" +resolver = "3" \ No newline at end of file diff --git a/migrations/20251030075606_thread_status.sql b/migrations/20251030075606_thread_status.sql new file mode 100644 index 0000000..4e7c09a --- /dev/null +++ b/migrations/20251030075606_thread_status.sql @@ -0,0 +1,11 @@ +-- Add migration script here +CREATE TABLE IF NOT EXISTS "thread_status" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "thread_id" TEXT NOT NULL UNIQUE, + "channel_id" INTEGER NOT NULL UNIQUE, + "owner_id" TEXT NOT NULL, + "taken_by" TEXT DEFAULT NULL, + "last_message_by" TEXT NOT NULL, + "last_message_at" INTEGER NOT NULL, + CONSTRAINT "thread_status_thread_id_fkey" FOREIGN KEY ("thread_id") REFERENCES "threads" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); \ No newline at end of file diff --git a/rustmail/Cargo.toml b/rustmail/Cargo.toml index de680c0..15b85a1 100644 --- a/rustmail/Cargo.toml +++ b/rustmail/Cargo.toml @@ -25,6 +25,7 @@ rand = "0.8.5" base64 = "0.22.1" subtle = "2.6.1" chrono-tz = "0.10.4" +console-subscriber = "0.4.1" [dependencies.uuid] version = "1.18.1" diff --git a/rustmail/src/commands/anonreply/text_command/anonreply.rs b/rustmail/src/commands/anonreply/text_command/anonreply.rs index c2c604f..c60475c 100644 --- a/rustmail/src/commands/anonreply/text_command/anonreply.rs +++ b/rustmail/src/commands/anonreply/text_command/anonreply.rs @@ -3,6 +3,8 @@ use crate::prelude::db::*; use crate::prelude::errors::*; use crate::prelude::handlers::*; use crate::prelude::utils::*; +use crate::types::TicketAuthor; +use chrono::Utc; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; @@ -66,6 +68,17 @@ pub async fn anonreply( .await .map_err(|_| validation_failed("Failed to allocate message number"))?; + let mut ticket_status = match get_thread_status(&thread.id, db_pool).await { + Some(status) => status, + None => { + return Err(validation_failed("Failed to get thread status")); + } + }; + + ticket_status.last_message_by = TicketAuthor::Staff; + ticket_status.last_message_at = Utc::now().timestamp(); + update_thread_status(&thread.id, &ticket_status, db_pool).await?; + let _ = msg.delete(&ctx.http).await; let mut sr = MessageBuilder::begin_staff_reply( diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index 237a2ae..ac35524 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -4,7 +4,9 @@ use crate::prelude::db::*; use crate::prelude::errors::*; use crate::prelude::handlers::*; use crate::prelude::i18n::*; +use crate::prelude::types::*; use crate::prelude::utils::*; +use chrono::Utc; use serenity::FutureExt; use serenity::all::{ Attachment, CommandDataOptionValue, CommandInteraction, CommandOptionType, Context, @@ -155,6 +157,17 @@ impl RegistrableCommand for ReplyCommand { .await .map_err(|_| validation_failed("Failed to allocate message number"))?; + let mut ticket_status = match get_thread_status(&thread.id, db_pool).await { + Some(status) => status, + None => { + return Err(validation_failed("Failed to get thread status")); + } + }; + + ticket_status.last_message_by = TicketAuthor::Staff; + ticket_status.last_message_at = Utc::now().timestamp(); + update_thread_status(&thread.id, &ticket_status, db_pool).await?; + let mut sr = MessageBuilder::begin_staff_reply( &ctx, &config, diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index efdf3d9..091cc38 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -2,7 +2,9 @@ use crate::prelude::config::*; use crate::prelude::db::*; use crate::prelude::errors::*; use crate::prelude::handlers::*; +use crate::prelude::types::*; use crate::prelude::utils::*; +use chrono::Utc; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; @@ -38,6 +40,17 @@ pub async fn reply( .await .map_err(|_| validation_failed("Failed to allocate message number"))?; + let mut ticket_status = match get_thread_status(&thread.id, db_pool).await { + Some(status) => status, + None => { + return Err(validation_failed("Failed to get thread status")); + } + }; + + ticket_status.last_message_by = TicketAuthor::Staff; + ticket_status.last_message_at = Utc::now().timestamp(); + update_thread_status(&thread.id, &ticket_status, db_pool).await?; + let _ = msg.delete(&ctx.http).await; let mut sr = MessageBuilder::begin_staff_reply( diff --git a/rustmail/src/db/operations/threads.rs b/rustmail/src/db/operations/threads.rs index aeafdf9..848d05d 100644 --- a/rustmail/src/db/operations/threads.rs +++ b/rustmail/src/db/operations/threads.rs @@ -1,5 +1,6 @@ use crate::prelude::db::*; use crate::prelude::errors::*; +use crate::prelude::types::*; use chrono::Utc; use serenity::all::{ChannelId, GuildChannel, UserId}; use sqlx::{Error, SqlitePool}; @@ -116,17 +117,17 @@ pub async fn create_thread_for_user( let channel_id = channel.id.to_string(); let thread_id = Uuid::new_v4().to_string(); - match sqlx::query!( + let res = match sqlx::query!( "INSERT INTO threads (id, user_id, user_name, channel_id) VALUES (?, ?, ?, ?)", thread_id, user_id, user_name, channel_id ) - .execute(pool) + .execute(&pool.clone()) .await { - Ok(_) => Ok(thread_id), + Ok(_) => Ok(thread_id.clone()), Err(Error::Database(db_err)) if db_err.code() == Some(std::borrow::Cow::Borrowed("2067")) => { @@ -142,7 +143,43 @@ pub async fn create_thread_for_user( } } Err(e) => Err(e), - } + }; + + let channel_id = channel.id.get() as i64; + let user_id_str = user_id.to_string(); + let timestamp = Utc::now().timestamp(); + + let _ = match sqlx::query!( + "INSERT INTO thread_status (thread_id, channel_id, owner_id, taken_by, last_message_by, last_message_at) VALUES (?, ?, ?, ?, ?, ?)", + thread_id, + channel_id, + user_id_str, + None::, + "user", + timestamp + ) + .execute(&pool.clone()) + .await + { + Ok(_) => Ok(thread_id), + Err(Error::Database(db_err)) + if db_err.code() == Some(std::borrow::Cow::Borrowed("2067")) => + { + if let Some(existing_thread_id) = + sqlx::query_scalar("SELECT id FROM threads WHERE user_id = ? AND status = 1") + .bind(user_id) + .fetch_optional(pool) + .await? + { + Ok(existing_thread_id) + } else { + Err(Error::Database(db_err)) + } + } + Err(e) => Err(e), + }; + + res } pub async fn close_thread( @@ -379,3 +416,97 @@ pub async fn is_orphaned_thread_channel( Ok(exists) } + +pub async fn get_all_thread_status(pool: &SqlitePool) -> Vec { + match sqlx::query!( + r#" + SELECT channel_id, owner_id, taken_by, last_message_by, last_message_at + FROM thread_status + "# + ) + .fetch_all(pool) + .await + { + Ok(rows) => rows + .into_iter() + .map(|r| TicketState { + channel_id: r.channel_id, + owner_id: r.owner_id, + taken_by: r.taken_by, + last_message_by: TicketAuthor::from_str(&r.last_message_by), + last_message_at: r.last_message_at, + }) + .collect(), + Err(e) => { + eprintln!("Database error getting thread statuses: {:?}", e); + vec![] + } + } +} + +pub async fn get_thread_status(thread_id: &str, pool: &SqlitePool) -> Option { + match sqlx::query!( + r#" + SELECT + channel_id, + owner_id, + taken_by, + last_message_by, + last_message_at + FROM thread_status + WHERE thread_id = ? + "#, + thread_id + ) + .fetch_optional(pool) + .await + { + Ok(Some(row)) => Some(TicketState { + channel_id: row.channel_id, + owner_id: row.owner_id, + taken_by: row.taken_by, + last_message_by: TicketAuthor::from_str(&row.last_message_by), + last_message_at: row.last_message_at, + }), + Ok(None) => None, + Err(e) => { + eprintln!( + "⚠️ Database error getting thread status for id {}: {:?}", + thread_id, e + ); + None + } + } +} + +pub async fn update_thread_status( + thread_id: &str, + ticket: &TicketState, + pool: &SqlitePool, +) -> ModmailResult<()> { + let last_message_by = format!("{:?}", ticket.last_message_by).to_lowercase(); + + println!( + "Updating thread status for thread_id {}: taken_by={:?}, last_message_by={}, last_message_at={}", + thread_id, ticket.taken_by, last_message_by, ticket.last_message_at + ); + + sqlx::query!( + r#" + UPDATE thread_status + SET + taken_by = ?, + last_message_by = ?, + last_message_at = ? + WHERE thread_id = ? + "#, + ticket.taken_by, + last_message_by, + ticket.last_message_at, + thread_id + ) + .execute(pool) + .await?; + + Ok(()) +} diff --git a/rustmail/src/handlers/ready_handler.rs b/rustmail/src/handlers/ready_handler.rs index 796abd5..f9e995d 100644 --- a/rustmail/src/handlers/ready_handler.rs +++ b/rustmail/src/handlers/ready_handler.rs @@ -1,14 +1,19 @@ +use crate::db::get_all_thread_status; use crate::prelude::commands::*; use crate::prelude::config::*; use crate::prelude::features::*; use crate::prelude::modules::*; use serenity::all::{ActivityData, CreateCommand, GuildId}; +use serenity::futures::future::join_all; use serenity::{ all::{Context, EventHandler, Ready}, async_trait, }; +use sqlx::SqlitePool; use std::sync::Arc; +use std::time::Duration; use tokio::sync::watch::Receiver; +use tokio::time::interval; #[derive(Clone)] pub struct ReadyHandler { @@ -39,11 +44,14 @@ impl EventHandler for ReadyHandler { } }; + let config = self.config.clone(); + ctx.set_activity(Option::from(ActivityData::playing(&self.config.bot.status))); tokio::spawn({ let ctx = ctx.clone(); - let config = self.config.clone(); + let config = config.clone(); + async move { let recovery_results = recover_missing_messages(&ctx, &config).await; send_recovery_summary(&ctx, &config, &recovery_results).await; @@ -52,7 +60,9 @@ impl EventHandler for ReadyHandler { } }); - load_reminders(&ctx, &self.config, pool, self.shutdown.clone()).await; + load_reminders(&ctx, &self.config, &pool.clone(), self.shutdown.clone()).await; + + update_threads_status(&ctx, &pool.clone()); let guild_id = GuildId::new(self.config.bot.get_staff_guild_id()); let guild_id2 = GuildId::new(self.config.bot.get_community_guild_id()); @@ -82,3 +92,40 @@ impl EventHandler for ReadyHandler { } } } + +fn update_threads_status(ctx: &Context, pool: &SqlitePool) { + tokio::spawn({ + let ctx = ctx.clone(); + let pool = pool.clone(); + + async move { + let mut interval = interval(Duration::from_secs(60 * 10)); + + loop { + let tickets_status = get_all_thread_status(&pool).await; + + let mut handles = Vec::new(); + for ticket in tickets_status.iter() { + let ctx = ctx.clone(); + let ticket = ticket.clone(); + let handle = tokio::spawn(async move { + println!("Update"); + if let Err(e) = update_thread_status(&ctx, &ticket).await { + eprintln!( + "Failed to update thread status for channel {}: {:?}", + ticket.channel_id, e + ); + } + }); + handles.push(handle); + } + + join_all(handles).await; + + println!("Updated {} ticket statuses", tickets_status.len()); + + interval.tick().await; + } + } + }); +} diff --git a/rustmail/src/modules/mod.rs b/rustmail/src/modules/mod.rs index ad296f4..4309409 100644 --- a/rustmail/src/modules/mod.rs +++ b/rustmail/src/modules/mod.rs @@ -3,9 +3,11 @@ pub mod message_recovery; pub mod reminders; pub mod scheduled_closures; pub mod threads; +pub mod threads_status; pub use commands::*; pub use message_recovery::*; pub use reminders::*; pub use scheduled_closures::*; pub use threads::*; +pub use threads_status::*; diff --git a/rustmail/src/modules/threads.rs b/rustmail/src/modules/threads.rs index b780190..d1d6945 100644 --- a/rustmail/src/modules/threads.rs +++ b/rustmail/src/modules/threads.rs @@ -3,6 +3,8 @@ use crate::prelude::db::*; use crate::prelude::errors::*; use crate::prelude::i18n::*; use crate::prelude::utils::*; +use crate::types::TicketAuthor; +use chrono::Utc; use serenity::all::{ ActionRowComponent, Channel, ChannelId, ComponentInteraction, Context, CreateChannel, CreateInteractionResponseFollowup, GuildId, Message, ModalInteraction, UserId, @@ -31,9 +33,11 @@ async fn create_or_get_thread_for_user( Err(_) => user_id.get().to_string(), }; + let thread_name = format!("🔴・{}・0m", username); + let staff_guild_id = GuildId::new(config.bot.get_staff_guild_id()); let channel_builder = - CreateChannel::new(&username).category(ChannelId::new(config.thread.inbox_category_id)); + CreateChannel::new(&thread_name).category(ChannelId::new(config.thread.inbox_category_id)); let channel = staff_guild_id .create_channel(&ctx.http, channel_builder) diff --git a/rustmail/src/modules/threads_status.rs b/rustmail/src/modules/threads_status.rs new file mode 100644 index 0000000..d6e605e --- /dev/null +++ b/rustmail/src/modules/threads_status.rs @@ -0,0 +1,60 @@ +use crate::prelude::errors::*; +use crate::prelude::types::*; +use chrono::Utc; +use serenity::all::{ChannelId, UserId}; +use serenity::builder::EditChannel; +use serenity::client::Context; + +pub async fn update_thread_status(ctx: &Context, ticket: &TicketState) -> ModmailResult<()> { + let channel = ChannelId::new(ticket.channel_id as u64); + + let color = match ticket.last_message_by { + TicketAuthor::Staff => "🔵", + TicketAuthor::User => "🔴", + }; + + let elapsed = Utc::now().timestamp() - ticket.last_message_at; + let minutes = elapsed / 60; + let time_str = if minutes < 60 { + format!("{}m", minutes) + } else { + format!("{}h", minutes / 60) + }; + + let owner_id = ticket.owner_id.parse().unwrap_or(0); + let owner_name = if owner_id != 0 { + ctx.cache + .user(UserId::new(owner_id)) + .map(|u| u.name.clone()) + .unwrap_or_else(|| format!("User-{}", owner_id)) + } else { + "Unknown".to_string() + }; + + let mut name = format!("{color}・{}", owner_name); + + if let Some(staff_id) = &ticket.taken_by { + let staff_id_u64 = staff_id.parse().unwrap_or(0); + if staff_id_u64 != 0 { + let staff_name = ctx + .cache + .user(UserId::new(staff_id_u64)) + .map(|u| u.name.clone()) + .unwrap_or_else(|| format!("Staff-{}", staff_id_u64)); + name.push_str(&format!("・{}", staff_name)); + } + } + + name.push_str(&format!("・{}", time_str)); + + tokio::spawn({ + let ctx = ctx.clone(); + async move { + let _ = channel + .edit(&ctx.http, EditChannel::new().name(&name)) + .await; + } + }); + + Ok(()) +} diff --git a/rustmail/src/types/mod.rs b/rustmail/src/types/mod.rs index 31b7717..13633bf 100644 --- a/rustmail/src/types/mod.rs +++ b/rustmail/src/types/mod.rs @@ -1,5 +1,7 @@ pub mod bot; pub mod logs; +pub mod threads_status; pub use bot::*; pub use logs::*; +pub use threads_status::*; diff --git a/rustmail/src/types/threads_status.rs b/rustmail/src/types/threads_status.rs new file mode 100644 index 0000000..c1561ef --- /dev/null +++ b/rustmail/src/types/threads_status.rs @@ -0,0 +1,23 @@ +#[derive(Debug, Clone)] +pub struct TicketState { + pub channel_id: i64, + pub owner_id: String, + pub taken_by: Option, + pub last_message_by: TicketAuthor, + pub last_message_at: i64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TicketAuthor { + Staff, + User, +} + +impl TicketAuthor { + pub fn from_str(s: &str) -> Self { + match s.to_lowercase().as_str() { + "staff" => TicketAuthor::Staff, + _ => TicketAuthor::User, + } + } +} diff --git a/rustmail/src/utils/thread/send_to_thread.rs b/rustmail/src/utils/thread/send_to_thread.rs index 7c31fc5..f96f0a9 100644 --- a/rustmail/src/utils/thread/send_to_thread.rs +++ b/rustmail/src/utils/thread/send_to_thread.rs @@ -3,6 +3,8 @@ use crate::prelude::db::*; use crate::prelude::errors::*; use crate::prelude::i18n::*; use crate::prelude::utils::*; +use crate::types::TicketAuthor; +use chrono::Utc; use serenity::all::{ChannelId, Context, CreateAttachment, GuildId, Message, UserId}; use std::collections::HashMap; @@ -126,6 +128,17 @@ pub async fn send_to_thread( } }; + let mut ticket_status = match get_thread_status(&thread_id.clone(), &pool).await { + Some(status) => status, + None => { + return Err(validation_failed("Failed to get thread status")); + } + }; + + ticket_status.last_message_by = TicketAuthor::User; + ticket_status.last_message_at = Utc::now().timestamp(); + update_thread_status(&thread_id.clone(), &ticket_status, &pool.clone()).await?; + let builder = MessageBuilder::begin_user_incoming(ctx, config, thread_id.clone(), msg) .to_thread(channel_id) .content(content) From b3cb6afb1f5d01fb4bb471c423f949b372f09840 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 30 Oct 2025 21:29:38 +0100 Subject: [PATCH 087/100] refactor(threads): add timeout for thread's status updates --- .../anonreply/text_command/anonreply.rs | 4 ++- .../src/commands/reply/slash_command/reply.rs | 4 ++- .../src/commands/reply/text_command/reply.rs | 4 ++- rustmail/src/db/operations/threads.rs | 2 +- rustmail/src/handlers/ready_handler.rs | 4 ++- rustmail/src/modules/threads_status.rs | 34 ++++++++++++++----- rustmail/src/utils/thread/send_to_thread.rs | 4 ++- 7 files changed, 41 insertions(+), 15 deletions(-) diff --git a/rustmail/src/commands/anonreply/text_command/anonreply.rs b/rustmail/src/commands/anonreply/text_command/anonreply.rs index c60475c..4df3181 100644 --- a/rustmail/src/commands/anonreply/text_command/anonreply.rs +++ b/rustmail/src/commands/anonreply/text_command/anonreply.rs @@ -8,6 +8,7 @@ use chrono::Utc; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; +use crate::modules::update_thread_status_ui; pub async fn anonreply( ctx: Context, @@ -77,7 +78,8 @@ pub async fn anonreply( ticket_status.last_message_by = TicketAuthor::Staff; ticket_status.last_message_at = Utc::now().timestamp(); - update_thread_status(&thread.id, &ticket_status, db_pool).await?; + update_thread_status_db(&thread.id, &ticket_status, db_pool).await?; + update_thread_status_ui(&ctx, &ticket_status).await?; let _ = msg.delete(&ctx.http).await; diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index ac35524..7705d5e 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -14,6 +14,7 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; +use crate::modules::update_thread_status_ui; pub struct ReplyCommand; @@ -166,7 +167,8 @@ impl RegistrableCommand for ReplyCommand { ticket_status.last_message_by = TicketAuthor::Staff; ticket_status.last_message_at = Utc::now().timestamp(); - update_thread_status(&thread.id, &ticket_status, db_pool).await?; + update_thread_status_db(&thread.id, &ticket_status, db_pool).await?; + update_thread_status_ui(&ctx, &ticket_status).await?; let mut sr = MessageBuilder::begin_staff_reply( &ctx, diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index 091cc38..45e67ed 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -8,6 +8,7 @@ use chrono::Utc; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; +use crate::modules::update_thread_status_ui; pub async fn reply( ctx: Context, @@ -49,7 +50,8 @@ pub async fn reply( ticket_status.last_message_by = TicketAuthor::Staff; ticket_status.last_message_at = Utc::now().timestamp(); - update_thread_status(&thread.id, &ticket_status, db_pool).await?; + update_thread_status_db(&thread.id, &ticket_status, db_pool).await?; + update_thread_status_ui(&ctx, &ticket_status).await?; let _ = msg.delete(&ctx.http).await; diff --git a/rustmail/src/db/operations/threads.rs b/rustmail/src/db/operations/threads.rs index 848d05d..a6b750a 100644 --- a/rustmail/src/db/operations/threads.rs +++ b/rustmail/src/db/operations/threads.rs @@ -479,7 +479,7 @@ pub async fn get_thread_status(thread_id: &str, pool: &SqlitePool) -> Option ModmailResult<()> { +pub async fn update_thread_status_ui(ctx: &Context, ticket: &TicketState) -> ModmailResult<()> { let channel = ChannelId::new(ticket.channel_id as u64); let color = match ticket.last_message_by { @@ -47,14 +49,28 @@ pub async fn update_thread_status(ctx: &Context, ticket: &TicketState) -> Modmai name.push_str(&format!("・{}", time_str)); - tokio::spawn({ - let ctx = ctx.clone(); - async move { - let _ = channel - .edit(&ctx.http, EditChannel::new().name(&name)) - .await; + let result = timeout( + Duration::from_secs(2), + channel.edit(&ctx.http, EditChannel::new().name(&name)) + ).await; + + match result { + Ok(Ok(_)) => Ok(()), + + Ok(Err(e)) => { + eprintln!( + "Failed to edit channel {}: {:?}", + ticket.channel_id, e + ); + Err(e.into()) } - }); - Ok(()) + Err(_) => { + eprintln!( + "Timeout editing channel {} (skipping)", + ticket.channel_id + ); + Ok(()) + } + } } diff --git a/rustmail/src/utils/thread/send_to_thread.rs b/rustmail/src/utils/thread/send_to_thread.rs index f96f0a9..2794bb1 100644 --- a/rustmail/src/utils/thread/send_to_thread.rs +++ b/rustmail/src/utils/thread/send_to_thread.rs @@ -7,6 +7,7 @@ use crate::types::TicketAuthor; use chrono::Utc; use serenity::all::{ChannelId, Context, CreateAttachment, GuildId, Message, UserId}; use std::collections::HashMap; +use crate::modules::update_thread_status_ui; fn extract_message_content_with_media(msg: &Message) -> (String, Vec) { let content = msg.content.clone(); @@ -137,7 +138,8 @@ pub async fn send_to_thread( ticket_status.last_message_by = TicketAuthor::User; ticket_status.last_message_at = Utc::now().timestamp(); - update_thread_status(&thread_id.clone(), &ticket_status, &pool.clone()).await?; + update_thread_status_db(&thread_id.clone(), &ticket_status, &pool.clone()).await?; + update_thread_status_ui(&ctx, &ticket_status).await?; let builder = MessageBuilder::begin_user_incoming(ctx, config, thread_id.clone(), msg) .to_thread(channel_id) From a598587e48c01d43fdf8d4d89606197bcb6cc11c Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Thu, 30 Oct 2025 21:30:08 +0100 Subject: [PATCH 088/100] refactor(db): add missing sqlx files --- ...9aaee2f4e172a153fc44a20ff7a98604d16a3.json | 12 +++++ ...639f55f82370b28eeb6272d5ee01c283611da.json | 12 +++++ ...de58070b22eabdc4cd28ad1ddfd453746f7b5.json | 44 +++++++++++++++++++ ...d08dc95726471d1d8e9319b5e6943fee45171.json | 44 +++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 .sqlx/query-3c9fbdf8549fa66a5ba863fad1e9aaee2f4e172a153fc44a20ff7a98604d16a3.json create mode 100644 .sqlx/query-a6bb2fb861c5e46306f8c370816639f55f82370b28eeb6272d5ee01c283611da.json create mode 100644 .sqlx/query-a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5.json create mode 100644 .sqlx/query-b5e89fffcf439bc2e8c770ca2ded08dc95726471d1d8e9319b5e6943fee45171.json diff --git a/.sqlx/query-3c9fbdf8549fa66a5ba863fad1e9aaee2f4e172a153fc44a20ff7a98604d16a3.json b/.sqlx/query-3c9fbdf8549fa66a5ba863fad1e9aaee2f4e172a153fc44a20ff7a98604d16a3.json new file mode 100644 index 0000000..fa4d274 --- /dev/null +++ b/.sqlx/query-3c9fbdf8549fa66a5ba863fad1e9aaee2f4e172a153fc44a20ff7a98604d16a3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE thread_status\n SET\n taken_by = ?,\n last_message_by = ?,\n last_message_at = ?\n WHERE thread_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "3c9fbdf8549fa66a5ba863fad1e9aaee2f4e172a153fc44a20ff7a98604d16a3" +} diff --git a/.sqlx/query-a6bb2fb861c5e46306f8c370816639f55f82370b28eeb6272d5ee01c283611da.json b/.sqlx/query-a6bb2fb861c5e46306f8c370816639f55f82370b28eeb6272d5ee01c283611da.json new file mode 100644 index 0000000..f227c79 --- /dev/null +++ b/.sqlx/query-a6bb2fb861c5e46306f8c370816639f55f82370b28eeb6272d5ee01c283611da.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO thread_status (thread_id, channel_id, owner_id, taken_by, last_message_by, last_message_at) VALUES (?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "a6bb2fb861c5e46306f8c370816639f55f82370b28eeb6272d5ee01c283611da" +} diff --git a/.sqlx/query-a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5.json b/.sqlx/query-a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5.json new file mode 100644 index 0000000..1e3ef68 --- /dev/null +++ b/.sqlx/query-a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT channel_id, owner_id, taken_by, last_message_by, last_message_at\n FROM thread_status\n ", + "describe": { + "columns": [ + { + "name": "channel_id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "owner_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "taken_by", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "last_message_by", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "last_message_at", + "ordinal": 4, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + true, + false, + false + ] + }, + "hash": "a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5" +} diff --git a/.sqlx/query-b5e89fffcf439bc2e8c770ca2ded08dc95726471d1d8e9319b5e6943fee45171.json b/.sqlx/query-b5e89fffcf439bc2e8c770ca2ded08dc95726471d1d8e9319b5e6943fee45171.json new file mode 100644 index 0000000..47ba670 --- /dev/null +++ b/.sqlx/query-b5e89fffcf439bc2e8c770ca2ded08dc95726471d1d8e9319b5e6943fee45171.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n channel_id,\n owner_id,\n taken_by,\n last_message_by,\n last_message_at\n FROM thread_status\n WHERE thread_id = ?\n ", + "describe": { + "columns": [ + { + "name": "channel_id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "owner_id", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "taken_by", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "last_message_by", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "last_message_at", + "ordinal": 4, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + false, + false + ] + }, + "hash": "b5e89fffcf439bc2e8c770ca2ded08dc95726471d1d8e9319b5e6943fee45171" +} From 5c9594b4474de31250dd2e23a755eecd5bc6e7d5 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 31 Oct 2025 08:38:57 +0100 Subject: [PATCH 089/100] refactor(db): only fetch thread_status entries with active threads --- .../new_thread/slash_command/new_thread.rs | 2 +- .../new_thread/text_command/new_thread.rs | 2 +- rustmail/src/db/operations/threads.rs | 10 +++++-- rustmail/src/modules/threads_status.rs | 26 +++++-------------- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index 64fc05c..9ad7b2f 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -137,7 +137,7 @@ impl RegistrableCommand for NewThreadCommand { } let inbox_category_id = ChannelId::new(config.thread.inbox_category_id); - let channel_name = user.name.to_lowercase().replace(" ", "-").to_string(); + let channel_name = format!("🔵・{}・0m", user.name); let mut channel_builder = serenity::all::CreateChannel::new(&channel_name); channel_builder = channel_builder .kind(serenity::model::channel::ChannelType::Text) diff --git a/rustmail/src/commands/new_thread/text_command/new_thread.rs b/rustmail/src/commands/new_thread/text_command/new_thread.rs index cb7b79b..174c3fb 100644 --- a/rustmail/src/commands/new_thread/text_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/text_command/new_thread.rs @@ -58,7 +58,7 @@ pub async fn new_thread( } let inbox_category_id = ChannelId::new(config.thread.inbox_category_id); - let channel_name = user.name.to_lowercase().replace(" ", "-").to_string(); + let channel_name = format!("🔵・{}・0m", user.name); let mut channel_builder = serenity::all::CreateChannel::new(&channel_name); channel_builder = channel_builder .kind(serenity::model::channel::ChannelType::Text) diff --git a/rustmail/src/db/operations/threads.rs b/rustmail/src/db/operations/threads.rs index a6b750a..78d5ed8 100644 --- a/rustmail/src/db/operations/threads.rs +++ b/rustmail/src/db/operations/threads.rs @@ -420,8 +420,14 @@ pub async fn is_orphaned_thread_channel( pub async fn get_all_thread_status(pool: &SqlitePool) -> Vec { match sqlx::query!( r#" - SELECT channel_id, owner_id, taken_by, last_message_by, last_message_at - FROM thread_status + SELECT ts.channel_id, + ts.owner_id, + ts.taken_by, + ts.last_message_by, + ts.last_message_at + FROM thread_status ts + JOIN threads t ON ts.thread_id = t.id + WHERE t.status = 1 "# ) .fetch_all(pool) diff --git a/rustmail/src/modules/threads_status.rs b/rustmail/src/modules/threads_status.rs index face887..54d5311 100644 --- a/rustmail/src/modules/threads_status.rs +++ b/rustmail/src/modules/threads_status.rs @@ -1,10 +1,10 @@ -use std::time::Duration; use crate::prelude::errors::*; use crate::prelude::types::*; use chrono::Utc; use serenity::all::{ChannelId, UserId}; use serenity::builder::EditChannel; use serenity::client::Context; +use std::time::Duration; use tokio::time::timeout; pub async fn update_thread_status_ui(ctx: &Context, ticket: &TicketState) -> ModmailResult<()> { @@ -24,14 +24,7 @@ pub async fn update_thread_status_ui(ctx: &Context, ticket: &TicketState) -> Mod }; let owner_id = ticket.owner_id.parse().unwrap_or(0); - let owner_name = if owner_id != 0 { - ctx.cache - .user(UserId::new(owner_id)) - .map(|u| u.name.clone()) - .unwrap_or_else(|| format!("User-{}", owner_id)) - } else { - "Unknown".to_string() - }; + let owner_name = UserId::new(owner_id).to_user(&ctx.http).await?.name; let mut name = format!("{color}・{}", owner_name); @@ -51,25 +44,20 @@ pub async fn update_thread_status_ui(ctx: &Context, ticket: &TicketState) -> Mod let result = timeout( Duration::from_secs(2), - channel.edit(&ctx.http, EditChannel::new().name(&name)) - ).await; + channel.edit(&ctx.http, EditChannel::new().name(&name)), + ) + .await; match result { Ok(Ok(_)) => Ok(()), Ok(Err(e)) => { - eprintln!( - "Failed to edit channel {}: {:?}", - ticket.channel_id, e - ); + eprintln!("Failed to edit channel {}: {:?}", ticket.channel_id, e); Err(e.into()) } Err(_) => { - eprintln!( - "Timeout editing channel {} (skipping)", - ticket.channel_id - ); + eprintln!("Timeout editing channel {} (skipping)", ticket.channel_id); Ok(()) } } From 01362178d517c5a96abdff70bc0ed1ac52a77a6e Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 31 Oct 2025 08:42:10 +0100 Subject: [PATCH 090/100] refactor(db): add missing sqlx files --- ...018f27ee5b43603fd99f8386ea396839656999962e86a06f8019.json} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .sqlx/{query-a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5.json => query-ea1cdea86310018f27ee5b43603fd99f8386ea396839656999962e86a06f8019.json} (65%) diff --git a/.sqlx/query-a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5.json b/.sqlx/query-ea1cdea86310018f27ee5b43603fd99f8386ea396839656999962e86a06f8019.json similarity index 65% rename from .sqlx/query-a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5.json rename to .sqlx/query-ea1cdea86310018f27ee5b43603fd99f8386ea396839656999962e86a06f8019.json index 1e3ef68..eb2fd0d 100644 --- a/.sqlx/query-a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5.json +++ b/.sqlx/query-ea1cdea86310018f27ee5b43603fd99f8386ea396839656999962e86a06f8019.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT channel_id, owner_id, taken_by, last_message_by, last_message_at\n FROM thread_status\n ", + "query": "\n SELECT ts.channel_id,\n ts.owner_id,\n ts.taken_by,\n ts.last_message_by,\n ts.last_message_at\n FROM thread_status ts\n JOIN threads t ON ts.thread_id = t.id\n WHERE t.status = 1\n ", "describe": { "columns": [ { @@ -40,5 +40,5 @@ false ] }, - "hash": "a830a59ecc2c2280bb668e4fb8ede58070b22eabdc4cd28ad1ddfd453746f7b5" + "hash": "ea1cdea86310018f27ee5b43603fd99f8386ea396839656999962e86a06f8019" } From 14f5c8dbbb6ff18865c2ef386dd6e47f462c1789 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 31 Oct 2025 09:39:35 +0100 Subject: [PATCH 091/100] refactor(status): add tokio task for updating channel's names --- .../anonreply/text_command/anonreply.rs | 10 +++- .../new_thread/slash_command/new_thread.rs | 2 +- .../new_thread/text_command/new_thread.rs | 2 +- .../commands/release/slash_command/release.rs | 57 ++++++++++++------- .../commands/release/text_command/release.rs | 47 +++++++++------ .../src/commands/reply/slash_command/reply.rs | 10 +++- .../src/commands/reply/text_command/reply.rs | 10 +++- .../src/commands/take/slash_command/take.rs | 57 ++++++++++++------- .../src/commands/take/text_command/take.rs | 50 +++++++++------- rustmail/src/i18n/language/en.rs | 7 ++- rustmail/src/i18n/language/fr.rs | 4 +- 11 files changed, 164 insertions(+), 92 deletions(-) diff --git a/rustmail/src/commands/anonreply/text_command/anonreply.rs b/rustmail/src/commands/anonreply/text_command/anonreply.rs index 4df3181..1173b06 100644 --- a/rustmail/src/commands/anonreply/text_command/anonreply.rs +++ b/rustmail/src/commands/anonreply/text_command/anonreply.rs @@ -1,3 +1,4 @@ +use crate::modules::update_thread_status_ui; use crate::prelude::config::*; use crate::prelude::db::*; use crate::prelude::errors::*; @@ -8,7 +9,6 @@ use chrono::Utc; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; -use crate::modules::update_thread_status_ui; pub async fn anonreply( ctx: Context, @@ -79,7 +79,13 @@ pub async fn anonreply( ticket_status.last_message_by = TicketAuthor::Staff; ticket_status.last_message_at = Utc::now().timestamp(); update_thread_status_db(&thread.id, &ticket_status, db_pool).await?; - update_thread_status_ui(&ctx, &ticket_status).await?; + + tokio::spawn({ + let ctx = ctx.clone(); + async move { + let _ = update_thread_status_ui(&ctx, &ticket_status).await; + } + }); let _ = msg.delete(&ctx.http).await; diff --git a/rustmail/src/commands/new_thread/slash_command/new_thread.rs b/rustmail/src/commands/new_thread/slash_command/new_thread.rs index 9ad7b2f..0a54df1 100644 --- a/rustmail/src/commands/new_thread/slash_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/slash_command/new_thread.rs @@ -137,7 +137,7 @@ impl RegistrableCommand for NewThreadCommand { } let inbox_category_id = ChannelId::new(config.thread.inbox_category_id); - let channel_name = format!("🔵・{}・0m", user.name); + let channel_name = format!("🔴・{}・0m", user.name); let mut channel_builder = serenity::all::CreateChannel::new(&channel_name); channel_builder = channel_builder .kind(serenity::model::channel::ChannelType::Text) diff --git a/rustmail/src/commands/new_thread/text_command/new_thread.rs b/rustmail/src/commands/new_thread/text_command/new_thread.rs index 174c3fb..afc91aa 100644 --- a/rustmail/src/commands/new_thread/text_command/new_thread.rs +++ b/rustmail/src/commands/new_thread/text_command/new_thread.rs @@ -58,7 +58,7 @@ pub async fn new_thread( } let inbox_category_id = ChannelId::new(config.thread.inbox_category_id); - let channel_name = format!("🔵・{}・0m", user.name); + let channel_name = format!("🔴・{}・0m", user.name); let mut channel_builder = serenity::all::CreateChannel::new(&channel_name); channel_builder = channel_builder .kind(serenity::model::channel::ChannelType::Text) diff --git a/rustmail/src/commands/release/slash_command/release.rs b/rustmail/src/commands/release/slash_command/release.rs index 3fd44d3..7b00013 100644 --- a/rustmail/src/commands/release/slash_command/release.rs +++ b/rustmail/src/commands/release/slash_command/release.rs @@ -1,3 +1,4 @@ +use crate::modules::update_thread_status_ui; use crate::prelude::commands::*; use crate::prelude::config::*; use crate::prelude::db::*; @@ -83,29 +84,45 @@ impl RegistrableCommand for ReleaseCommand { if thread_name == thread.user_name { return Err(ModmailError::Command(CommandError::TicketAlreadyReleased)); } - - tokio::spawn(async move { - let _ = rename_channel_with_timeout( - &ctx, - &config, - thread_id, - thread.user_name.clone(), - None, - Some(&command), - ) - .await; - - let mut params = std::collections::HashMap::new(); - params.insert("staff".to_string(), format!("<@{}>", command.user.id)); - let response = MessageBuilder::system_message(&ctx, &config) - .translated_content("release.confirmation", Some(¶ms), None, None) - .await - .to_channel(command.channel_id) - .build_interaction_message_followup() + tokio::spawn({ + let db_pool = db_pool.clone(); + + async move { + let mut ticket_status = match get_thread_status(&thread.id, &db_pool).await + { + Some(status) => status, + None => { + return; + } + }; + ticket_status.taken_by = None; + let _ = update_thread_status_db( + &thread.id.to_string(), + &ticket_status, + &db_pool, + ) .await; - let _ = command.create_followup(ctx.clone(), response).await; + tokio::spawn({ + let ctx = ctx.clone(); + async move { + let _ = update_thread_status_ui(&ctx, &ticket_status).await; + } + }); + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", command.user.id)); + + let response = MessageBuilder::system_message(&ctx, &config) + .translated_content("release.confirmation", Some(¶ms), None, None) + .await + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + let _ = command.create_followup(ctx.clone(), response).await; + } }); Ok(()) diff --git a/rustmail/src/commands/release/text_command/release.rs b/rustmail/src/commands/release/text_command/release.rs index 1bb4da0..5f81a40 100644 --- a/rustmail/src/commands/release/text_command/release.rs +++ b/rustmail/src/commands/release/text_command/release.rs @@ -1,3 +1,4 @@ +use crate::modules::update_thread_status_ui; use crate::prelude::commands::*; use crate::prelude::config::*; use crate::prelude::db::*; @@ -39,26 +40,36 @@ pub async fn release( let config_clone = config.clone(); - tokio::spawn(async move { - let _ = rename_channel_with_timeout( - &ctx, - &config_clone, - thread_id, - thread.user_name.clone(), - Some(&msg), - None, - ) - .await; + tokio::spawn({ + let db_pool = db_pool.clone(); - let mut params = std::collections::HashMap::new(); - params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); + async move { + let mut ticket_status = match get_thread_status(&thread.id, &db_pool).await { + Some(status) => status, + None => { + return; + } + }; + ticket_status.taken_by = None; + let _ = update_thread_status_db(&thread.id, &ticket_status, &db_pool).await; - let _ = MessageBuilder::system_message(&ctx, &config_clone) - .translated_content("release.confirmation", Some(¶ms), None, None) - .await - .to_channel(msg.channel_id) - .send(true) - .await; + tokio::spawn({ + let ctx = ctx.clone(); + async move { + let _ = update_thread_status_ui(&ctx, &ticket_status).await; + } + }); + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); + + let _ = MessageBuilder::system_message(&ctx, &config_clone) + .translated_content("release.confirmation", Some(¶ms), None, None) + .await + .to_channel(msg.channel_id) + .send(true) + .await; + } }); Ok(()) diff --git a/rustmail/src/commands/reply/slash_command/reply.rs b/rustmail/src/commands/reply/slash_command/reply.rs index 7705d5e..1c9bbee 100644 --- a/rustmail/src/commands/reply/slash_command/reply.rs +++ b/rustmail/src/commands/reply/slash_command/reply.rs @@ -1,3 +1,4 @@ +use crate::modules::update_thread_status_ui; use crate::prelude::commands::*; use crate::prelude::config::*; use crate::prelude::db::*; @@ -14,7 +15,6 @@ use serenity::all::{ }; use std::collections::HashMap; use std::sync::Arc; -use crate::modules::update_thread_status_ui; pub struct ReplyCommand; @@ -168,7 +168,13 @@ impl RegistrableCommand for ReplyCommand { ticket_status.last_message_by = TicketAuthor::Staff; ticket_status.last_message_at = Utc::now().timestamp(); update_thread_status_db(&thread.id, &ticket_status, db_pool).await?; - update_thread_status_ui(&ctx, &ticket_status).await?; + + tokio::spawn({ + let ctx = ctx.clone(); + async move { + let _ = update_thread_status_ui(&ctx, &ticket_status).await; + } + }); let mut sr = MessageBuilder::begin_staff_reply( &ctx, diff --git a/rustmail/src/commands/reply/text_command/reply.rs b/rustmail/src/commands/reply/text_command/reply.rs index 45e67ed..20b9436 100644 --- a/rustmail/src/commands/reply/text_command/reply.rs +++ b/rustmail/src/commands/reply/text_command/reply.rs @@ -1,3 +1,4 @@ +use crate::modules::update_thread_status_ui; use crate::prelude::config::*; use crate::prelude::db::*; use crate::prelude::errors::*; @@ -8,7 +9,6 @@ use chrono::Utc; use serenity::all::{Context, GuildId, Message, UserId}; use std::collections::HashMap; use std::sync::Arc; -use crate::modules::update_thread_status_ui; pub async fn reply( ctx: Context, @@ -51,7 +51,13 @@ pub async fn reply( ticket_status.last_message_by = TicketAuthor::Staff; ticket_status.last_message_at = Utc::now().timestamp(); update_thread_status_db(&thread.id, &ticket_status, db_pool).await?; - update_thread_status_ui(&ctx, &ticket_status).await?; + + tokio::spawn({ + let ctx = ctx.clone(); + async move { + let _ = update_thread_status_ui(&ctx, &ticket_status).await; + } + }); let _ = msg.delete(&ctx.http).await; diff --git a/rustmail/src/commands/take/slash_command/take.rs b/rustmail/src/commands/take/slash_command/take.rs index 56f3fd2..e6095ee 100644 --- a/rustmail/src/commands/take/slash_command/take.rs +++ b/rustmail/src/commands/take/slash_command/take.rs @@ -1,3 +1,4 @@ +use crate::modules::update_thread_status_ui; use crate::prelude::commands::*; use crate::prelude::config::*; use crate::prelude::db::*; @@ -84,28 +85,40 @@ impl RegistrableCommand for TakeCommand { return Err(ModmailError::Command(CommandError::TicketAlreadyTaken)); } - tokio::spawn(async move { - let _ = rename_channel_with_timeout( - &ctx, - &config, - thread_id, - format!("🔵-{}", command.user.name.clone()), - None, - Some(&command), - ) - .await; - - let mut params = std::collections::HashMap::new(); - params.insert("staff".to_string(), format!("<@{}>", command.user.id)); - - let response = MessageBuilder::system_message(&ctx, &config) - .translated_content("take.confirmation", Some(¶ms), None, None) - .await - .to_channel(command.channel_id) - .build_interaction_message_followup() - .await; - - let _ = command.create_followup(ctx.clone(), response).await; + tokio::spawn({ + let config = config.clone(); + let db_pool = db_pool.clone(); + + async move { + let mut ticket_status = match get_thread_status(&thread.id, &db_pool).await + { + Some(status) => status, + None => { + return; + } + }; + ticket_status.taken_by = Some(command.user.id.to_string()); + let _ = update_thread_status_db(&thread.id, &ticket_status, &db_pool).await; + + tokio::spawn({ + let ctx = ctx.clone(); + async move { + let _ = update_thread_status_ui(&ctx, &ticket_status).await; + } + }); + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", command.user.id)); + + let response = MessageBuilder::system_message(&ctx, &config) + .translated_content("take.confirmation", Some(¶ms), None, None) + .await + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + let _ = command.create_followup(ctx.clone(), response).await; + } }); Ok(()) diff --git a/rustmail/src/commands/take/text_command/take.rs b/rustmail/src/commands/take/text_command/take.rs index ea7bb8a..4d62d2b 100644 --- a/rustmail/src/commands/take/text_command/take.rs +++ b/rustmail/src/commands/take/text_command/take.rs @@ -1,3 +1,4 @@ +use crate::modules::update_thread_status_ui; use crate::prelude::commands::*; use crate::prelude::config::*; use crate::prelude::db::*; @@ -36,29 +37,38 @@ pub async fn take( if thread_name == format!("🔵-{}", msg.author.name) { return Err(ModmailError::Command(CommandError::TicketAlreadyTaken)); } - + let config_clone = config.clone(); - - tokio::spawn(async move { - let _ = rename_channel_with_timeout( - &ctx, - &config_clone, - thread_id, - format!("🔵-{}", msg.author.name.clone()), - Some(&msg), - None, - ) - .await; - let mut params = std::collections::HashMap::new(); - params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); + tokio::spawn({ + let db_pool = db_pool.clone(); + async move { + let mut ticket_status = match get_thread_status(&thread.id, &db_pool).await { + Some(status) => status, + None => { + return; + } + }; + ticket_status.taken_by = Some(msg.author.id.to_string()); + let _ = update_thread_status_db(&thread.id, &ticket_status, &db_pool).await; + + tokio::spawn({ + let ctx = ctx.clone(); + async move { + let _ = update_thread_status_ui(&ctx, &ticket_status).await; + } + }); + + let mut params = std::collections::HashMap::new(); + params.insert("staff".to_string(), format!("<@{}>", msg.author.id)); - let _ = MessageBuilder::system_message(&ctx, &config_clone) - .translated_content("take.confirmation", Some(¶ms), None, None) - .await - .to_channel(msg.channel_id) - .send(true) - .await; + let _ = MessageBuilder::system_message(&ctx, &config_clone) + .translated_content("take.confirmation", Some(¶ms), None, None) + .await + .to_channel(msg.channel_id) + .send(true) + .await; + } }); Ok(()) diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 20a603f..6f664df 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -1,4 +1,7 @@ +use crate::commands::help; +use crate::errors::ModmailError::Discord; use crate::prelude::errors::*; +use std::thread::current; pub fn load_english_messages(dict: &mut ErrorDictionary) { dict.messages.insert( @@ -892,7 +895,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "take.confirmation".to_string(), - DictionaryMessage::new("The ticket is now taken by {staff}."), + DictionaryMessage::new("The ticket is now taken by {staff}.\nDue to **Discord's API**, the channel name change may take up to **10 minutes**."), ); dict.messages.insert( "take.timeout".to_string(), @@ -919,7 +922,7 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "release.confirmation".to_string(), - DictionaryMessage::new("The ticket has been released by {staff}."), + DictionaryMessage::new("The ticket has been released by {staff}.\nDue to **Discord's API**, the channel name change may take up to **10 minutes**."), ); dict.messages.insert( "slash_command.help_command_argument_desc".to_string(), diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 68f3597..4a81770 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -914,7 +914,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "take.confirmation".to_string(), - DictionaryMessage::new("Le ticket est maintenant pris en charge par {staff}."), + DictionaryMessage::new("Le ticket est maintenant pris en charge par {staff}.\nA cause de **l’API de Discord**, le changement de nom du salon peut prendre jusqu’à **10 minutes**."), ); dict.messages.insert( "take.timeout".to_string(), @@ -934,7 +934,7 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { ); dict.messages.insert( "release.confirmation".to_string(), - DictionaryMessage::new("Le ticket n'est plus pris en charge par {staff}."), + DictionaryMessage::new("Le ticket n'est plus pris en charge par {staff}.\nA cause de **l’API de Discord**, le changement de nom du salon peut prendre jusqu’à **10 minutes**."), ); dict.messages.insert( "slash_command.help_command_argument_desc".to_string(), From 25d7fac2f32b3a6d9da5b98e3321f72c5769588d Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 14 Nov 2025 23:20:25 +0100 Subject: [PATCH 092/100] refactor(gitignore): add config.toml.backup file in gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fe9956e..52ec881 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /target config.toml +config.toml.backup node_modules .env db/db.sqlite From 1a29f76681d2ea0a06ae6422b97edb35ce74a340 Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 14 Nov 2025 23:24:31 +0100 Subject: [PATCH 093/100] feat(types): add rustmail_types crate for shared types --- Cargo.lock | 494 ++------------------ Cargo.toml | 2 +- rustmail/Cargo.toml | 3 +- rustmail_panel/Cargo.toml | 8 +- rustmail_types/Cargo.toml | 9 + rustmail_types/src/api/mod.rs | 14 + rustmail_types/src/config/bot.rs | 114 +++++ rustmail_types/src/config/commands.rs | 14 + rustmail_types/src/config/error_handling.rs | 29 ++ rustmail_types/src/config/languages.rs | 18 + rustmail_types/src/config/logs.rs | 16 + rustmail_types/src/config/mod.rs | 17 + rustmail_types/src/config/notifications.rs | 36 ++ rustmail_types/src/config/reminders.rs | 14 + rustmail_types/src/config/threads.rs | 42 ++ rustmail_types/src/lib.rs | 5 + 16 files changed, 361 insertions(+), 474 deletions(-) create mode 100644 rustmail_types/Cargo.toml create mode 100644 rustmail_types/src/api/mod.rs create mode 100644 rustmail_types/src/config/bot.rs create mode 100644 rustmail_types/src/config/commands.rs create mode 100644 rustmail_types/src/config/error_handling.rs create mode 100644 rustmail_types/src/config/languages.rs create mode 100644 rustmail_types/src/config/logs.rs create mode 100644 rustmail_types/src/config/mod.rs create mode 100644 rustmail_types/src/config/notifications.rs create mode 100644 rustmail_types/src/config/reminders.rs create mode 100644 rustmail_types/src/config/threads.rs create mode 100644 rustmail_types/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1ed8f13..985efca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,12 +45,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - [[package]] name = "anymap2" version = "0.13.0" @@ -66,28 +60,6 @@ dependencies = [ "serde", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "async-trait" version = "0.1.89" @@ -120,40 +92,13 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "axum" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" -dependencies = [ - "async-trait", - "axum-core 0.4.5", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "itoa", - "matchit 0.7.3", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "sync_wrapper 1.0.2", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - [[package]] name = "axum" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ - "axum-core 0.5.5", + "axum-core", "bytes", "form_urlencoded", "futures-util", @@ -163,7 +108,7 @@ dependencies = [ "hyper 1.7.0", "hyper-util", "itoa", - "matchit 0.8.4", + "matchit", "memchr", "mime", "percent-encoding", @@ -174,32 +119,12 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.2", "tokio", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "axum-core" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper 1.0.2", - "tower-layer", - "tower-service", -] - [[package]] name = "axum-core" version = "0.5.5" @@ -225,8 +150,8 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ - "axum 0.8.6", - "axum-core 0.5.5", + "axum", + "axum-core", "bytes", "cookie", "futures-util", @@ -391,6 +316,7 @@ checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" dependencies = [ "chrono", "phf 0.12.1", + "serde", ] [[package]] @@ -413,45 +339,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console-api" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" -dependencies = [ - "futures-core", - "prost", - "prost-types", - "tonic", - "tracing-core", -] - -[[package]] -name = "console-subscriber" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" -dependencies = [ - "console-api", - "crossbeam-channel", - "crossbeam-utils", - "futures-task", - "hdrhistogram", - "humantime", - "hyper-util", - "prost", - "prost-types", - "serde", - "serde_json", - "thread_local", - "tokio", - "tokio-stream", - "tonic", - "tracing", - "tracing-core", - "tracing-subscriber", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -694,17 +581,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "erased-serde" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - [[package]] name = "errno" version = "0.3.12" @@ -1015,25 +891,6 @@ dependencies = [ "gloo-worker 0.4.0", ] -[[package]] -name = "gloo" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15282ece24eaf4bd338d73ef580c6714c8615155c4190c781290ee3fa0fd372" -dependencies = [ - "gloo-console 0.3.0", - "gloo-dialogs 0.2.0", - "gloo-events 0.2.0", - "gloo-file 0.3.0", - "gloo-history 0.2.2", - "gloo-net 0.5.0", - "gloo-render 0.2.0", - "gloo-storage 0.3.0", - "gloo-timers 0.3.0", - "gloo-utils 0.2.0", - "gloo-worker 0.5.0", -] - [[package]] name = "gloo-console" version = "0.2.3" @@ -1200,27 +1057,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "gloo-net" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" -dependencies = [ - "futures-channel", - "futures-core", - "futures-sink", - "gloo-utils 0.2.0", - "http 0.2.12", - "js-sys", - "pin-project", - "serde", - "serde_json", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "gloo-net" version = "0.6.0" @@ -1376,25 +1212,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "gloo-worker" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "085f262d7604911c8150162529cefab3782e91adb20202e8658f7275d2aefe5d" -dependencies = [ - "bincode", - "futures", - "gloo-utils 0.2.0", - "gloo-worker-macros", - "js-sys", - "pinned", - "serde", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "gloo-worker-macros" version = "0.1.0" @@ -1419,7 +1236,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.4", + "indexmap", "slab", "tokio", "tokio-util", @@ -1438,19 +1255,13 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.11.4", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -1477,19 +1288,6 @@ dependencies = [ "hashbrown 0.15.3", ] -[[package]] -name = "hdrhistogram" -version = "7.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" -dependencies = [ - "base64 0.21.7", - "byteorder", - "flate2", - "nom", - "num-traits", -] - [[package]] name = "heck" version = "0.5.0" @@ -1535,15 +1333,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "html-escape" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -dependencies = [ - "utf8-width", -] - [[package]] name = "html5ever" version = "0.35.0" @@ -1623,12 +1412,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - [[package]] name = "hyper" version = "0.14.32" @@ -1706,19 +1489,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-timeout" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" -dependencies = [ - "hyper 1.7.0", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - [[package]] name = "hyper-tls" version = "0.6.0" @@ -1910,7 +1680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84" dependencies = [ "implicit-clone-derive", - "indexmap 2.11.4", + "indexmap", ] [[package]] @@ -1923,16 +1693,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.11.4" @@ -1959,15 +1719,6 @@ dependencies = [ "serde", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -2084,21 +1835,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "matchit" version = "0.8.4" @@ -2152,12 +1888,6 @@ dependencies = [ "triomphe", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.8" @@ -2195,28 +1925,12 @@ dependencies = [ "tempfile", ] -[[package]] -name = "navigator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd7563c38e388f6d2a568ae781a71ca4e957768655923526d2e0d64a67bc4d6" - [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2614,38 +2328,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn 2.0.101", -] - -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost", -] - [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -2834,7 +2516,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-native-tls", - "tower 0.5.2", + "tower", "tower-http", "tower-service", "url", @@ -3024,18 +2706,17 @@ name = "rustmail" version = "0.1.1" dependencies = [ "async-trait", - "axum 0.8.6", + "axum", "axum-extra", "base64 0.22.1", "chrono", - "chrono-tz", - "console-subscriber", "hyper 1.7.0", "mime_guess", "rand", "regex", "reqwest 0.12.24", "rust-embed", + "rustmail_types", "serde", "serde_json", "serenity", @@ -3052,15 +2733,15 @@ name = "rustmail_panel" version = "0.1.0" dependencies = [ "ammonia", + "chrono-tz", "gloo-net 0.6.0", "gloo-utils 0.2.0", "i18nrs", "js-sys", - "navigator", "pulldown-cmark 0.13.0", + "rustmail_types", "serde", "serde_json", - "tailyew", "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", @@ -3069,6 +2750,14 @@ dependencies = [ "yew-router", ] +[[package]] +name = "rustmail_types" +version = "0.1.1" +dependencies = [ + "chrono-tz", + "serde", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -3322,15 +3011,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.3.0" @@ -3465,7 +3145,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.3", "hashlink", - "indexmap 2.11.4", + "indexmap", "log", "memchr", "once_cell", @@ -3772,28 +3452,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" -[[package]] -name = "tailyew" -version = "0.1.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af2a7bc870cf622199c42565c9fe3e5e5781a838f35b16d003be12890e8139f2" -dependencies = [ - "erased-serde", - "gloo 0.11.0", - "gloo-net 0.6.0", - "gloo-timers 0.3.0", - "js-sys", - "pulldown-cmark 0.13.0", - "regex", - "serde", - "serde_json", - "uuid", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "yew", -] - [[package]] name = "tempfile" version = "3.20.0" @@ -3858,15 +3516,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "time" version = "0.3.41" @@ -3936,7 +3585,6 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.0", "tokio-macros", - "tracing", "windows-sys 0.61.1", ] @@ -4038,7 +3686,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ - "indexmap 2.11.4", + "indexmap", "serde_core", "serde_spanned", "toml_datetime 0.7.3", @@ -4068,7 +3716,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.4", + "indexmap", "toml_datetime 0.6.11", "winnow 0.5.40", ] @@ -4088,56 +3736,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-stream", - "async-trait", - "axum 0.7.9", - "base64 0.22.1", - "bytes", - "h2 0.4.10", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.7.0", - "hyper-timeout", - "hyper-util", - "percent-encoding", - "pin-project", - "prost", - "socket2 0.5.9", - "tokio", - "tokio-stream", - "tower 0.4.13", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -4167,7 +3765,7 @@ dependencies = [ "http-body 1.0.1", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", ] @@ -4214,22 +3812,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "matchers", - "once_cell", - "regex-automata", - "sharded-slab", - "thread_local", - "tracing", - "tracing-core", ] [[package]] @@ -4265,12 +3847,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typemap_rev" version = "0.3.0" @@ -4381,12 +3957,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf8-width" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4401,7 +3971,6 @@ checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", "js-sys", - "serde", "wasm-bindgen", ] @@ -4411,12 +3980,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "vcpkg" version = "0.2.15" @@ -4897,14 +4460,11 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f1a03f255c70c7aa3e9c62e15292f142ede0564123543c1cc0c7a4f31660cac" dependencies = [ - "base64ct", - "bincode", "console_error_panic_hook", "futures", "gloo 0.10.0", - "html-escape", "implicit-clone", - "indexmap 2.11.4", + "indexmap", "js-sys", "prokio", "rustversion", diff --git a/Cargo.toml b/Cargo.toml index 1549bee..2e3f2b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["rustmail", "rustmail_panel"] +members = ["rustmail", "rustmail_panel", "rustmail_types"] resolver = "3" \ No newline at end of file diff --git a/rustmail/Cargo.toml b/rustmail/Cargo.toml index 15b85a1..5bac39e 100644 --- a/rustmail/Cargo.toml +++ b/rustmail/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" license = "MIT" [dependencies] +rustmail_types = { path = "../rustmail_types" } serde = { version = "*", features = ["derive"] } serenity = "0.12.4" sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite", "macros", "migrate"] } @@ -24,8 +25,6 @@ serde_json = "1.0.145" rand = "0.8.5" base64 = "0.22.1" subtle = "2.6.1" -chrono-tz = "0.10.4" -console-subscriber = "0.4.1" [dependencies.uuid] version = "1.18.1" diff --git a/rustmail_panel/Cargo.toml b/rustmail_panel/Cargo.toml index 9796784..d5d54e9 100644 --- a/rustmail_panel/Cargo.toml +++ b/rustmail_panel/Cargo.toml @@ -5,20 +5,20 @@ edition = "2024" license = "MIT" [dependencies] +rustmail_types = { path = "../rustmail_types" } yew = { version = "0.21", features = ["csr"] } yew-router = "0.18.0" wasm-bindgen-futures = "0.4.50" gloo-net = "0.6.0" gloo-utils = "0.2.0" -web-sys = "0.3.77" +web-sys = { version = "0.3.77", features = ["HtmlSelectElement"] } serde_json = "1.0.145" i18nrs = { version = "0.1.7", features = ["yew"] } -tailyew = "0.1.38" serde = { version = "1.0.228", features = ["derive"] } js-sys = "0.3.77" wasm-bindgen = "0.2.100" urlencoding = "2.1.3" ammonia = "4.1.2" pulldown-cmark = "0.13.0" -[build-dependencies] -navigator = "0.3.0" +chrono-tz = { version = "0.10", features = ["serde"] } +[build-dependencies] \ No newline at end of file diff --git a/rustmail_types/Cargo.toml b/rustmail_types/Cargo.toml new file mode 100644 index 0000000..38e1617 --- /dev/null +++ b/rustmail_types/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rustmail_types" +version = "0.1.1" +edition = "2024" +license = "MIT" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +chrono-tz = { version = "0.10", features = ["serde"] } diff --git a/rustmail_types/src/api/mod.rs b/rustmail_types/src/api/mod.rs new file mode 100644 index 0000000..60ee249 --- /dev/null +++ b/rustmail_types/src/api/mod.rs @@ -0,0 +1,14 @@ +use crate::config::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct ConfigResponse { + pub bot: BotConfig, + pub command: CommandConfig, + pub thread: ThreadConfig, + pub language: LanguageConfig, + pub error_handling: ErrorHandlingConfig, + pub notifications: NotificationsConfig, + pub reminders: ReminderConfig, + pub logs: LogsConfig, +} diff --git a/rustmail_types/src/config/bot.rs b/rustmail_types/src/config/bot.rs new file mode 100644 index 0000000..1ace0e5 --- /dev/null +++ b/rustmail_types/src/config/bot.rs @@ -0,0 +1,114 @@ +use chrono_tz::Tz; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct BotConfig { + pub token: String, + pub mode: ServerMode, + pub status: String, + pub welcome_message: String, + pub close_message: String, + pub typing_proxy_from_user: bool, + pub typing_proxy_from_staff: bool, + pub enable_logs: bool, + pub enable_features: bool, + pub enable_panel: bool, + pub client_id: u64, + pub client_secret: String, + pub redirect_url: String, + #[serde( + default = "default_timezone", + deserialize_with = "deserialize_timezone", + serialize_with = "serialize_timezone" + )] + pub timezone: Tz, + #[serde(default)] + pub logs_channel_id: Option, + #[serde(default)] + pub features_channel_id: Option, + #[serde(default)] + pub ip: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum ServerMode { + Single { guild_id: u64 }, + Dual { + community_guild_id: u64, + staff_guild_id: u64, + }, +} + +impl BotConfig { + pub fn get_community_guild_id(&self) -> u64 { + match &self.mode { + ServerMode::Single { guild_id } => *guild_id, + ServerMode::Dual { community_guild_id, .. } => *community_guild_id, + } + } + + pub fn get_staff_guild_id(&self) -> u64 { + match &self.mode { + ServerMode::Single { guild_id } => *guild_id, + ServerMode::Dual { staff_guild_id, .. } => *staff_guild_id, + } + } + + pub fn is_dual_mode(&self) -> bool { + matches!(self.mode, ServerMode::Dual { .. }) + } + + pub fn validate_logs_config(&self) -> Result<(), String> { + match (self.enable_logs, self.logs_channel_id) { + (true, None) => { + Err("'logs_channel_id' field is required if 'enable_logs' is true".to_string()) + } + (false, Some(_)) => { + Err("'logs_channel_id' must not be filled in if 'enable_logs' is false".to_string()) + } + (true, Some(_)) => Ok(()), + (false, None) => Ok(()), + } + } + + pub fn validate_features_config(&self) -> Result<(), String> { + match (self.enable_features, self.features_channel_id) { + (true, None) => Err( + "'features_channel_id' field is required if 'enable_features' is true".to_string(), + ), + (false, Some(_)) => Err( + "'features_channel_id' must not be filled in if 'enable_features' is false" + .to_string(), + ), + (true, Some(_)) => Ok(()), + (false, None) => Ok(()), + } + } + + pub fn is_community_guild(&self, guild_id: u64) -> bool { + match &self.mode { + ServerMode::Single { guild_id: gid } => *gid == guild_id, + ServerMode::Dual { community_guild_id, .. } => *community_guild_id == guild_id, + } + } +} + +fn deserialize_timezone<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + s.parse::().map_err(serde::de::Error::custom) +} + +fn serialize_timezone(tz: &Tz, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&tz.to_string()) +} + +fn default_timezone() -> Tz { + chrono_tz::UTC +} diff --git a/rustmail_types/src/config/commands.rs b/rustmail_types/src/config/commands.rs new file mode 100644 index 0000000..ebd67d8 --- /dev/null +++ b/rustmail_types/src/config/commands.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct CommandConfig { + pub prefix: String, +} + +impl Default for CommandConfig { + fn default() -> Self { + Self { + prefix: "!".to_string(), + } + } +} diff --git a/rustmail_types/src/config/error_handling.rs b/rustmail_types/src/config/error_handling.rs new file mode 100644 index 0000000..afb4e4d --- /dev/null +++ b/rustmail_types/src/config/error_handling.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ErrorHandlingConfig { + pub show_detailed_errors: bool, + pub log_errors: bool, + pub send_error_embeds: bool, + pub auto_delete_error_messages: bool, + pub error_message_ttl: Option, + #[serde(default = "default_display_errors")] + pub display_errors: bool, +} + +fn default_display_errors() -> bool { + true +} + +impl Default for ErrorHandlingConfig { + fn default() -> Self { + Self { + show_detailed_errors: true, + log_errors: true, + send_error_embeds: true, + auto_delete_error_messages: false, + error_message_ttl: None, + display_errors: true, + } + } +} diff --git a/rustmail_types/src/config/languages.rs b/rustmail_types/src/config/languages.rs new file mode 100644 index 0000000..8066056 --- /dev/null +++ b/rustmail_types/src/config/languages.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct LanguageConfig { + pub default_language: String, + pub fallback_language: String, + pub supported_languages: Vec, +} + +impl Default for LanguageConfig { + fn default() -> Self { + Self { + default_language: "en".to_string(), + fallback_language: "en".to_string(), + supported_languages: vec!["en".to_string(), "fr".to_string()], + } + } +} diff --git a/rustmail_types/src/config/logs.rs b/rustmail_types/src/config/logs.rs new file mode 100644 index 0000000..5cf5d3d --- /dev/null +++ b/rustmail_types/src/config/logs.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct LogsConfig { + pub show_log_on_edit: bool, + pub show_log_on_delete: bool, +} + +impl Default for LogsConfig { + fn default() -> Self { + Self { + show_log_on_edit: true, + show_log_on_delete: true, + } + } +} diff --git a/rustmail_types/src/config/mod.rs b/rustmail_types/src/config/mod.rs new file mode 100644 index 0000000..49143a4 --- /dev/null +++ b/rustmail_types/src/config/mod.rs @@ -0,0 +1,17 @@ +mod bot; +mod commands; +mod threads; +mod languages; +mod notifications; +mod error_handling; +mod logs; +mod reminders; + +pub use bot::{BotConfig, ServerMode}; +pub use commands::CommandConfig; +pub use threads::ThreadConfig; +pub use languages::LanguageConfig; +pub use notifications::NotificationsConfig; +pub use error_handling::ErrorHandlingConfig; +pub use logs::LogsConfig; +pub use reminders::ReminderConfig; diff --git a/rustmail_types/src/config/notifications.rs b/rustmail_types/src/config/notifications.rs new file mode 100644 index 0000000..773fd36 --- /dev/null +++ b/rustmail_types/src/config/notifications.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct NotificationsConfig { + pub show_success_on_edit: bool, + pub show_partial_success_on_edit: bool, + pub show_failure_on_edit: bool, + pub show_success_on_reply: bool, + pub show_success_on_delete: bool, + #[serde(default = "default_show_success")] + pub show_success: bool, + #[serde(default = "default_show_error")] + pub show_error: bool, +} + +fn default_show_success() -> bool { + true +} + +fn default_show_error() -> bool { + true +} + +impl Default for NotificationsConfig { + fn default() -> Self { + Self { + show_success_on_edit: true, + show_partial_success_on_edit: true, + show_failure_on_edit: true, + show_success_on_reply: true, + show_success_on_delete: true, + show_success: true, + show_error: true, + } + } +} diff --git a/rustmail_types/src/config/reminders.rs b/rustmail_types/src/config/reminders.rs new file mode 100644 index 0000000..90340a7 --- /dev/null +++ b/rustmail_types/src/config/reminders.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ReminderConfig { + pub embed_color: String, +} + +impl Default for ReminderConfig { + fn default() -> Self { + Self { + embed_color: "ffcc00".to_string(), + } + } +} diff --git a/rustmail_types/src/config/threads.rs b/rustmail_types/src/config/threads.rs new file mode 100644 index 0000000..c7853fd --- /dev/null +++ b/rustmail_types/src/config/threads.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct ThreadConfig { + pub inbox_category_id: u64, + pub embedded_message: bool, + pub user_message_color: String, + pub staff_message_color: String, + pub system_message_color: String, + pub block_quote: bool, + pub time_to_close_thread: u64, + pub create_ticket_by_create_channel: bool, + #[serde(default = "default_close_on_leave")] + pub close_on_leave: bool, + #[serde(default = "default_auto_archive_duration")] + pub auto_archive_duration: u16, +} + +fn default_close_on_leave() -> bool { + false +} + +fn default_auto_archive_duration() -> u16 { + 10080 +} + +impl Default for ThreadConfig { + fn default() -> Self { + Self { + inbox_category_id: 0, + embedded_message: false, + user_message_color: "5865f2".to_string(), + staff_message_color: "57f287".to_string(), + system_message_color: "faa81a".to_string(), + block_quote: false, + time_to_close_thread: 0, + create_ticket_by_create_channel: false, + close_on_leave: false, + auto_archive_duration: 10080, + } + } +} diff --git a/rustmail_types/src/lib.rs b/rustmail_types/src/lib.rs new file mode 100644 index 0000000..0adf1a0 --- /dev/null +++ b/rustmail_types/src/lib.rs @@ -0,0 +1,5 @@ +pub mod config; +pub mod api; + +pub use config::*; +pub use api::*; From 7511e877fffeaf18475eef9d46a8beb5d646018d Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 14 Nov 2025 23:25:42 +0100 Subject: [PATCH 094/100] feat(api): add api route for update rustmail config file --- rustmail/src/api/handler/bot/config.rs | 164 +++++++++++++++++++++++++ rustmail/src/api/handler/bot/mod.rs | 2 + rustmail/src/api/routes/bot.rs | 2 + rustmail/src/commands/edit/common.rs | 2 + 4 files changed, 170 insertions(+) create mode 100644 rustmail/src/api/handler/bot/config.rs diff --git a/rustmail/src/api/handler/bot/config.rs b/rustmail/src/api/handler/bot/config.rs new file mode 100644 index 0000000..dd1f191 --- /dev/null +++ b/rustmail/src/api/handler/bot/config.rs @@ -0,0 +1,164 @@ +use crate::config::{load_config, Config, LanguageConfigExt}; +use crate::prelude::types::*; +use axum::extract::State; +use axum::http::StatusCode; +use axum::Json; +use rustmail_types::ConfigResponse; +use std::fs; +use std::sync::Arc; +use tokio::sync::Mutex; + +fn mask_secret(secret: &str) -> String { + let len = secret.chars().count(); + if len <= 8 { + "*".repeat(len) + } else { + let chars: Vec = secret.chars().collect(); + let start: String = chars.iter().take(4).collect(); + let end: String = chars.iter().skip(len - 4).collect(); + format!("{}...{}", start, end) + } +} + +pub async fn handle_get_config( + State(bot_state): State>>, +) -> Result, StatusCode> { + let state = bot_state.lock().await; + + let config = match &state.config { + Some(c) => c, + None => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + + let mut masked_bot = config.bot.clone(); + masked_bot.token = mask_secret(&config.bot.token); + masked_bot.client_secret = mask_secret(&config.bot.client_secret); + + let response = ConfigResponse { + bot: masked_bot, + command: config.command.clone(), + thread: config.thread.clone(), + language: config.language.clone(), + error_handling: config.error_handling.clone(), + notifications: config.notifications.clone(), + reminders: config.reminders.clone(), + logs: config.logs.clone(), + }; + + Ok(Json(response)) +} + +pub async fn handle_update_config( + State(bot_state): State>>, + Json(update): Json, +) -> Result, (StatusCode, String)> { + let current_config = { + let state = bot_state.lock().await; + match &state.config { + Some(c) => c.clone(), + None => { + return Err(( + StatusCode::INTERNAL_SERVER_ERROR, + "Configuration not loaded".to_string(), + )) + } + } + }; + + let mut new_bot_config = update.bot.clone(); + + if new_bot_config.token.contains("...") { + new_bot_config.token = current_config.bot.token.clone(); + } + + if new_bot_config.client_secret.contains("...") { + new_bot_config.client_secret = current_config.bot.client_secret.clone(); + } + + new_bot_config.ip = current_config.bot.ip.clone(); + + let new_config = Config { + bot: new_bot_config, + command: update.command, + thread: update.thread, + language: update.language, + error_handling: update.error_handling, + notifications: update.notifications, + reminders: update.reminders, + logs: update.logs, + db_pool: None, + error_handler: None, + thread_locks: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())), + }; + + if let Err(e) = validate_config(&new_config) { + return Err((StatusCode::BAD_REQUEST, e)); + } + + if let Err(e) = save_config_with_backup(&new_config, "config.toml").await { + return Err((StatusCode::INTERNAL_SERVER_ERROR, e)); + } + + let mut state = bot_state.lock().await; + state.config = load_config("config.toml"); + + Ok(Json(serde_json::json!({ + "success": true, + "message": "Configuration saved successfully. Restart the bot to apply changes." + }))) +} + +fn validate_config(config: &Config) -> Result<(), String> { + if u64::from_str_radix(&config.thread.user_message_color, 16).is_err() { + return Err("Invalid user message color format (must be hex)".to_string()); + } + + if u64::from_str_radix(&config.thread.staff_message_color, 16).is_err() { + return Err("Invalid staff message color format (must be hex)".to_string()); + } + + if u64::from_str_radix(&config.reminders.embed_color, 16).is_err() { + return Err("Invalid reminder embed color format (must be hex)".to_string()); + } + + config.bot.validate_logs_config()?; + config.bot.validate_features_config()?; + + if !config + .language + .is_language_supported(config.language.get_default_language()) + { + return Err(format!( + "Default language '{}' is not in supported languages list", + config.language.default_language + )); + } + + Ok(()) +} + +async fn save_config_with_backup(config: &Config, path: &str) -> Result<(), String> { + if std::path::Path::new(path).exists() { + let backup_path = format!("{}.backup", path); + fs::copy(path, &backup_path) + .map_err(|e| format!("Failed to create backup: {}", e))?; + } + + let config_response = ConfigResponse { + bot: config.bot.clone(), + command: config.command.clone(), + thread: config.thread.clone(), + language: config.language.clone(), + error_handling: config.error_handling.clone(), + notifications: config.notifications.clone(), + reminders: config.reminders.clone(), + logs: config.logs.clone(), + }; + + let toml_content = toml::to_string_pretty(&config_response) + .map_err(|e| format!("Failed to serialize config: {}", e))?; + + fs::write(path, toml_content).map_err(|e| format!("Failed to write config file: {}", e))?; + + Ok(()) +} diff --git a/rustmail/src/api/handler/bot/mod.rs b/rustmail/src/api/handler/bot/mod.rs index 0c63b22..ff3c0ce 100644 --- a/rustmail/src/api/handler/bot/mod.rs +++ b/rustmail/src/api/handler/bot/mod.rs @@ -1,9 +1,11 @@ +pub mod config; pub mod restart; pub mod start; pub mod status; pub mod stop; pub mod tickets; +pub use config::*; pub use restart::*; pub use start::*; pub use status::*; diff --git a/rustmail/src/api/routes/bot.rs b/rustmail/src/api/routes/bot.rs index 58ccefc..f974af2 100644 --- a/rustmail/src/api/routes/bot.rs +++ b/rustmail/src/api/routes/bot.rs @@ -12,6 +12,8 @@ pub fn create_bot_router(bot_state: Arc>) -> Router Date: Fri, 14 Nov 2025 23:26:22 +0100 Subject: [PATCH 095/100] refactor(config): rustmail config now use shared rustmail types --- rustmail/src/config.rs | 340 +++++++---------------------------------- 1 file changed, 56 insertions(+), 284 deletions(-) diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index d76b1e5..d630a8b 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -1,7 +1,5 @@ use crate::prelude::errors::*; use crate::prelude::i18n::*; -use chrono_tz::Tz; -use serde::Deserialize; use serenity::all::GuildId; use serenity::http::Http; use sqlx::SqlitePool; @@ -9,7 +7,8 @@ use std::fs; use std::net::UdpSocket; use std::sync::Arc; -#[derive(Debug, Deserialize, Clone)] +pub use rustmail_types::*; +#[derive(Debug, Clone)] pub struct Config { pub bot: BotConfig, pub command: CommandConfig, @@ -19,124 +18,14 @@ pub struct Config { pub notifications: NotificationsConfig, pub reminders: ReminderConfig, pub logs: LogsConfig, - #[serde(skip)] + pub db_pool: Option, - #[serde(skip)] pub error_handler: Option>, - #[serde(skip)] - pub thread_locks: - Arc>>>>, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct BotConfig { - pub token: String, - pub mode: ServerMode, - pub status: String, - pub welcome_message: String, - pub close_message: String, - pub typing_proxy_from_user: bool, - pub typing_proxy_from_staff: bool, - pub enable_logs: bool, - pub enable_features: bool, - pub enable_panel: bool, - pub client_id: u64, - pub client_secret: String, - pub redirect_url: String, - #[serde( - default = "default_timezone", - deserialize_with = "deserialize_timezone" - )] - pub timezone: Tz, - #[serde(default)] - pub logs_channel_id: Option, - #[serde(default)] - pub features_channel_id: Option, - #[serde(default)] - pub ip: Option, -} - -fn deserialize_timezone<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - s.parse::().map_err(serde::de::Error::custom) -} - -fn default_timezone() -> Tz { - chrono_tz::UTC -} - -#[derive(Debug, Deserialize, Clone)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ServerMode { - Single { - guild_id: u64, - }, - Dual { - community_guild_id: u64, - staff_guild_id: u64, - }, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct CommandConfig { - pub prefix: String, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct ThreadConfig { - pub inbox_category_id: u64, - pub embedded_message: bool, - pub user_message_color: String, - pub staff_message_color: String, - pub system_message_color: String, - pub block_quote: bool, - pub time_to_close_thread: u64, - pub create_ticket_by_create_channel: bool, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct NotificationsConfig { - pub show_success_on_edit: bool, - pub show_partial_success_on_edit: bool, - pub show_failure_on_edit: bool, - pub show_success_on_reply: bool, - pub show_success_on_delete: bool, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct LogsConfig { - pub show_log_on_edit: bool, - pub show_log_on_delete: bool, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct ReminderConfig { - pub embed_color: String, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct LanguageConfig { - pub default_language: String, - pub fallback_language: String, - pub supported_languages: Vec, - pub error_message_ttl: Option, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct ErrorHandlingConfig { - pub show_detailed_errors: bool, - pub log_errors: bool, - pub send_error_embeds: bool, - pub auto_delete_error_messages: bool, - pub error_message_ttl: Option, + pub thread_locks: Arc>>>>, } fn get_local_ip() -> Option { let socket = UdpSocket::bind("0.0.0.0:0").ok()?; - socket.connect("1.1.1.1:80").ok()?; Some(socket.local_addr().ok()?.ip().to_string()) } @@ -147,7 +36,7 @@ pub fn load_config(path: &str) -> Option { Err(_) => return None, }; - let mut config: Config = match toml::from_str(&content) { + let config_response: ConfigResponse = match toml::from_str(&content) { Ok(c) => c, Err(e) => { eprintln!("Failed to parse config.toml: {}", e); @@ -155,213 +44,96 @@ pub fn load_config(path: &str) -> Option { } }; - if config.bot.ip.is_none() { - config.bot.ip = get_local_ip(); + let mut bot = config_response.bot; + if bot.ip.is_none() { + bot.ip = get_local_ip(); } - match u64::from_str_radix(&config.thread.user_message_color, 16) { - Ok(_) => {} - Err(_) => { - eprintln!( - "Incorect user message color in the config.toml! Please put a color in hex format!" - ); - return None; - } + if u64::from_str_radix(&config_response.thread.user_message_color, 16).is_err() { + eprintln!("Incorrect user message color in the config.toml! Please put a color in hex format!"); + return None; } - match u64::from_str_radix(&config.thread.staff_message_color, 16) { - Ok(_) => {} - Err(_) => { - eprintln!( - "Incorect staff message color in the config.toml! Please put a color in hex format!" - ); - return None; - } + if u64::from_str_radix(&config_response.thread.staff_message_color, 16).is_err() { + eprintln!("Incorrect staff message color in the config.toml! Please put a color in hex format!"); + return None; } - match u64::from_str_radix(&config.reminders.embed_color, 16) { - Ok(_) => {} - Err(_) => { - eprintln!( - "Incorect reminder embed color in the config.toml! Please put a color in hex format!" - ); - return None; - } + if u64::from_str_radix(&config_response.reminders.embed_color, 16).is_err() { + eprintln!("Incorrect reminder embed color in the config.toml! Please put a color in hex format!"); + return None; } - if !config - .language - .is_language_supported(config.language.get_default_language()) - { + if !config_response.language.is_language_supported(config_response.language.get_default_language()) { eprintln!( "Warning: Default language '{}' is not in supported languages list", - config.language.default_language + config_response.language.default_language ); } - match config.bot.validate_logs_config() { - Ok(_) => {} - Err(e) => { - eprintln!("Invalid logs configuration: {}", e); - return None; - } + if let Err(e) = bot.validate_logs_config() { + eprintln!("Invalid logs configuration: {}", e); + return None; } - match config.bot.validate_features_config() { - Ok(_) => {} - Err(e) => { - eprintln!("Invalid features configuration: {}", e); - return None; - } + if let Err(e) = bot.validate_features_config() { + eprintln!("Invalid features configuration: {}", e); + return None; } - let default_lang = config.language.get_default_language(); - let fallback_lang = config.language.get_fallback_language(); + let default_lang = config_response.language.get_default_language(); + let fallback_lang = config_response.language.get_fallback_language(); let error_handler = Arc::new(ErrorHandler::with_languages(default_lang, fallback_lang)); - config.error_handler = Some(error_handler); - config.thread_locks = Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())); - - Some(config) -} - -impl Default for LanguageConfig { - fn default() -> Self { - Self { - default_language: "en".to_string(), - fallback_language: "en".to_string(), - supported_languages: vec!["en".to_string(), "fr".to_string()], - error_message_ttl: Some(30), - } - } -} - -impl Default for ReminderConfig { - fn default() -> Self { - Self { - embed_color: "ffcc00".to_string(), - } - } -} - -impl Default for LogsConfig { - fn default() -> Self { - Self { - show_log_on_edit: true, - show_log_on_delete: true, - } - } -} - -impl Default for ErrorHandlingConfig { - fn default() -> Self { - Self { - show_detailed_errors: false, - log_errors: true, - send_error_embeds: true, - auto_delete_error_messages: true, - error_message_ttl: Some(30), - } - } -} - -impl Default for NotificationsConfig { - fn default() -> Self { - Self { - show_success_on_edit: true, - show_partial_success_on_edit: true, - show_failure_on_edit: true, - show_success_on_reply: true, - show_success_on_delete: true, - } - } -} - -impl LanguageConfig { - pub fn get_default_language(&self) -> Language { + Some(Config { + bot, + command: config_response.command, + thread: config_response.thread, + language: config_response.language, + error_handling: config_response.error_handling, + notifications: config_response.notifications, + reminders: config_response.reminders, + logs: config_response.logs, + db_pool: None, + error_handler: Some(error_handler), + thread_locks: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())), + }) +} + +pub trait LanguageConfigExt { + fn get_default_language(&self) -> Language; + fn get_fallback_language(&self) -> Language; + fn get_supported_languages(&self) -> Vec; + fn is_language_supported(&self, language: Language) -> bool; +} + +impl LanguageConfigExt for LanguageConfig { + fn get_default_language(&self) -> Language { Language::from_str(&self.default_language).unwrap_or(Language::English) } - pub fn get_fallback_language(&self) -> Language { + fn get_fallback_language(&self) -> Language { Language::from_str(&self.fallback_language).unwrap_or(Language::English) } - pub fn get_supported_languages(&self) -> Vec { + fn get_supported_languages(&self) -> Vec { self.supported_languages .iter() .filter_map(|s| Language::from_str(s)) .collect() } - pub fn is_language_supported(&self, language: Language) -> bool { + fn is_language_supported(&self, language: Language) -> bool { self.get_supported_languages().contains(&language) } } -impl BotConfig { - pub fn get_community_guild_id(&self) -> u64 { - match &self.mode { - ServerMode::Single { guild_id } => *guild_id, - ServerMode::Dual { - community_guild_id, .. - } => *community_guild_id, - } - } - - pub fn get_staff_guild_id(&self) -> u64 { - match &self.mode { - ServerMode::Single { guild_id } => *guild_id, - ServerMode::Dual { staff_guild_id, .. } => *staff_guild_id, - } - } - - pub fn is_dual_mode(&self) -> bool { - matches!(self.mode, ServerMode::Dual { .. }) - } - - pub fn validate_logs_config(&self) -> Result<(), String> { - match (self.enable_logs, self.logs_channel_id) { - (true, None) => { - Err("'logs_channel_id' field is required if 'enable_logs' is true".to_string()) - } - (false, Some(_)) => { - Err("'logs_channel_id' must not be filled in if 'enable_logs' is false".to_string()) - } - (true, Some(_)) => Ok(()), - (false, None) => Ok(()), - } - } - - pub fn validate_features_config(&self) -> Result<(), String> { - match (self.enable_features, self.features_channel_id) { - (true, None) => Err( - "'features_channel_id' field is required if 'enable_features' is true".to_string(), - ), - (false, Some(_)) => Err( - "'features_channel_id' must not be filled in if 'enable_features' is false" - .to_string(), - ), - (true, Some(_)) => Ok(()), - (false, None) => Ok(()), - } - } - - pub fn is_community_guild(&self, p0: u64) -> bool { - match &self.mode { - ServerMode::Single { guild_id } => *guild_id == p0, - ServerMode::Dual { - community_guild_id, .. - } => *community_guild_id == p0, - } - } -} - impl Config { pub async fn validate_servers(&self, http: &Http) -> Result<(), String> { match &self.bot.mode { ServerMode::Single { guild_id } => { let guild_id = GuildId::new(*guild_id); - if let Err(_) = guild_id.to_partial_guild(http).await { + if guild_id.to_partial_guild(http).await.is_err() { return Err(format!("Serveur principal introuvable: {}", guild_id)); } } @@ -380,7 +152,7 @@ impl Config { )); } - if let Err(_) = staff_guild_id.to_partial_guild(http).await { + if staff_guild_id.to_partial_guild(http).await.is_err() { return Err(format!("Serveur staff introuvable: {}", staff_guild_id)); } From 03edd8f69022ca0c4924bc1eed4c4fbfe64102fb Mon Sep 17 00:00:00 2001 From: Akinator31 Date: Fri, 14 Nov 2025 23:27:21 +0100 Subject: [PATCH 096/100] feat(config): add complete panel's configuration page --- .../src/components/configuration.rs | 1381 +++++++++++++++-- 1 file changed, 1226 insertions(+), 155 deletions(-) diff --git a/rustmail_panel/src/components/configuration.rs b/rustmail_panel/src/components/configuration.rs index 933d487..a8ca7fe 100644 --- a/rustmail_panel/src/components/configuration.rs +++ b/rustmail_panel/src/components/configuration.rs @@ -3,12 +3,20 @@ use i18nrs::yew::use_translation; use wasm_bindgen_futures::spawn_local; use yew::prelude::*; +use rustmail_types::*; + #[function_component(ConfigurationPage)] pub fn configuration_page() -> Html { let (i18n, _set_language) = use_translation(); let bot_status = use_state(|| "running".to_string()); let is_loading = use_state(|| false); + let config = use_state(|| None::); + let config_loading = use_state(|| true); + let show_restart_modal = use_state(|| false); + let save_message = use_state(|| None::<(bool, String)>); + + let expanded_sections = use_state(|| vec![true, false, false, false, false, false, false, false]); { let bot_status = bot_status.clone(); @@ -17,7 +25,6 @@ pub fn configuration_page() -> Html { use_effect_with((), move |_| { spawn_local(async move { is_loading.set(true); - if let Ok(resp) = Request::get("/api/bot/status").send().await { if resp.ok() { if let Ok(json) = resp.json::().await { @@ -27,21 +34,31 @@ pub fn configuration_page() -> Html { } } } - is_loading.set(false); }); - || () }); } - let bot_token = use_state(|| "".to_string()); - let guild_id = use_state(|| "".to_string()); - let ticket_category = use_state(|| "".to_string()); - let log_channel = use_state(|| "".to_string()); - let support_role = use_state(|| "".to_string()); - let auto_close = use_state(|| "".to_string()); - let enable_transcripts = use_state(|| false); + { + let config = config.clone(); + let config_loading = config_loading.clone(); + + use_effect_with((), move |_| { + spawn_local(async move { + config_loading.set(true); + if let Ok(resp) = Request::get("/api/bot/config").send().await { + if resp.ok() { + if let Ok(config_data) = resp.json::().await { + config.set(Some(config_data)); + } + } + } + config_loading.set(false); + }); + || () + }); + } let handle_bot_action = { let bot_status = bot_status.clone(); @@ -53,7 +70,6 @@ pub fn configuration_page() -> Html { spawn_local(async move { is_loading.set(true); - let url = format!("/api/bot/{}", action); if let Ok(resp) = Request::post(&url).send().await { if resp.ok() { @@ -64,43 +80,66 @@ pub fn configuration_page() -> Html { } } } - is_loading.set(false); }); }) }; let handle_save = { - let bot_token = bot_token.clone(); - let guild_id = guild_id.clone(); - let ticket_category = ticket_category.clone(); - let log_channel = log_channel.clone(); - let support_role = support_role.clone(); - let auto_close = auto_close.clone(); - let enable_transcripts = enable_transcripts.clone(); - - Callback::from(move |_| { - let data = serde_json::json!({ - "bot_token": *bot_token, - "guild_id": *guild_id, - "ticket_category": *ticket_category, - "log_channel": *log_channel, - "support_role": *support_role, - "auto_close": *auto_close, - "enable_transcripts": *enable_transcripts, - }); + let show_restart_modal = show_restart_modal.clone(); + let save_message = save_message.clone(); + let i18n = i18n.clone(); + + Callback::from(move |new_config: ConfigResponse| { + let show_restart_modal = show_restart_modal.clone(); + let save_message = save_message.clone(); + let i18n = i18n.clone(); spawn_local(async move { - let _ = Request::post("/api/bot/config") - .header("Content-Type", "application/json") - .body(serde_json::to_string(&data).unwrap()) - .expect("REASON") - .send() - .await; + match Request::put("/api/bot/config").json(&new_config) { + Ok(req) => match req.send().await { + Ok(resp) => { + if resp.ok() { + save_message.set(Some((true, i18n.t("panel.configuration.save_success")))); + show_restart_modal.set(true); + } else { + let error_msg = resp.text().await.unwrap_or_else(|_| "Erreur inconnue".to_string()); + save_message.set(Some((false, error_msg))); + } + } + Err(e) => { + save_message.set(Some((false, format!("Erreur réseau : {:?}", e)))); + } + }, + Err(e) => { + save_message.set(Some((false, format!("Erreur : {:?}", e)))); + } + } }); }) }; + let handle_close_modal = { + let show_restart_modal = show_restart_modal.clone(); + let handle_bot_action = handle_bot_action.clone(); + + Callback::from(move |should_restart: bool| { + show_restart_modal.set(false); + if should_restart { + handle_bot_action.emit("restart".to_string()); + } + }) + }; + + let toggle_section = { + let expanded_sections = expanded_sections.clone(); + Callback::from(move |index: usize| { + let mut sections = (*expanded_sections).clone(); + sections[index] = !sections[index]; + expanded_sections.set(sections); + }) + }; + html! {
@@ -116,7 +155,6 @@ pub fn configuration_page() -> Html {

{i18n.t("panel.configuration.bot_status")}

{i18n.t("panel.configuration.bot_status_description")}

-
Html {

{i18n.t("panel.configuration.config_file.title")}

-
+ { + if let Some((is_success, message)) = (*save_message).clone() { + html! { +
+ {message} +
+ } + } else { + html! {} + } + } -
- - ().value()); - } - }} - class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -

{"Le token de votre bot Discord"}

-
+ { + if *config_loading { + html! { +
+
{i18n.t("panel.configuration.loading")}
+
+ } + } else if let Some(cfg) = (*config).clone() { + html! { + + } + } else { + html! { +
+ {i18n.t("panel.configuration.load_error")} +
+ } + } + } +
-
- - ().value()); - } - }} - class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -

{"L'ID de votre serveur Discord"}

-
+ { + if *show_restart_modal { + html! { +
+
+

{i18n.t("panel.configuration.restart_modal.title")}

+

+ {i18n.t("panel.configuration.restart_modal.message")} +

+
+ + +
+
+
+ } + } else { + html! {} + } + } -
- - ().value()); - } - }} - class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -

{"La catégorie où les tickets seront créés"}

+
+
+ } +} + +#[derive(Properties, PartialEq)] +struct ConfigFormProps { + config: ConfigResponse, + on_save: Callback, + expanded_sections: Vec, + on_toggle_section: Callback, +} + +#[function_component(ConfigForm)] +fn config_form(props: &ConfigFormProps) -> Html { + let (i18n, _set_language) = use_translation(); + let config = use_state(|| props.config.clone()); + + html! { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +

+ {i18n.t("panel.configuration.save_help")} +

+
+
+ } +} + +#[derive(Properties, PartialEq)] +struct AccordionSectionProps { + title: String, + is_expanded: bool, + on_toggle: Callback<()>, + children: Children, +} + +#[function_component(AccordionSection)] +fn accordion_section(props: &AccordionSectionProps) -> Html { + html! { +
+ + + { + if props.is_expanded { + html! { +
+ {for props.children.iter()}
+ } + } else { + html! {} + } + } +
+ } +} -
- - ().value()); +#[derive(Properties, PartialEq)] +struct TextInputProps { + label: String, + value: String, + on_change: Callback, + #[prop_or_default] + input_type: Option, + #[prop_or_default] + placeholder: Option, + #[prop_or_default] + help: Option, +} + +#[function_component(TextInput)] +fn text_input(props: &TextInputProps) -> Html { + let input_type = props.input_type.clone().unwrap_or_else(|| "text".to_string()); + let placeholder = props.placeholder.clone().unwrap_or_default(); + + html! { +
+ + () { + on_change.emit(input.value()); + } + } + }} + class="w-full px-4 py-2 bg-slate-900/50 border border-slate-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + { + if let Some(help) = &props.help { + html! {

{help}

} + } else { + html! {} + } + } +
+ } +} + +#[derive(Properties, PartialEq)] +struct TextAreaInputProps { + label: String, + value: String, + on_change: Callback, + #[prop_or(3)] + rows: u32, +} + +#[function_component(TextAreaInput)] +fn textarea_input(props: &TextAreaInputProps) -> Html { + html! { +
+ +