diff --git a/src/connection_monitor.rs b/src/connection_monitor.rs new file mode 100644 index 0000000..df25eef --- /dev/null +++ b/src/connection_monitor.rs @@ -0,0 +1,147 @@ +use serenity::{model::channel::Message, prelude::Context, all::{CreateMessage, CreateEmbed}}; +use std::time::Duration; +use tokio::time; +use tracing::{error, info, warn}; + +use crate::data::{DatabaseClientData, TTSData}; + +/// Connection monitor that periodically checks voice channel connections +pub struct ConnectionMonitor; + +impl ConnectionMonitor { + /// Start the connection monitoring task + pub fn start(ctx: Context) { + tokio::spawn(async move { + info!("Starting connection monitor with 5s interval"); + let mut interval = time::interval(Duration::from_secs(5)); + + loop { + interval.tick().await; + Self::check_connections(&ctx).await; + } + }); + } + + /// Check all active TTS instances and their voice channel connections + async fn check_connections(ctx: &Context) { + let storage_lock = { + let data_read = ctx.data.read().await; + data_read + .get::() + .expect("Cannot get TTSStorage") + .clone() + }; + + let database = { + let data_read = ctx.data.read().await; + data_read + .get::() + .expect("Cannot get DatabaseClientData") + .clone() + }; + + let mut storage = storage_lock.write().await; + let mut guilds_to_remove = Vec::new(); + + for (guild_id, instance) in storage.iter() { + // Check if bot is still connected to voice channel + let manager = match songbird::get(ctx).await { + Some(manager) => manager, + None => { + error!("Cannot get songbird manager"); + continue; + } + }; + + let call = manager.get(*guild_id); + let is_connected = if let Some(call) = call { + if let Some(connection) = call.lock().await.current_connection() { + connection.channel_id.is_some() + } else { + false + } + } else { + false + }; + + if !is_connected { + warn!("Bot disconnected from voice channel in guild {}", guild_id); + + // Check if there are users in the voice channel + let should_reconnect = match Self::check_voice_channel_users(ctx, instance).await { + Ok(has_users) => has_users, + Err(_) => { + // If we can't check users, don't reconnect + false + } + }; + + if should_reconnect { + // Try to reconnect + match instance.reconnect(ctx, true).await { + Ok(_) => { + info!( + "Successfully reconnected to voice channel in guild {}", + guild_id + ); + + // Send notification message to text channel with embed + let embed = CreateEmbed::new() + .title("πŸ”„ θ‡ͺ動再ζŽ₯ηΆšγ—γΎγ—γŸ") + .description("θͺ­γΏδΈŠγ’γ‚’εœζ­’γ—γŸγ„ε ΄εˆγ― `/stop` γ‚³γƒžγƒ³γƒ‰γ‚’δ½Ώη”¨γ—γ¦γγ γ•γ„γ€‚") + .color(0x00ff00); + if let Err(e) = instance.text_channel.send_message(&ctx.http, CreateMessage::new().embed(embed)).await { + error!("Failed to send reconnection message to text channel: {}", e); + } + } + Err(e) => { + error!( + "Failed to reconnect to voice channel in guild {}: {}", + guild_id, e + ); + guilds_to_remove.push(*guild_id); + } + } + } else { + info!( + "No users in voice channel, removing instance for guild {}", + guild_id + ); + guilds_to_remove.push(*guild_id); + } + } + } + + // Remove disconnected instances + for guild_id in guilds_to_remove { + storage.remove(&guild_id); + + // Remove from database + if let Err(e) = database.remove_tts_instance(guild_id).await { + error!("Failed to remove TTS instance from database: {}", e); + } + + // Ensure bot leaves voice channel + if let Some(manager) = songbird::get(ctx).await { + let _ = manager.remove(guild_id).await; + } + } + } + + /// Check if there are users in the voice channel + async fn check_voice_channel_users( + ctx: &Context, + instance: &crate::tts::instance::TTSInstance, + ) -> Result> { + let channels = instance.guild.channels(&ctx.http).await?; + + if let Some(channel) = channels.get(&instance.voice_channel) { + let members = channel.members(&ctx.cache)?; + let user_count = members.iter().filter(|member| !member.user.bot).count(); + Ok(user_count > 0) + } else { + // Channel doesn't exist anymore + Ok(false) + } + } +} diff --git a/src/events/ready.rs b/src/events/ready.rs index f22b410..c2dbb37 100644 --- a/src/events/ready.rs +++ b/src/events/ready.rs @@ -5,7 +5,10 @@ use serenity::{ }; use tracing::info; -use crate::data::{DatabaseClientData, TTSData}; +use crate::{ + connection_monitor::ConnectionMonitor, + data::{DatabaseClientData, TTSData}, +}; #[tracing::instrument] pub async fn ready(ctx: Context, ready: Ready) { @@ -35,6 +38,9 @@ pub async fn ready(ctx: Context, ready: Ready) { // Restore TTS instances from database restore_tts_instances(&ctx).await; + + // Start connection monitor + ConnectionMonitor::start(ctx.clone()); } /// Restore TTS instances from database and reconnect to voice channels @@ -107,7 +113,7 @@ async fn restore_tts_instances(ctx: &Context) { } // Try to reconnect to voice channel - match instance.reconnect(ctx).await { + match instance.reconnect(ctx, true).await { Ok(_) => { // Add to in-memory storage let mut tts_data = tts_data.write().await; diff --git a/src/main.rs b/src/main.rs index 35fad26..9de9d17 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod commands; mod config; +mod connection_monitor; mod data; mod database; mod event_handler; diff --git a/src/tts/instance.rs b/src/tts/instance.rs index 786ea53..fd44034 100644 --- a/src/tts/instance.rs +++ b/src/tts/instance.rs @@ -31,18 +31,40 @@ impl TTSInstance { } } + pub async fn check_connection(&self, ctx: &Context) -> bool { + let manager = match songbird::get(ctx).await { + Some(manager) => manager, + None => { + tracing::error!("Cannot get songbird manager"); + return false; + } + }; + + let call = manager.get(self.guild); + if let Some(call) = call { + if let Some(connection) = call.lock().await.current_connection() { + connection.channel_id.is_some() + } else { + false + } + } else { + false + } + } + /// Reconnect to the voice channel after bot restart #[tracing::instrument] pub async fn reconnect( &self, ctx: &Context, + skip_check: bool, ) -> Result<(), Box> { let manager = songbird::get(&ctx) .await .ok_or("Songbird manager not available")?; // Check if we're already connected - if manager.get(self.guild).is_some() { + if self.check_connection(&ctx).await { tracing::info!("Already connected to guild {}", self.guild); return Ok(()); }