diff --git a/rustmail/src/bot.rs b/rustmail/src/bot.rs index ce6f18b9..96c5b1eb 100644 --- a/rustmail/src/bot.rs +++ b/rustmail/src/bot.rs @@ -7,8 +7,9 @@ use crate::prelude::panel_commands::*; use crate::prelude::types::*; use base64::Engine; use rand::RngCore; -use serenity::all::{ClientBuilder, GatewayIntents}; +use serenity::all::{ClientBuilder, GatewayIntents, ShardManager}; use serenity::cache::Settings as CacheSettings; +use serenity::prelude::TypeMapKey; use std::collections::HashMap; use std::process; use std::sync::Arc; @@ -16,12 +17,18 @@ use std::time::Duration; use tokio::sync::Mutex; use tokio::{select, spawn}; +pub struct ShardManagerKey; + +impl TypeMapKey for ShardManagerKey { + type Value = Arc; +} + pub async fn init_bot_state() -> Arc> { let pool = init_database().await.expect("An error occured!"); println!("Database connected!"); let mut bytes = [0u8; 32]; - rand::thread_rng().fill_bytes(&mut bytes); + rand::rng().fill_bytes(&mut bytes); let token = base64::engine::general_purpose::STANDARD.encode(&bytes); let config = load_config("config.toml"); @@ -136,6 +143,7 @@ pub async fn run_bot( registry.register_command(LogsCommand); registry.register_command(TakeCommand); registry.register_command(ReleaseCommand); + registry.register_command(PingCommand); let registry = Arc::new(registry); @@ -174,6 +182,11 @@ pub async fn run_bot( state_lock.bot_http = Some(client.http.clone()); } + { + let mut data = client.data.write().await; + data.insert::(client.shard_manager.clone()); + } + if let Err(e) = config.validate_servers(&client.http).await { eprintln!("Configuration validation error: {}", e); eprintln!( diff --git a/rustmail/src/commands/mod.rs b/rustmail/src/commands/mod.rs index 273a77e3..c55f34b8 100644 --- a/rustmail/src/commands/mod.rs +++ b/rustmail/src/commands/mod.rs @@ -22,6 +22,7 @@ pub mod id; pub mod logs; pub mod move_thread; pub mod new_thread; +pub mod ping; pub mod recover; pub mod release; pub mod remove_reminder; @@ -42,6 +43,7 @@ pub use id::*; pub use logs::*; pub use move_thread::*; pub use new_thread::*; +pub use ping::*; pub use recover::*; pub use release::*; pub use remove_reminder::*; diff --git a/rustmail/src/commands/ping/mod.rs b/rustmail/src/commands/ping/mod.rs new file mode 100644 index 00000000..9317acfa --- /dev/null +++ b/rustmail/src/commands/ping/mod.rs @@ -0,0 +1,5 @@ +mod slash_command; +mod text_command; + +pub use slash_command::*; +pub use text_command::*; diff --git a/rustmail/src/commands/ping/slash_command/mod.rs b/rustmail/src/commands/ping/slash_command/mod.rs new file mode 100644 index 00000000..2df3c1c3 --- /dev/null +++ b/rustmail/src/commands/ping/slash_command/mod.rs @@ -0,0 +1,3 @@ +pub mod ping; + +pub use ping::*; diff --git a/rustmail/src/commands/ping/slash_command/ping.rs b/rustmail/src/commands/ping/slash_command/ping.rs new file mode 100644 index 00000000..d2197f46 --- /dev/null +++ b/rustmail/src/commands/ping/slash_command/ping.rs @@ -0,0 +1,121 @@ +use crate::bot::ShardManagerKey; +use crate::commands::{BoxFuture, CommunityRegistrable, RegistrableCommand}; +use crate::config::Config; +use crate::errors::{DiscordError, ModmailError, ModmailResult}; +use crate::handlers::InteractionHandler; +use crate::i18n::get_translated_message; +use crate::utils::{MessageBuilder, defer_response}; +use chrono::Utc; +use serenity::FutureExt; +use serenity::all::{CommandInteraction, Context, CreateCommand, ResolvedOption}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use tokio::time::Instant; + +pub struct PingCommand; + +#[async_trait::async_trait] +impl RegistrableCommand for PingCommand { + fn as_community(&self) -> Option<&dyn CommunityRegistrable> { + None + } + + fn name(&self) -> &'static str { + "ping" + } + + fn doc<'a>(&self, config: &'a Config) -> BoxFuture<'a, String> { + async move { get_translated_message(config, "help.ping", 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.ping_command_desc", + 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 { + defer_response(&ctx, &command).await?; + let response = MessageBuilder::system_message(&ctx, &config) + .content("...") + .to_channel(command.channel_id) + .build_interaction_message_followup() + .await; + + let shard_manager = ctx + .data + .read() + .await + .get::() + .cloned() + .ok_or(ModmailError::Discord(DiscordError::ShardManagerNotFound))?; + + let time_before = Instant::now(); + let mut res = command.create_followup(&ctx.http, response).await?; + let msg_send_ping = time_before.elapsed().as_millis(); + + let gateway_ping = { + let runners = shard_manager.runners.lock().await; + runners.get(&ctx.shard_id).and_then(|runner| runner.latency) + }; + + let start = Instant::now(); + ctx.http.get_gateway().await?; + let api_ping = start.elapsed(); + + let mut params = HashMap::new(); + params.insert( + "gateway_latency".to_string(), + format!( + "{:?}", + gateway_ping.unwrap_or(Duration::default()).as_millis() + ), + ); + params.insert( + "api_latency".to_string(), + format!("{:?}", api_ping.as_millis()), + ); + params.insert( + "message_latency".to_string(), + format!("{:?}", msg_send_ping), + ); + + let response = MessageBuilder::system_message(&ctx, &config) + .translated_content("slash_command.ping_command", Some(¶ms), None, None) + .await + .to_channel(command.channel_id) + .build_edit_message() + .await; + + res.edit(&ctx.http, response).await?; + + Ok(()) + }) + } +} diff --git a/rustmail/src/commands/ping/text_command/mod.rs b/rustmail/src/commands/ping/text_command/mod.rs new file mode 100644 index 00000000..2df3c1c3 --- /dev/null +++ b/rustmail/src/commands/ping/text_command/mod.rs @@ -0,0 +1,3 @@ +pub mod ping; + +pub use ping::*; diff --git a/rustmail/src/commands/ping/text_command/ping.rs b/rustmail/src/commands/ping/text_command/ping.rs new file mode 100644 index 00000000..d43a24e2 --- /dev/null +++ b/rustmail/src/commands/ping/text_command/ping.rs @@ -0,0 +1,67 @@ +use crate::bot::ShardManagerKey; +use crate::prelude::config::*; +use crate::prelude::errors::*; +use crate::prelude::handlers::*; +use crate::utils::MessageBuilder; +use chrono::Utc; +use serenity::all::{Context, Message}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use tokio::time::Instant; + +pub async fn ping( + ctx: Context, + msg: Message, + config: &Config, + _handler: Arc, +) -> ModmailResult<()> { + let shard_manager = ctx + .data + .read() + .await + .get::() + .cloned() + .ok_or(ModmailError::Discord(DiscordError::ShardManagerNotFound))?; + + let time_before = Instant::now(); + let mut res = msg.reply(&ctx.http, "...").await?; + let msg_send_ping = time_before.elapsed().as_millis(); + + let gateway_ping = { + let runners = shard_manager.runners.lock().await; + runners.get(&ctx.shard_id).and_then(|runner| runner.latency) + }; + + let start = Instant::now(); + ctx.http.get_gateway().await?; + let api_ping = start.elapsed(); + + let mut params = HashMap::new(); + params.insert( + "gateway_latency".to_string(), + format!( + "{:?}", + gateway_ping.unwrap_or(Duration::default()).as_millis() + ), + ); + params.insert( + "api_latency".to_string(), + format!("{:?}", api_ping.as_millis()), + ); + params.insert( + "message_latency".to_string(), + format!("{:?}", msg_send_ping), + ); + + let edited_msg = MessageBuilder::system_message(&ctx, &config) + .translated_content("slash_command.ping_command", Some(¶ms), None, None) + .await + .to_channel(msg.channel_id) + .build_edit_message() + .await; + + res.edit(&ctx.http, edited_msg).await?; + + Ok(()) +} diff --git a/rustmail/src/config.rs b/rustmail/src/config.rs index d630a8be..2feb46fc 100644 --- a/rustmail/src/config.rs +++ b/rustmail/src/config.rs @@ -21,7 +21,8 @@ pub struct Config { pub db_pool: Option, pub error_handler: Option>, - pub thread_locks: Arc>>>>, + pub thread_locks: + Arc>>>>, } fn get_local_ip() -> Option { @@ -50,21 +51,30 @@ pub fn load_config(path: &str) -> Option { } 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!"); + eprintln!( + "Incorrect user 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!"); + eprintln!( + "Incorrect staff message 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!"); + eprintln!( + "Incorrect reminder embed color in the config.toml! Please put a color in hex format!" + ); return None; } - if !config_response.language.is_language_supported(config_response.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_response.language.default_language diff --git a/rustmail/src/errors/dictionary.rs b/rustmail/src/errors/dictionary.rs index 54e42c5b..d2f2278b 100644 --- a/rustmail/src/errors/dictionary.rs +++ b/rustmail/src/errors/dictionary.rs @@ -188,6 +188,9 @@ impl DictionaryManager { DiscordError::UserIsABot => ("discord.user_is_a_bot".to_string(), None), DiscordError::PermissionDenied => ("discord.permission_denied".to_string(), None), DiscordError::DmCreationFailed => ("discord.dm_creation_failed".to_string(), None), + DiscordError::ShardManagerNotFound => { + ("discord.shard_manager_not_found".to_string(), None) + } _ => ("discord.api_error".to_string(), None), }, ModmailError::Command(cmd_err) => match cmd_err { diff --git a/rustmail/src/errors/types.rs b/rustmail/src/errors/types.rs index 2ccca1d4..093ce033 100644 --- a/rustmail/src/errors/types.rs +++ b/rustmail/src/errors/types.rs @@ -56,6 +56,7 @@ pub enum DiscordError { DmCreationFailed, FailedToFetchCategories, FailedToMoveChannel, + ShardManagerNotFound, } #[derive(Debug, Clone)] @@ -198,6 +199,7 @@ impl fmt::Display for DiscordError { DiscordError::DmCreationFailed => write!(f, "Failed to create DM channel"), DiscordError::FailedToFetchCategories => write!(f, "Failed to fetch categories"), DiscordError::FailedToMoveChannel => write!(f, "Failed to move_thread channel"), + DiscordError::ShardManagerNotFound => write!(f, "Shard manager not found"), } } } diff --git a/rustmail/src/handlers/guild_messages_handler.rs b/rustmail/src/handlers/guild_messages_handler.rs index 67faabc2..55ddf01a 100644 --- a/rustmail/src/handlers/guild_messages_handler.rs +++ b/rustmail/src/handlers/guild_messages_handler.rs @@ -77,6 +77,7 @@ impl GuildMessagesHandler { wrap_command!(lock, "logs", logs); wrap_command!(lock, "take", take); wrap_command!(lock, "release", release); + wrap_command!(lock, "ping", ping); drop(lock); h diff --git a/rustmail/src/i18n/language/en.rs b/rustmail/src/i18n/language/en.rs index 6f664dfd..c61ec0f4 100644 --- a/rustmail/src/i18n/language/en.rs +++ b/rustmail/src/i18n/language/en.rs @@ -50,6 +50,10 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "discord.user_is_a_bot".to_string(), DictionaryMessage::new("The specified user is a rustmail."), ); + dict.messages.insert( + "discord.shard_manager_not_found".to_string(), + DictionaryMessage::new("Shard manager not found."), + ); dict.messages.insert( "command.invalid_format".to_string(), DictionaryMessage::new("Invalid command format") @@ -883,6 +887,10 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "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( + "help.ping".to_string(), + DictionaryMessage::new("Shows the actual latency of the bot."), + ); dict.messages.insert( "add_reminder.helper".to_string(), DictionaryMessage::new( @@ -928,4 +936,12 @@ pub fn load_english_messages(dict: &mut ErrorDictionary) { "slash_command.help_command_argument_desc".to_string(), DictionaryMessage::new("The command to get help with"), ); + dict.messages.insert( + "slash_command.ping_command_desc".to_string(), + DictionaryMessage::new("Check the Discord bot latency."), + ); + dict.messages.insert( + "slash_command.ping_command".to_string(), + DictionaryMessage::new("## Latency\n\nGateway latency: **{gateway_latency}** ms\nMinimal REST latency (GET /gateway): **{api_latency}** ms\nREST latency (message send): **{message_latency}** ms"), + ); } diff --git a/rustmail/src/i18n/language/fr.rs b/rustmail/src/i18n/language/fr.rs index 4a817701..783d9894 100644 --- a/rustmail/src/i18n/language/fr.rs +++ b/rustmail/src/i18n/language/fr.rs @@ -45,6 +45,10 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "discord.user_is_a_bot".to_string(), DictionaryMessage::new("L'utilisateur spécifié est un rustmail"), ); + dict.messages.insert( + "discord.shard_manager_not_found".to_string(), + DictionaryMessage::new("Shard manager non trouvé."), + ); dict.messages.insert( "command.invalid_format".to_string(), DictionaryMessage::new("Format de commande invalide") @@ -904,6 +908,10 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "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( + "help.ping".to_string(), + DictionaryMessage::new("Permet d'afficher la latence actuelle du bot."), + ); dict.messages.insert( "add_reminder.helper".to_string(), DictionaryMessage::new("Format incorrect. Utilisation : `{prefix}remind ou {prefix}rem [contenu du rappel]`"), @@ -940,4 +948,12 @@ pub fn load_french_messages(dict: &mut ErrorDictionary) { "slash_command.help_command_argument_desc".to_string(), DictionaryMessage::new("Le nom de la commande pour laquelle vous souhaitez de l'aide"), ); + dict.messages.insert( + "slash_command.ping_command_desc".to_string(), + DictionaryMessage::new("Afficher la latence actuelle du bot."), + ); + dict.messages.insert( + "slash_command.ping_command".to_string(), + DictionaryMessage::new("## Latence\n\nLatence Gateway : **{gateway_latency}** ms.\nLatence REST minimale (GET /gateway) : **{api_latency}** ms.\nLatence REST (envoi d'un message) : **{message_latency}** ms."), + ); } diff --git a/rustmail/src/utils/message/message_builder.rs b/rustmail/src/utils/message/message_builder.rs index 201d87cf..59269655 100644 --- a/rustmail/src/utils/message/message_builder.rs +++ b/rustmail/src/utils/message/message_builder.rs @@ -531,6 +531,7 @@ impl<'a> MessageBuilder<'a> { if self.should_use_embed().await { message = message.embed(self.build_embed().await); + message = message.content(""); } else { let content = self.build_plain_message(); if !content.is_empty() { @@ -550,6 +551,7 @@ impl<'a> MessageBuilder<'a> { if self.should_use_embed().await { message = message.embed(self.build_embed().await); + message = message.content(""); } else { let content = self.build_plain_message(); if !content.is_empty() { @@ -571,6 +573,7 @@ impl<'a> MessageBuilder<'a> { if self.should_use_embed().await { message = message.embed(self.build_embed().await); + message = message.content(""); } else { let content = self.build_plain_message(); if !content.is_empty() {