mirror of
https://github.com/mii443/ncb-tts-r2.git
synced 2025-08-22 16:15:29 +00:00
feat: add connection monitor with auto-reconnect notifications
Implements automatic voice channel connection monitoring with user-friendly notifications: - Monitor connections every 5 seconds to detect disconnections - Auto-reconnect when users are present in voice channel - Send embed notifications to text channel upon reconnection - Include /stop command information in reconnection messages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
147
src/connection_monitor.rs
Normal file
147
src/connection_monitor.rs
Normal file
@ -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::<TTSData>()
|
||||
.expect("Cannot get TTSStorage")
|
||||
.clone()
|
||||
};
|
||||
|
||||
let database = {
|
||||
let data_read = ctx.data.read().await;
|
||||
data_read
|
||||
.get::<DatabaseClientData>()
|
||||
.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<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -1,5 +1,6 @@
|
||||
mod commands;
|
||||
mod config;
|
||||
mod connection_monitor;
|
||||
mod data;
|
||||
mod database;
|
||||
mod event_handler;
|
||||
|
@ -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<dyn std::error::Error + Send + Sync>> {
|
||||
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(());
|
||||
}
|
||||
|
Reference in New Issue
Block a user