mirror of
https://github.com/mii443/ncb-tts-r2.git
synced 2025-08-22 16:15:29 +00:00
feat: テキストチャンネルの自動参加設定を追加
- 複数のテキストチャンネルをサポートするために、TTSインスタンスの構造を変更 - 自動参加テキストチャンネルの設定と解除をUIセレクトメニューで実装 - 再接続時にテキストチャンネルに通知を送信する機能を強化 - コードの可読性向上のために、エラーハンドリングとロギングを改善 🤖 Generated with [Claude Code](https://claude.ai/code)
This commit is contained in:
@ -34,7 +34,10 @@ pub async fn config_command(
|
|||||||
let tts_client = data_read
|
let tts_client = data_read
|
||||||
.get::<TTSClientData>()
|
.get::<TTSClientData>()
|
||||||
.expect("Cannot get TTSClientData");
|
.expect("Cannot get TTSClientData");
|
||||||
let voicevox_speakers = tts_client.voicevox_client.get_styles().await
|
let voicevox_speakers = tts_client
|
||||||
|
.voicevox_client
|
||||||
|
.get_styles()
|
||||||
|
.await
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
tracing::error!("Failed to get VOICEVOX styles: {}", e);
|
tracing::error!("Failed to get VOICEVOX styles: {}", e);
|
||||||
vec![("VOICEVOX API unavailable".to_string(), 1)]
|
vec![("VOICEVOX API unavailable".to_string(), 1)]
|
||||||
@ -58,11 +61,7 @@ pub async fn config_command(
|
|||||||
.placeholder("読み上げAPIを選択"),
|
.placeholder("読み上げAPIを選択"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let server_button = CreateActionRow::Buttons(vec![CreateButton::new("TTS_CONFIG_SERVER")
|
let mut components = vec![engine_select];
|
||||||
.label("サーバー設定")
|
|
||||||
.style(ButtonStyle::Primary)]);
|
|
||||||
|
|
||||||
let mut components = vec![engine_select, server_button];
|
|
||||||
|
|
||||||
for (index, speaker_chunk) in voicevox_speakers[0..24].chunks(25).enumerate() {
|
for (index, speaker_chunk) in voicevox_speakers[0..24].chunks(25).enumerate() {
|
||||||
let mut options = Vec::new();
|
let mut options = Vec::new();
|
||||||
@ -86,6 +85,12 @@ pub async fn config_command(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let server_button = CreateActionRow::Buttons(vec![CreateButton::new("TTS_CONFIG_SERVER")
|
||||||
|
.label("サーバー設定")
|
||||||
|
.style(ButtonStyle::Primary)]);
|
||||||
|
|
||||||
|
components.push(server_button);
|
||||||
|
|
||||||
command
|
command
|
||||||
.create_response(
|
.create_response(
|
||||||
&ctx.http,
|
&ctx.http,
|
||||||
|
@ -81,32 +81,44 @@ pub async fn setup_command(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_channel_id = {
|
let text_channel_ids = {
|
||||||
if let Some(mode) = command.data.options.get(0) {
|
if let Some(mode) = command.data.options.get(0) {
|
||||||
match &mode.value {
|
match &mode.value {
|
||||||
serenity::all::CommandDataOptionValue::String(value) => {
|
serenity::all::CommandDataOptionValue::String(value) => {
|
||||||
match value.as_str() {
|
match value.as_str() {
|
||||||
"TEXT_CHANNEL" => command.channel_id,
|
"TEXT_CHANNEL" => vec![command.channel_id],
|
||||||
"NEW_THREAD" => {
|
"NEW_THREAD" => {
|
||||||
command
|
vec![command
|
||||||
.channel_id
|
.channel_id
|
||||||
.create_thread(&ctx.http, CreateThread::new("TTS").auto_archive_duration(AutoArchiveDuration::OneHour).kind(serenity::all::ChannelType::PublicThread))
|
.create_thread(&ctx.http, CreateThread::new("TTS").auto_archive_duration(AutoArchiveDuration::OneHour).kind(serenity::all::ChannelType::PublicThread))
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.id
|
.id]
|
||||||
}
|
}
|
||||||
"VOICE_CHANNEL" => channel_id,
|
"VOICE_CHANNEL" => vec![channel_id],
|
||||||
_ => channel_id,
|
_ => if channel_id != command.channel_id {
|
||||||
|
vec![command.channel_id, channel_id]
|
||||||
|
} else {
|
||||||
|
vec![channel_id]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => channel_id,
|
_ => if channel_id != command.channel_id {
|
||||||
|
vec![command.channel_id, channel_id]
|
||||||
|
} else {
|
||||||
|
vec![channel_id]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel_id
|
if channel_id != command.channel_id {
|
||||||
|
vec![command.channel_id, channel_id]
|
||||||
|
} else {
|
||||||
|
vec![channel_id]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let instance = TTSInstance::new(text_channel_id, channel_id, guild.id);
|
let instance = TTSInstance::new(text_channel_ids.clone(), channel_id, guild.id);
|
||||||
storage.insert(guild.id, instance.clone());
|
storage.insert(guild.id, instance.clone());
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
@ -121,7 +133,7 @@ pub async fn setup_command(
|
|||||||
tracing::error!("Failed to save TTS instance to database: {}", e);
|
tracing::error!("Failed to save TTS instance to database: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
text_channel_id
|
text_channel_ids[0]
|
||||||
};
|
};
|
||||||
|
|
||||||
command
|
command
|
||||||
|
@ -78,7 +78,7 @@ pub async fn stop_command(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_channel_id = storage.get(&guild.id).unwrap().text_channel;
|
let text_channel_id = storage.get(&guild.id).unwrap().text_channels[0];
|
||||||
storage.remove(&guild.id);
|
storage.remove(&guild.id);
|
||||||
|
|
||||||
// Remove from database
|
// Remove from database
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
use serenity::{prelude::Context, all::{CreateMessage, CreateEmbed}};
|
use serenity::{
|
||||||
|
all::{CreateEmbed, CreateMessage},
|
||||||
|
prelude::Context,
|
||||||
|
};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use tracing::{error, info, warn, instrument};
|
use tracing::{error, info, instrument, warn};
|
||||||
|
|
||||||
use crate::data::{DatabaseClientData, TTSData};
|
use crate::data::{DatabaseClientData, TTSData};
|
||||||
|
|
||||||
@ -69,7 +72,9 @@ impl ConnectionMonitor {
|
|||||||
let data_read = ctx.data.read().await;
|
let data_read = ctx.data.read().await;
|
||||||
data_read
|
data_read
|
||||||
.get::<TTSData>()
|
.get::<TTSData>()
|
||||||
.ok_or_else(|| ConnectionMonitorError::VoiceChannelCheck("Cannot get TTSStorage".to_string()))?
|
.ok_or_else(|| {
|
||||||
|
ConnectionMonitorError::VoiceChannelCheck("Cannot get TTSStorage".to_string())
|
||||||
|
})?
|
||||||
.clone()
|
.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,7 +82,11 @@ impl ConnectionMonitor {
|
|||||||
let data_read = ctx.data.read().await;
|
let data_read = ctx.data.read().await;
|
||||||
data_read
|
data_read
|
||||||
.get::<DatabaseClientData>()
|
.get::<DatabaseClientData>()
|
||||||
.ok_or_else(|| ConnectionMonitorError::VoiceChannelCheck("Cannot get DatabaseClientData".to_string()))?
|
.ok_or_else(|| {
|
||||||
|
ConnectionMonitorError::VoiceChannelCheck(
|
||||||
|
"Cannot get DatabaseClientData".to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
.clone()
|
.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,7 +95,8 @@ impl ConnectionMonitor {
|
|||||||
|
|
||||||
for (guild_id, instance) in storage.iter() {
|
for (guild_id, instance) in storage.iter() {
|
||||||
// Check if bot is still connected to voice channel
|
// Check if bot is still connected to voice channel
|
||||||
let manager = songbird::get(ctx).await
|
let manager = songbird::get(ctx)
|
||||||
|
.await
|
||||||
.ok_or(ConnectionMonitorError::SongbirdManagerNotFound)?;
|
.ok_or(ConnectionMonitorError::SongbirdManagerNotFound)?;
|
||||||
|
|
||||||
let call = manager.get(*guild_id);
|
let call = manager.get(*guild_id);
|
||||||
@ -114,8 +124,12 @@ impl ConnectionMonitor {
|
|||||||
|
|
||||||
if should_reconnect {
|
if should_reconnect {
|
||||||
// Try to reconnect with retry logic
|
// Try to reconnect with retry logic
|
||||||
let attempts = self.reconnection_attempts.get(guild_id).copied().unwrap_or(0);
|
let attempts = self
|
||||||
|
.reconnection_attempts
|
||||||
|
.get(guild_id)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
if attempts >= MAX_RECONNECTION_ATTEMPTS {
|
if attempts >= MAX_RECONNECTION_ATTEMPTS {
|
||||||
error!(
|
error!(
|
||||||
guild_id = %guild_id,
|
guild_id = %guild_id,
|
||||||
@ -129,7 +143,8 @@ impl ConnectionMonitor {
|
|||||||
|
|
||||||
// Apply exponential backoff
|
// Apply exponential backoff
|
||||||
if attempts > 0 {
|
if attempts > 0 {
|
||||||
let backoff_duration = Duration::from_secs(RECONNECTION_BACKOFF_SECS * (2_u64.pow(attempts)));
|
let backoff_duration =
|
||||||
|
Duration::from_secs(RECONNECTION_BACKOFF_SECS * (2_u64.pow(attempts)));
|
||||||
warn!(
|
warn!(
|
||||||
guild_id = %guild_id,
|
guild_id = %guild_id,
|
||||||
attempt = attempts + 1,
|
attempt = attempts + 1,
|
||||||
@ -146,17 +161,24 @@ impl ConnectionMonitor {
|
|||||||
attempts = attempts + 1,
|
attempts = attempts + 1,
|
||||||
"Successfully reconnected to voice channel"
|
"Successfully reconnected to voice channel"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset reconnection attempts on success
|
// Reset reconnection attempts on success
|
||||||
self.reconnection_attempts.remove(guild_id);
|
self.reconnection_attempts.remove(guild_id);
|
||||||
|
|
||||||
// Send notification message to text channel with embed
|
// Send notification message to text channel with embed
|
||||||
let embed = CreateEmbed::new()
|
let embed = CreateEmbed::new()
|
||||||
.title("🔄 自動再接続しました")
|
.title("🔄 自動再接続しました")
|
||||||
.description("読み上げを停止したい場合は `/stop` コマンドを使用してください。")
|
.description("読み上げを停止したい場合は `/stop` コマンドを使用してください。")
|
||||||
.color(0x00ff00);
|
.color(0x00ff00);
|
||||||
if let Err(e) = instance.text_channel.send_message(&ctx.http, CreateMessage::new().embed(embed)).await {
|
|
||||||
error!(guild_id = %guild_id, error = %e, "Failed to send reconnection message");
|
// Send message to the first text channel
|
||||||
|
if let Some(&text_channel) = instance.text_channels.first() {
|
||||||
|
if let Err(e) = text_channel
|
||||||
|
.send_message(&ctx.http, CreateMessage::new().embed(embed))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!(guild_id = %guild_id, error = %e, "Failed to send reconnection message");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -168,7 +190,7 @@ impl ConnectionMonitor {
|
|||||||
error = %e,
|
error = %e,
|
||||||
"Failed to reconnect to voice channel"
|
"Failed to reconnect to voice channel"
|
||||||
);
|
);
|
||||||
|
|
||||||
if new_attempts >= MAX_RECONNECTION_ATTEMPTS {
|
if new_attempts >= MAX_RECONNECTION_ATTEMPTS {
|
||||||
guilds_to_remove.push(*guild_id);
|
guilds_to_remove.push(*guild_id);
|
||||||
self.reconnection_attempts.remove(guild_id);
|
self.reconnection_attempts.remove(guild_id);
|
||||||
@ -201,10 +223,10 @@ impl ConnectionMonitor {
|
|||||||
error!(guild_id = %guild_id, error = %e, "Failed to remove bot from voice channel");
|
error!(guild_id = %guild_id, error = %e, "Failed to remove bot from voice channel");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info!(guild_id = %guild_id, "Removed disconnected TTS instance");
|
info!(guild_id = %guild_id, "Removed disconnected TTS instance");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,21 +237,29 @@ impl ConnectionMonitor {
|
|||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
instance: &crate::tts::instance::TTSInstance,
|
instance: &crate::tts::instance::TTSInstance,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let channels = instance.guild.channels(&ctx.http).await
|
let channels = instance.guild.channels(&ctx.http).await.map_err(|e| {
|
||||||
.map_err(|e| ConnectionMonitorError::VoiceChannelCheck(format!("Failed to get guild channels: {}", e)))?;
|
ConnectionMonitorError::VoiceChannelCheck(format!(
|
||||||
|
"Failed to get guild channels: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
if let Some(channel) = channels.get(&instance.voice_channel) {
|
if let Some(channel) = channels.get(&instance.voice_channel) {
|
||||||
let members = channel.members(&ctx.cache)
|
let members = channel.members(&ctx.cache).map_err(|e| {
|
||||||
.map_err(|e| ConnectionMonitorError::VoiceChannelCheck(format!("Failed to get channel members: {}", e)))?;
|
ConnectionMonitorError::VoiceChannelCheck(format!(
|
||||||
|
"Failed to get channel members: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
let user_count = members.iter().filter(|member| !member.user.bot).count();
|
let user_count = members.iter().filter(|member| !member.user.bot).count();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
guild_id = %instance.guild,
|
guild_id = %instance.guild,
|
||||||
channel_id = %instance.voice_channel,
|
channel_id = %instance.voice_channel,
|
||||||
user_count = user_count,
|
user_count = user_count,
|
||||||
"Checked voice channel users"
|
"Checked voice channel users"
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(user_count > 0)
|
Ok(user_count > 0)
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use bb8_redis::{bb8::Pool, RedisConnectionManager, redis::AsyncCommands};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
errors::{NCBError, Result, constants::*},
|
errors::{constants::*, NCBError, Result},
|
||||||
tts::{
|
tts::{
|
||||||
gcp_tts::structs::voice_selection_params::VoiceSelectionParams, instance::TTSInstance,
|
gcp_tts::structs::voice_selection_params::VoiceSelectionParams, instance::TTSInstance,
|
||||||
tts_type::TTSType,
|
tts_type::TTSType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use serenity::model::id::{GuildId, UserId, ChannelId};
|
use bb8_redis::{bb8::Pool, redis::AsyncCommands, RedisConnectionManager};
|
||||||
|
use serenity::model::id::{ChannelId, GuildId, UserId};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use super::{dictionary::Dictionary, server_config::ServerConfig, user_config::UserConfig};
|
use super::{dictionary::Dictionary, server_config::ServerConfig, user_config::UserConfig};
|
||||||
@ -22,7 +22,7 @@ impl Database {
|
|||||||
pub fn new(pool: Pool<RedisConnectionManager>) -> Self {
|
pub fn new(pool: Pool<RedisConnectionManager>) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new_with_url(redis_url: String) -> Result<Self> {
|
pub async fn new_with_url(redis_url: String) -> Result<Self> {
|
||||||
let manager = RedisConnectionManager::new(redis_url)?;
|
let manager = RedisConnectionManager::new(redis_url)?;
|
||||||
let pool = Pool::builder()
|
let pool = Pool::builder()
|
||||||
@ -62,13 +62,13 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
async fn get_config<T: serde::de::DeserializeOwned>(
|
async fn get_config<T: serde::de::DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
|
||||||
&self,
|
let mut connection = self
|
||||||
key: &str,
|
.pool
|
||||||
) -> Result<Option<T>> {
|
.get()
|
||||||
let mut connection = self.pool.get().await
|
.await
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
|
|
||||||
let config: String = connection.get(key).await.unwrap_or_default();
|
let config: String = connection.get(key).await.unwrap_or_default();
|
||||||
|
|
||||||
if config.is_empty() {
|
if config.is_empty() {
|
||||||
@ -85,24 +85,20 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
async fn set_config<T: serde::Serialize + Debug>(
|
async fn set_config<T: serde::Serialize + Debug>(&self, key: &str, config: &T) -> Result<()> {
|
||||||
&self,
|
let mut connection = self
|
||||||
key: &str,
|
.pool
|
||||||
config: &T,
|
.get()
|
||||||
) -> Result<()> {
|
.await
|
||||||
let mut connection = self.pool.get().await
|
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
|
|
||||||
let config_str = serde_json::to_string(config)?;
|
let config_str = serde_json::to_string(config)?;
|
||||||
connection.set::<_, _, ()>(key, config_str).await?;
|
connection.set::<_, _, ()>(key, config_str).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn get_server_config(
|
pub async fn get_server_config(&self, server_id: u64) -> Result<Option<ServerConfig>> {
|
||||||
&self,
|
|
||||||
server_id: u64,
|
|
||||||
) -> Result<Option<ServerConfig>> {
|
|
||||||
self.get_config(&Self::server_key(server_id)).await
|
self.get_config(&Self::server_key(server_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,20 +108,12 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn set_server_config(
|
pub async fn set_server_config(&self, server_id: u64, config: ServerConfig) -> Result<()> {
|
||||||
&self,
|
|
||||||
server_id: u64,
|
|
||||||
config: ServerConfig,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.set_config(&Self::server_key(server_id), &config).await
|
self.set_config(&Self::server_key(server_id), &config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn set_user_config(
|
pub async fn set_user_config(&self, user_id: u64, config: UserConfig) -> Result<()> {
|
||||||
&self,
|
|
||||||
user_id: u64,
|
|
||||||
config: UserConfig,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.set_config(&Self::user_key(user_id), &config).await
|
self.set_config(&Self::user_key(user_id), &config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,8 +122,9 @@ impl Database {
|
|||||||
let config = ServerConfig {
|
let config = ServerConfig {
|
||||||
dictionary: Dictionary::new(),
|
dictionary: Dictionary::new(),
|
||||||
autostart_channel_id: None,
|
autostart_channel_id: None,
|
||||||
voice_state_announce: Some(true),
|
autostart_text_channel_id: None,
|
||||||
read_username: Some(true),
|
voice_state_announce: Some(false),
|
||||||
|
read_username: Some(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.set_server_config(server_id, config).await
|
self.set_server_config(server_id, config).await
|
||||||
@ -173,10 +162,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn get_user_config_or_default(
|
pub async fn get_user_config_or_default(&self, user_id: u64) -> Result<Option<UserConfig>> {
|
||||||
&self,
|
|
||||||
user_id: u64,
|
|
||||||
) -> Result<Option<UserConfig>> {
|
|
||||||
match self.get_user_config(user_id).await? {
|
match self.get_user_config(user_id).await? {
|
||||||
Some(config) => Ok(Some(config)),
|
Some(config) => Ok(Some(config)),
|
||||||
None => {
|
None => {
|
||||||
@ -187,11 +173,7 @@ impl Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Save TTS instance to database
|
/// Save TTS instance to database
|
||||||
pub async fn save_tts_instance(
|
pub async fn save_tts_instance(&self, guild_id: GuildId, instance: &TTSInstance) -> Result<()> {
|
||||||
&self,
|
|
||||||
guild_id: GuildId,
|
|
||||||
instance: &TTSInstance,
|
|
||||||
) -> Result<()> {
|
|
||||||
let key = Self::tts_instance_key(guild_id.get());
|
let key = Self::tts_instance_key(guild_id.get());
|
||||||
let list_key = Self::tts_instances_list_key();
|
let list_key = Self::tts_instances_list_key();
|
||||||
|
|
||||||
@ -199,19 +181,21 @@ impl Database {
|
|||||||
self.set_config(&key, instance).await?;
|
self.set_config(&key, instance).await?;
|
||||||
|
|
||||||
// Add guild_id to the list of active instances
|
// Add guild_id to the list of active instances
|
||||||
let mut connection = self.pool.get().await
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
|
|
||||||
connection.sadd::<_, _, ()>(&list_key, guild_id.get()).await?;
|
connection
|
||||||
|
.sadd::<_, _, ()>(&list_key, guild_id.get())
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load TTS instance from database
|
/// Load TTS instance from database
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn load_tts_instance(
|
pub async fn load_tts_instance(&self, guild_id: GuildId) -> Result<Option<TTSInstance>> {
|
||||||
&self,
|
|
||||||
guild_id: GuildId,
|
|
||||||
) -> Result<Option<TTSInstance>> {
|
|
||||||
let key = Self::tts_instance_key(guild_id.get());
|
let key = Self::tts_instance_key(guild_id.get());
|
||||||
self.get_config(&key).await
|
self.get_config(&key).await
|
||||||
}
|
}
|
||||||
@ -222,12 +206,16 @@ impl Database {
|
|||||||
let key = Self::tts_instance_key(guild_id.get());
|
let key = Self::tts_instance_key(guild_id.get());
|
||||||
let list_key = Self::tts_instances_list_key();
|
let list_key = Self::tts_instances_list_key();
|
||||||
|
|
||||||
let mut connection = self.pool.get().await
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
|
|
||||||
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.del(&key).await;
|
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.del(&key).await;
|
||||||
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.srem(&list_key, guild_id.get()).await;
|
let _: std::result::Result<(), bb8_redis::redis::RedisError> =
|
||||||
|
connection.srem(&list_key, guild_id.get()).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,9 +224,12 @@ impl Database {
|
|||||||
pub async fn get_all_tts_instances(&self) -> Result<Vec<(GuildId, TTSInstance)>> {
|
pub async fn get_all_tts_instances(&self) -> Result<Vec<(GuildId, TTSInstance)>> {
|
||||||
let list_key = Self::tts_instances_list_key();
|
let list_key = Self::tts_instances_list_key();
|
||||||
|
|
||||||
let mut connection = self.pool.get().await
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
|
|
||||||
let guild_ids: Vec<u64> = connection.smembers(&list_key).await.unwrap_or_default();
|
let guild_ids: Vec<u64> = connection.smembers(&list_key).await.unwrap_or_default();
|
||||||
let mut instances = Vec::new();
|
let mut instances = Vec::new();
|
||||||
|
|
||||||
@ -274,39 +265,34 @@ impl Database {
|
|||||||
self.get_config(&key).await
|
self.get_config(&key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_user_config(
|
pub async fn delete_user_config(&self, guild_id: GuildId, user_id: UserId) -> Result<()> {
|
||||||
&self,
|
|
||||||
guild_id: GuildId,
|
|
||||||
user_id: UserId,
|
|
||||||
) -> Result<()> {
|
|
||||||
let key = Self::user_config_key(guild_id.get(), user_id.get());
|
let key = Self::user_config_key(guild_id.get(), user_id.get());
|
||||||
let mut connection = self.pool.get().await
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.del(&key).await;
|
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.del(&key).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional server config methods
|
// Additional server config methods
|
||||||
pub async fn save_server_config(
|
pub async fn save_server_config(&self, guild_id: GuildId, config: &ServerConfig) -> Result<()> {
|
||||||
&self,
|
|
||||||
guild_id: GuildId,
|
|
||||||
config: &ServerConfig,
|
|
||||||
) -> Result<()> {
|
|
||||||
let key = Self::server_config_key(guild_id.get());
|
let key = Self::server_config_key(guild_id.get());
|
||||||
self.set_config(&key, config).await
|
self.set_config(&key, config).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_server_config(
|
pub async fn load_server_config(&self, guild_id: GuildId) -> Result<Option<ServerConfig>> {
|
||||||
&self,
|
|
||||||
guild_id: GuildId,
|
|
||||||
) -> Result<Option<ServerConfig>> {
|
|
||||||
let key = Self::server_config_key(guild_id.get());
|
let key = Self::server_config_key(guild_id.get());
|
||||||
self.get_config(&key).await
|
self.get_config(&key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_server_config(&self, guild_id: GuildId) -> Result<()> {
|
pub async fn delete_server_config(&self, guild_id: GuildId) -> Result<()> {
|
||||||
let key = Self::server_config_key(guild_id.get());
|
let key = Self::server_config_key(guild_id.get());
|
||||||
let mut connection = self.pool.get().await
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.del(&key).await;
|
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.del(&key).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -322,10 +308,7 @@ impl Database {
|
|||||||
self.set_config(&key, dictionary).await
|
self.set_config(&key, dictionary).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn load_dictionary(
|
pub async fn load_dictionary(&self, guild_id: GuildId) -> Result<HashMap<String, String>> {
|
||||||
&self,
|
|
||||||
guild_id: GuildId,
|
|
||||||
) -> Result<HashMap<String, String>> {
|
|
||||||
let key = Self::dictionary_key(guild_id.get());
|
let key = Self::dictionary_key(guild_id.get());
|
||||||
let dict: Option<HashMap<String, String>> = self.get_config(&key).await?;
|
let dict: Option<HashMap<String, String>> = self.get_config(&key).await?;
|
||||||
Ok(dict.unwrap_or_default())
|
Ok(dict.unwrap_or_default())
|
||||||
@ -333,7 +316,10 @@ impl Database {
|
|||||||
|
|
||||||
pub async fn delete_dictionary(&self, guild_id: GuildId) -> Result<()> {
|
pub async fn delete_dictionary(&self, guild_id: GuildId) -> Result<()> {
|
||||||
let key = Self::dictionary_key(guild_id.get());
|
let key = Self::dictionary_key(guild_id.get());
|
||||||
let mut connection = self.pool.get().await
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.del(&key).await;
|
let _: std::result::Result<(), bb8_redis::redis::RedisError> = connection.del(&key).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -345,7 +331,10 @@ impl Database {
|
|||||||
|
|
||||||
pub async fn list_active_instances(&self) -> Result<Vec<u64>> {
|
pub async fn list_active_instances(&self) -> Result<Vec<u64>> {
|
||||||
let list_key = Self::tts_instances_list_key();
|
let list_key = Self::tts_instances_list_key();
|
||||||
let mut connection = self.pool.get().await
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.get()
|
||||||
|
.await
|
||||||
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
.map_err(|e| NCBError::Database(format!("Pool connection failed: {}", e)))?;
|
||||||
let guild_ids: Vec<u64> = connection.smembers(&list_key).await.unwrap_or_default();
|
let guild_ids: Vec<u64> = connection.smembers(&list_key).await.unwrap_or_default();
|
||||||
Ok(guild_ids)
|
Ok(guild_ids)
|
||||||
@ -355,9 +344,9 @@ impl Database {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::errors::constants;
|
||||||
use bb8_redis::redis::AsyncCommands;
|
use bb8_redis::redis::AsyncCommands;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use crate::errors::constants;
|
|
||||||
|
|
||||||
// Helper function to create test database (requires Redis running)
|
// Helper function to create test database (requires Redis running)
|
||||||
async fn create_test_database() -> Result<Database> {
|
async fn create_test_database() -> Result<Database> {
|
||||||
@ -422,16 +411,19 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let guild_id = GuildId::new(12345);
|
let guild_id = GuildId::new(12345);
|
||||||
let test_instance = TTSInstance::new(
|
let test_instance =
|
||||||
ChannelId::new(123),
|
TTSInstance::new_single(ChannelId::new(123), ChannelId::new(456), guild_id);
|
||||||
ChannelId::new(456),
|
|
||||||
guild_id
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear any existing data
|
// Clear any existing data
|
||||||
if let Ok(mut conn) = db.pool.get().await {
|
if let Ok(mut conn) = db.pool.get().await {
|
||||||
let _: () = conn.del(Database::tts_instance_key(guild_id.get())).await.unwrap_or_default();
|
let _: () = conn
|
||||||
let _: () = conn.srem(Database::tts_instances_list_key(), guild_id.get()).await.unwrap_or_default();
|
.del(Database::tts_instance_key(guild_id.get()))
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
let _: () = conn
|
||||||
|
.srem(Database::tts_instances_list_key(), guild_id.get())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
} else {
|
} else {
|
||||||
return; // Skip if can't get connection
|
return; // Skip if can't get connection
|
||||||
}
|
}
|
||||||
@ -452,7 +444,7 @@ mod tests {
|
|||||||
let loaded_instance = load_result.unwrap();
|
let loaded_instance = load_result.unwrap();
|
||||||
if let Some(instance) = loaded_instance {
|
if let Some(instance) = loaded_instance {
|
||||||
assert_eq!(instance.guild, test_instance.guild);
|
assert_eq!(instance.guild, test_instance.guild);
|
||||||
assert_eq!(instance.text_channel, test_instance.text_channel);
|
assert_eq!(instance.text_channels, test_instance.text_channels);
|
||||||
assert_eq!(instance.voice_channel, test_instance.voice_channel);
|
assert_eq!(instance.voice_channel, test_instance.voice_channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,4 +477,4 @@ mod tests {
|
|||||||
assert!(constants::REDIS_MAX_CONNECTIONS > 0);
|
assert!(constants::REDIS_MAX_CONNECTIONS > 0);
|
||||||
assert!(constants::REDIS_MIN_IDLE_CONNECTIONS <= constants::REDIS_MAX_CONNECTIONS);
|
assert!(constants::REDIS_MIN_IDLE_CONNECTIONS <= constants::REDIS_MAX_CONNECTIONS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ pub struct DictionaryOnlyServerConfig {
|
|||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
pub dictionary: Dictionary,
|
pub dictionary: Dictionary,
|
||||||
pub autostart_channel_id: Option<u64>,
|
pub autostart_channel_id: Option<u64>,
|
||||||
|
pub autostart_text_channel_id: Option<u64>,
|
||||||
pub voice_state_announce: Option<bool>,
|
pub voice_state_announce: Option<bool>,
|
||||||
pub read_username: Option<bool>,
|
pub read_username: Option<bool>,
|
||||||
}
|
}
|
||||||
|
@ -324,6 +324,8 @@ pub mod constants {
|
|||||||
pub const CHANNEL_LEAVE_SUCCESS: &str = "CHANNEL_LEAVE_SUCCESS";
|
pub const CHANNEL_LEAVE_SUCCESS: &str = "CHANNEL_LEAVE_SUCCESS";
|
||||||
pub const AUTOSTART_CHANNEL_SET: &str = "AUTOSTART_CHANNEL_SET";
|
pub const AUTOSTART_CHANNEL_SET: &str = "AUTOSTART_CHANNEL_SET";
|
||||||
pub const SET_AUTOSTART_CHANNEL_CLEAR: &str = "SET_AUTOSTART_CHANNEL_CLEAR";
|
pub const SET_AUTOSTART_CHANNEL_CLEAR: &str = "SET_AUTOSTART_CHANNEL_CLEAR";
|
||||||
|
pub const SET_AUTOSTART_TEXT_CHANNEL: &str = "SET_AUTOSTART_TEXT_CHANNEL";
|
||||||
|
pub const SET_AUTOSTART_TEXT_CHANNEL_CLEAR: &str = "SET_AUTOSTART_TEXT_CHANNEL_CLEAR";
|
||||||
|
|
||||||
// TTS configuration constants
|
// TTS configuration constants
|
||||||
pub const TTS_CONFIG_SERVER_ADD_DICTIONARY: &str = "TTS_CONFIG_SERVER_ADD_DICTIONARY";
|
pub const TTS_CONFIG_SERVER_ADD_DICTIONARY: &str = "TTS_CONFIG_SERVER_ADD_DICTIONARY";
|
||||||
|
@ -55,55 +55,60 @@ impl EventHandler for Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let rows = modal.data.components.clone();
|
let rows = modal.data.components.clone();
|
||||||
|
|
||||||
// Extract rule name with proper error handling
|
// Extract rule name with proper error handling
|
||||||
let rule_name = match rows.get(0)
|
let rule_name =
|
||||||
.and_then(|row| row.components.get(0))
|
match rows
|
||||||
.and_then(|component| {
|
.get(0)
|
||||||
if let ActionRowComponent::InputText(text) = component {
|
.and_then(|row| row.components.get(0))
|
||||||
text.value.as_ref()
|
.and_then(|component| {
|
||||||
} else {
|
if let ActionRowComponent::InputText(text) = component {
|
||||||
None
|
text.value.as_ref()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Some(name) => {
|
||||||
|
if let Err(e) = validation::validate_rule_name(name) {
|
||||||
|
tracing::error!("Invalid rule name: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
name.clone()
|
||||||
}
|
}
|
||||||
}) {
|
None => {
|
||||||
Some(name) => {
|
tracing::error!("Cannot extract rule name from modal");
|
||||||
if let Err(e) = validation::validate_rule_name(name) {
|
|
||||||
tracing::error!("Invalid rule name: {}", e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
name.clone()
|
};
|
||||||
},
|
|
||||||
None => {
|
|
||||||
tracing::error!("Cannot extract rule name from modal");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract 'from' field with validation
|
// Extract 'from' field with validation
|
||||||
let from = match rows.get(1)
|
let from =
|
||||||
.and_then(|row| row.components.get(0))
|
match rows
|
||||||
.and_then(|component| {
|
.get(1)
|
||||||
if let ActionRowComponent::InputText(text) = component {
|
.and_then(|row| row.components.get(0))
|
||||||
text.value.as_ref()
|
.and_then(|component| {
|
||||||
} else {
|
if let ActionRowComponent::InputText(text) = component {
|
||||||
None
|
text.value.as_ref()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Some(pattern) => {
|
||||||
|
if let Err(e) = validation::validate_regex_pattern(pattern) {
|
||||||
|
tracing::error!("Invalid regex pattern: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pattern.clone()
|
||||||
}
|
}
|
||||||
}) {
|
None => {
|
||||||
Some(pattern) => {
|
tracing::error!("Cannot extract regex pattern from modal");
|
||||||
if let Err(e) = validation::validate_regex_pattern(pattern) {
|
|
||||||
tracing::error!("Invalid regex pattern: {}", e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pattern.clone()
|
};
|
||||||
},
|
|
||||||
None => {
|
|
||||||
tracing::error!("Cannot extract regex pattern from modal");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract 'to' field with validation
|
// Extract 'to' field with validation
|
||||||
let to = match rows.get(2)
|
let to = match rows
|
||||||
|
.get(2)
|
||||||
.and_then(|row| row.components.get(0))
|
.and_then(|row| row.components.get(0))
|
||||||
.and_then(|component| {
|
.and_then(|component| {
|
||||||
if let ActionRowComponent::InputText(text) = component {
|
if let ActionRowComponent::InputText(text) = component {
|
||||||
@ -118,7 +123,7 @@ impl EventHandler for Handler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
replacement.clone()
|
replacement.clone()
|
||||||
},
|
}
|
||||||
None => {
|
None => {
|
||||||
tracing::error!("Cannot extract replacement text from modal");
|
tracing::error!("Cannot extract replacement text from modal");
|
||||||
return;
|
return;
|
||||||
@ -143,12 +148,15 @@ impl EventHandler for Handler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match database.get_server_config_or_default(modal.guild_id.unwrap().get()).await {
|
match database
|
||||||
|
.get_server_config_or_default(modal.guild_id.unwrap().get())
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(Some(config)) => config,
|
Ok(Some(config)) => config,
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
tracing::error!("No server config found");
|
tracing::error!("No server config found");
|
||||||
return;
|
return;
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Database error: {}", e);
|
tracing::error!("Database error: {}", e);
|
||||||
return;
|
return;
|
||||||
@ -166,7 +174,10 @@ impl EventHandler for Handler {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = database.set_server_config(modal.guild_id.unwrap().get(), config).await {
|
if let Err(e) = database
|
||||||
|
.set_server_config(modal.guild_id.unwrap().get(), config)
|
||||||
|
.await
|
||||||
|
{
|
||||||
tracing::error!("Failed to save server config: {}", e);
|
tracing::error!("Failed to save server config: {}", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -502,8 +513,64 @@ impl EventHandler for Handler {
|
|||||||
.create_response(
|
.create_response(
|
||||||
&ctx.http,
|
&ctx.http,
|
||||||
CreateInteractionResponse::UpdateMessage(
|
CreateInteractionResponse::UpdateMessage(
|
||||||
CreateInteractionResponseMessage::new()
|
CreateInteractionResponseMessage::new().content(response_content),
|
||||||
.content(response_content),
|
),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
id if id == SET_AUTOSTART_TEXT_CHANNEL => {
|
||||||
|
let autostart_text_channel_id = match message_component.data.kind {
|
||||||
|
ComponentInteractionDataKind::StringSelect { ref values, .. } => {
|
||||||
|
if values.len() == 0 {
|
||||||
|
None
|
||||||
|
} else if values[0] == "SET_AUTOSTART_TEXT_CHANNEL_CLEAR" {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
u64::from_str_radix(
|
||||||
|
&values[0]
|
||||||
|
.strip_prefix("SET_AUTOSTART_TEXT_CHANNEL_")
|
||||||
|
.unwrap(),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("Cannot get index"),
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let data_read = ctx.data.read().await;
|
||||||
|
let database = data_read
|
||||||
|
.get::<DatabaseClientData>()
|
||||||
|
.expect("Cannot get DatabaseClientData")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut config = database
|
||||||
|
.get_server_config_or_default(message_component.guild_id.unwrap().get())
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
config.autostart_text_channel_id = autostart_text_channel_id;
|
||||||
|
database
|
||||||
|
.set_server_config(message_component.guild_id.unwrap().get(), config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_content = if autostart_text_channel_id.is_some() {
|
||||||
|
"自動参加テキストチャンネルを設定しました。"
|
||||||
|
} else {
|
||||||
|
"自動参加テキストチャンネルを解除しました。"
|
||||||
|
};
|
||||||
|
|
||||||
|
message_component
|
||||||
|
.create_response(
|
||||||
|
&ctx.http,
|
||||||
|
CreateInteractionResponse::UpdateMessage(
|
||||||
|
CreateInteractionResponseMessage::new().content(response_content),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@ -534,17 +601,15 @@ impl EventHandler for Handler {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut options = Vec::new();
|
let mut options = Vec::new();
|
||||||
|
|
||||||
// 解除オプションを追加
|
// 解除オプションを追加
|
||||||
let clear_option = CreateSelectMenuOption::new(
|
let clear_option =
|
||||||
"解除",
|
CreateSelectMenuOption::new("解除", "SET_AUTOSTART_CHANNEL_CLEAR")
|
||||||
"SET_AUTOSTART_CHANNEL_CLEAR",
|
.description("自動参加チャンネルを解除します")
|
||||||
)
|
.default_selection(autostart_channel_id == 0);
|
||||||
.description("自動参加チャンネルを解除します")
|
|
||||||
.default_selection(autostart_channel_id == 0);
|
|
||||||
options.push(clear_option);
|
options.push(clear_option);
|
||||||
|
|
||||||
for (id, channel) in channels {
|
for (id, channel) in channels.clone() {
|
||||||
if channel.kind != ChannelType::Voice {
|
if channel.kind != ChannelType::Voice {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -562,6 +627,33 @@ impl EventHandler for Handler {
|
|||||||
options.push(option);
|
options.push(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut text_channel_options = Vec::new();
|
||||||
|
|
||||||
|
let clear_option =
|
||||||
|
CreateSelectMenuOption::new("解除", "SET_AUTOSTART_TEXT_CHANNEL_CLEAR")
|
||||||
|
.description("自動参加テキストチャンネルを解除します")
|
||||||
|
.default_selection(config.autostart_text_channel_id.is_none());
|
||||||
|
text_channel_options.push(clear_option);
|
||||||
|
|
||||||
|
for (id, channel) in channels {
|
||||||
|
if channel.kind != ChannelType::Text {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let description = channel
|
||||||
|
.topic
|
||||||
|
.unwrap_or_else(|| String::from("No topic provided."));
|
||||||
|
let option = CreateSelectMenuOption::new(
|
||||||
|
&channel.name,
|
||||||
|
format!("SET_AUTOSTART_TEXT_CHANNEL_{}", id.get()),
|
||||||
|
)
|
||||||
|
.description(description)
|
||||||
|
.default_selection(
|
||||||
|
channel.id.get() == config.autostart_text_channel_id.unwrap_or(0),
|
||||||
|
);
|
||||||
|
text_channel_options.push(option);
|
||||||
|
}
|
||||||
|
|
||||||
message_component
|
message_component
|
||||||
.create_response(
|
.create_response(
|
||||||
&ctx.http,
|
&ctx.http,
|
||||||
@ -577,6 +669,16 @@ impl EventHandler for Handler {
|
|||||||
.min_values(0)
|
.min_values(0)
|
||||||
.max_values(1),
|
.max_values(1),
|
||||||
),
|
),
|
||||||
|
CreateActionRow::SelectMenu(
|
||||||
|
CreateSelectMenu::new(
|
||||||
|
"SET_AUTOSTART_TEXT_CHANNEL",
|
||||||
|
CreateSelectMenuKind::String {
|
||||||
|
options: text_channel_options,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.min_values(0)
|
||||||
|
.max_values(1),
|
||||||
|
),
|
||||||
CreateActionRow::Buttons(vec![CreateButton::new(
|
CreateActionRow::Buttons(vec![CreateButton::new(
|
||||||
"TTS_CONFIG_SERVER_BACK",
|
"TTS_CONFIG_SERVER_BACK",
|
||||||
)
|
)
|
||||||
|
@ -31,7 +31,7 @@ pub async fn message(ctx: Context, message: Message) {
|
|||||||
|
|
||||||
let instance = storage.get_mut(&guild_id).unwrap();
|
let instance = storage.get_mut(&guild_id).unwrap();
|
||||||
|
|
||||||
if instance.text_channel != message.channel_id {
|
if !instance.contains_text_channel(message.channel_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,14 @@ pub async fn voice_state_update(ctx: Context, old: Option<VoiceState>, new: Voic
|
|||||||
.expect("Cannot get songbird client.")
|
.expect("Cannot get songbird client.")
|
||||||
.clone();
|
.clone();
|
||||||
|
|
||||||
let instance = TTSInstance::new(new_channel, new_channel, guild_id);
|
let text_channel_ids =
|
||||||
|
if let Some(text_channel_id) = config.autostart_text_channel_id {
|
||||||
|
vec![text_channel_id.into(), new_channel]
|
||||||
|
} else {
|
||||||
|
vec![new_channel]
|
||||||
|
};
|
||||||
|
|
||||||
|
let instance = TTSInstance::new(text_channel_ids, new_channel, guild_id);
|
||||||
storage.insert(guild_id, instance.clone());
|
storage.insert(guild_id, instance.clone());
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
@ -82,7 +89,10 @@ pub async fn voice_state_update(ctx: Context, old: Option<VoiceState>, new: Voic
|
|||||||
let tts_client = data
|
let tts_client = data
|
||||||
.get::<TTSClientData>()
|
.get::<TTSClientData>()
|
||||||
.expect("Cannot get TTSClientData");
|
.expect("Cannot get TTSClientData");
|
||||||
let voicevox_speakers = tts_client.voicevox_client.get_speakers().await
|
let voicevox_speakers = tts_client
|
||||||
|
.voicevox_client
|
||||||
|
.get_speakers()
|
||||||
|
.await
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
tracing::error!("Failed to get VOICEVOX speakers: {}", e);
|
tracing::error!("Failed to get VOICEVOX speakers: {}", e);
|
||||||
vec!["VOICEVOX API unavailable".to_string()]
|
vec!["VOICEVOX API unavailable".to_string()]
|
||||||
@ -142,12 +152,15 @@ pub async fn voice_state_update(ctx: Context, old: Option<VoiceState>, new: Voic
|
|||||||
}
|
}
|
||||||
|
|
||||||
if del_flag {
|
if del_flag {
|
||||||
let _ = storage
|
// Archive thread if it exists
|
||||||
.get(&guild_id)
|
if let Some(&channel_id) = storage.get(&guild_id).unwrap().text_channels.first() {
|
||||||
.unwrap()
|
let http = ctx.http.clone();
|
||||||
.text_channel
|
tokio::spawn(async move {
|
||||||
.edit_thread(&ctx.http, EditThread::new().archived(true))
|
let _ = channel_id
|
||||||
.await;
|
.edit_thread(&http, EditThread::new().archived(true))
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
storage.remove(&guild_id);
|
storage.remove(&guild_id);
|
||||||
|
|
||||||
// Remove from database
|
// Remove from database
|
||||||
|
@ -15,22 +15,54 @@ use crate::tts::message::TTSMessage;
|
|||||||
pub struct TTSInstance {
|
pub struct TTSInstance {
|
||||||
#[serde(skip)] // Messageは複雑すぎるのでシリアライズしない
|
#[serde(skip)] // Messageは複雑すぎるのでシリアライズしない
|
||||||
pub before_message: Option<Message>,
|
pub before_message: Option<Message>,
|
||||||
pub text_channel: ChannelId,
|
pub text_channels: Vec<ChannelId>,
|
||||||
pub voice_channel: ChannelId,
|
pub voice_channel: ChannelId,
|
||||||
pub guild: GuildId,
|
pub guild: GuildId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TTSInstance {
|
impl TTSInstance {
|
||||||
/// Create a new TTSInstance
|
/// Create a new TTSInstance
|
||||||
pub fn new(text_channel: ChannelId, voice_channel: ChannelId, guild: GuildId) -> Self {
|
pub fn new(text_channels: Vec<ChannelId>, voice_channel: ChannelId, guild: GuildId) -> Self {
|
||||||
Self {
|
Self {
|
||||||
before_message: None,
|
before_message: None,
|
||||||
text_channel,
|
text_channels,
|
||||||
voice_channel,
|
voice_channel,
|
||||||
guild,
|
guild,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new TTSInstance with a single text channel
|
||||||
|
pub fn new_single(text_channel: ChannelId, voice_channel: ChannelId, guild: GuildId) -> Self {
|
||||||
|
Self::new(vec![text_channel], voice_channel, guild)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a text channel to the instance
|
||||||
|
pub fn add_text_channel(&mut self, channel_id: ChannelId) {
|
||||||
|
if !self.text_channels.contains(&channel_id) {
|
||||||
|
self.text_channels.push(channel_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a text channel from the instance
|
||||||
|
pub fn remove_text_channel(&mut self, channel_id: ChannelId) -> bool {
|
||||||
|
if let Some(pos) = self.text_channels.iter().position(|&x| x == channel_id) {
|
||||||
|
self.text_channels.remove(pos);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a channel is in the text channels list
|
||||||
|
pub fn contains_text_channel(&self, channel_id: ChannelId) -> bool {
|
||||||
|
self.text_channels.contains(&channel_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all text channels
|
||||||
|
pub fn get_text_channels(&self) -> &Vec<ChannelId> {
|
||||||
|
&self.text_channels
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn check_connection(&self, ctx: &Context) -> bool {
|
pub async fn check_connection(&self, ctx: &Context) -> bool {
|
||||||
let manager = match songbird::get(ctx).await {
|
let manager = match songbird::get(ctx).await {
|
||||||
Some(manager) => manager,
|
Some(manager) => manager,
|
||||||
|
Reference in New Issue
Block a user