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 tracing::info;
|
||||||
|
|
||||||
use crate::data::{DatabaseClientData, TTSData};
|
use crate::{
|
||||||
|
connection_monitor::ConnectionMonitor,
|
||||||
|
data::{DatabaseClientData, TTSData},
|
||||||
|
};
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn ready(ctx: Context, ready: Ready) {
|
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 from database
|
||||||
restore_tts_instances(&ctx).await;
|
restore_tts_instances(&ctx).await;
|
||||||
|
|
||||||
|
// Start connection monitor
|
||||||
|
ConnectionMonitor::start(ctx.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore TTS instances from database and reconnect to voice channels
|
/// 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
|
// Try to reconnect to voice channel
|
||||||
match instance.reconnect(ctx).await {
|
match instance.reconnect(ctx, true).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Add to in-memory storage
|
// Add to in-memory storage
|
||||||
let mut tts_data = tts_data.write().await;
|
let mut tts_data = tts_data.write().await;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod connection_monitor;
|
||||||
mod data;
|
mod data;
|
||||||
mod database;
|
mod database;
|
||||||
mod event_handler;
|
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
|
/// Reconnect to the voice channel after bot restart
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn reconnect(
|
pub async fn reconnect(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
|
skip_check: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let manager = songbird::get(&ctx)
|
let manager = songbird::get(&ctx)
|
||||||
.await
|
.await
|
||||||
.ok_or("Songbird manager not available")?;
|
.ok_or("Songbird manager not available")?;
|
||||||
|
|
||||||
// Check if we're already connected
|
// 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);
|
tracing::info!("Already connected to guild {}", self.guild);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user