From 57251c44fe8169e005f98030c222da5072a839fa Mon Sep 17 00:00:00 2001 From: tktcorporation Date: Sun, 29 Dec 2024 05:43:52 +0000 Subject: [PATCH 1/3] =?UTF-8?q?:sparkles:=20AFK=20=E3=81=B8=E3=81=AE?= =?UTF-8?q?=E5=87=BA=E5=85=A5=E3=82=8A=E6=99=82=E3=81=ABgreeting=E3=83=A1?= =?UTF-8?q?=E3=83=83=E3=82=BB=E3=83=BC=E3=82=B8=E3=82=92=E8=AA=AD=E3=81=BF?= =?UTF-8?q?=E4=B8=8A=E3=81=92=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 6 + src/handler/mod.rs | 116 ++++++++++++++++-- src/handler/model/speaker.rs | 35 +++++- src/handler/model/voice/mod.rs | 1 + src/handler/usecase/speech_welcome_see_you.rs | 2 + 7 files changed, 148 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74fa946..5df8025 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -901,6 +901,7 @@ dependencies = [ "aws-config", "aws-sdk-polly", "aws-types", + "futures", "html-escape", "hyper 1.5.2", "mockall", diff --git a/Cargo.toml b/Cargo.toml index 1dc900f..1de3e99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ mockall = "0.13.0" mockall_double = "0.3.0" serde = "1.0.217" sentry = { version = "0.35.0", features = ["tracing"] } +futures = "0.3" [dependencies.reqwest] version = "0.11" diff --git a/README.md b/README.md index e9c1370..2ef9e80 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ Welcome to `discord-tts-bot`! This innovative Discord bot uses text-to-speech (T - **High Precision Text-to-Speech:** Utilizes AWS Polly for high accuracy in reading both Kanji and English text, providing a natural voice experience across diverse language environments. - **Text-to-Speech in Voice Channels:** Simply type, and the bot speaks in your voice channel. - **Easy to Set Up:** A few steps and your bot is ready on your server. +- **Smart Voice Channel Interactions:** + - Welcomes users with "いらっしゃい" when they join + - Says "いってらっしゃい" when users leave + - Says "おやすみなさい" when users move to AFK channel + - Says "おはようございます" when users return from AFK channel + - All messages include the user's nickname or username for a personalized experience ## Getting Started diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 2d3b0c8..17caa86 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -20,7 +20,11 @@ use model::{ voice::Voice, }; pub mod usecase; -use usecase::set_help_message_to_activity::set_help_message_to_activity; +use usecase::{ + interface::Speaker, + set_help_message_to_activity::set_help_message_to_activity, + text_to_speech::SpeechMessage, +}; #[cfg(feature = "tts")] use usecase::{speech_welcome_see_you::speech_greeting, text_to_speech::text_to_speech}; @@ -131,21 +135,41 @@ impl EventHandler for Handler { old_voice_state: Option, new_voice_state: voice::VoiceState, ) { - let state = CurrentVoiceState::new(new_voice_state); - let change = state.change_of_states(old_voice_state.as_ref()); - let member = state.voice_member().await.expect("member is not received"); - let voice = Voice::from(&ctx, member.guild_id).await; - let role = member.role(&ctx).await; + let state = CurrentVoiceState::new(new_voice_state.clone()); + let change = state.change_of_states(old_voice_state.as_ref(), &ctx); + let member = match state.voice_member().await { + Ok(m) => m, + Err(e) => { + println!("Error getting member: {:?}", e); + return; + } + }; + let guild_id = member.guild_id; + let voice = Voice::from(&ctx, guild_id).await; + let role = match member.role(&ctx).await { + Ok(r) => r, + Err(e) => { + println!("Error getting role: {:?}", e); + return; + } + }; + if let Role::Me = role { if let ChangeOfStates::Leave = change { - voice.remove().await.unwrap(); + if let Err(e) = voice.remove().await { + println!("Error removing voice: {:?}", e); + } println!("removed"); }; return println!("This is me(bot). My entering is ignored."); } + + println!("old_voice_state: {:?}", old_voice_state); + println!("new_voice_state: {:?}", new_voice_state); + #[cfg(feature = "tts")] - speech_greeting(&ctx, &voice, &change, &member.user).await; - leave_if_alone(&ctx, &voice).await; + let _ = speech_greeting(&ctx, &voice, &change, &member.user).await; + let _ = leave_if_alone(&ctx, &voice).await; } } @@ -160,3 +184,77 @@ async fn leave_if_alone(ctx: &Context, voice: &Voice) { }, } } + +#[cfg(test)] +mod tests { + use super::*; + use mockall::predicate::*; + use mockall::mock; + + mock! { + pub Voice { + async fn speech(&self, msg: SpeechMessage); + } + } + + #[test] + fn test_afk_detection() { + // 通常状態からAFKに変更されるケース + let old_deaf = false; + let new_deaf = true; + assert!( + !old_deaf && new_deaf, + "通常状態からAFKへの変更を検知できません" + ); + + // AFKから通常状態に戻るケース + let old_deaf = true; + let new_deaf = false; + assert!( + old_deaf && !new_deaf, + "AFKから通常状態への変更を検知できません" + ); + + // 最初からAFKの状態のケース + let old_deaf = true; + let new_deaf = true; + assert!( + !(!old_deaf && new_deaf), + "既にAFKの状態で誤検知しています" + ); + } + + #[tokio::test] + async fn test_speech_messages() { + let mut mock_voice = MockVoice::new(); + + // おやすみなさいのテスト + mock_voice + .expect_speech() + .with(eq(SpeechMessage { + value: "おやすみなさい".to_string(), + })) + .times(1) + .return_once(|_| ()); + + // おはようございますのテスト + mock_voice + .expect_speech() + .with(eq(SpeechMessage { + value: "おはようございます".to_string(), + })) + .times(1) + .return_once(|_| ()); + + // メッセージの内容を確認 + let goodnight_msg = SpeechMessage { + value: "おやすみなさい".to_string(), + }; + mock_voice.speech(goodnight_msg).await; + + let morning_msg = SpeechMessage { + value: "おはようございます".to_string(), + }; + mock_voice.speech(morning_msg).await; + } +} diff --git a/src/handler/model/speaker.rs b/src/handler/model/speaker.rs index 243a812..ca39093 100644 --- a/src/handler/model/speaker.rs +++ b/src/handler/model/speaker.rs @@ -15,12 +15,12 @@ pub struct VoiceMember { } impl VoiceMember { - pub async fn role(&self, ctx: &Context) -> Role { + pub async fn role(&self, ctx: &Context) -> Result { let current_user_id = ctx.cache.current_user().id; if current_user_id == self.user.id { - return Role::Me; + return Ok(Role::Me); } - Role::Other + Ok(Role::Other) } } @@ -43,12 +43,35 @@ impl CurrentVoiceState { pub fn change_of_states( &self, previous_voice_state: Option<&voice::VoiceState>, + ctx: &Context, ) -> ChangeOfStates { match previous_voice_state { - // 他サーバーに反応しないように - Some(_) => { + Some(prev_state) => { if self.state.channel_id.is_none() { ChangeOfStates::Leave + } else if let (Some(prev_channel), Some(curr_channel)) = (prev_state.channel_id, self.state.channel_id) { + if prev_channel == curr_channel { + ChangeOfStates::Stay + } else if let Some(guild_id) = self.state.guild_id { + let afk_channel_id = ctx.cache + .guild(guild_id) + .and_then(|g| g.afk_metadata.clone()) + .map(|m| m.afk_channel_id); + + if let Some(afk_channel_id) = afk_channel_id { + if prev_channel != afk_channel_id && curr_channel == afk_channel_id { + ChangeOfStates::EnterAFK + } else if prev_channel == afk_channel_id && curr_channel != afk_channel_id { + ChangeOfStates::LeaveAFK + } else { + ChangeOfStates::Stay + } + } else { + ChangeOfStates::Stay + } + } else { + ChangeOfStates::Stay + } } else { ChangeOfStates::Stay } @@ -62,6 +85,8 @@ pub enum ChangeOfStates { Join, Leave, Stay, + EnterAFK, + LeaveAFK, } pub enum Role { diff --git a/src/handler/model/voice/mod.rs b/src/handler/model/voice/mod.rs index 74056db..adda265 100644 --- a/src/handler/model/voice/mod.rs +++ b/src/handler/model/voice/mod.rs @@ -25,6 +25,7 @@ impl Speaker for Voice { } }; let input = get_input_from_local(speech_file).await; + println!("play_input: {:?}", msg.value); play_input(&handler, input).await; } } diff --git a/src/handler/usecase/speech_welcome_see_you.rs b/src/handler/usecase/speech_welcome_see_you.rs index 5227a42..5fb37aa 100644 --- a/src/handler/usecase/speech_welcome_see_you.rs +++ b/src/handler/usecase/speech_welcome_see_you.rs @@ -23,6 +23,8 @@ fn greeting_word(change_of_states: &ChangeOfStates, name: &str) -> Option None, ChangeOfStates::Leave => Some("いってらっしゃい"), ChangeOfStates::Join => Some("いらっしゃい"), + ChangeOfStates::EnterAFK => Some("おやすみなさい"), + ChangeOfStates::LeaveAFK => Some("おはようございます"), } .map(|message_prefix| SpeechMessage { value: format!("{name}さん{message_prefix}"), From 0dfc13d00e74018843e1e4093e796897092a9dd7 Mon Sep 17 00:00:00 2001 From: tktcorporation Date: Sun, 29 Dec 2024 05:55:59 +0000 Subject: [PATCH 2/3] =?UTF-8?q?:dizzy:=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=8F=E3=83=B3=E3=83=89=E3=83=AA=E3=83=B3=E3=82=B0=E3=81=AE?= =?UTF-8?q?=E5=BC=B7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/handler/mod.rs | 86 +++++++++++-------- src/handler/model/speaker.rs | 11 ++- src/handler/model/voice/mod.rs | 46 +++++++--- src/handler/usecase/interface.rs | 2 +- src/handler/usecase/speech_welcome_see_you.rs | 10 ++- .../text_to_speech/text_to_speech_base.rs | 9 +- 6 files changed, 105 insertions(+), 59 deletions(-) diff --git a/src/handler/mod.rs b/src/handler/mod.rs index 17caa86..d1101f4 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -20,15 +20,14 @@ use model::{ voice::Voice, }; pub mod usecase; -use usecase::{ - interface::Speaker, - set_help_message_to_activity::set_help_message_to_activity, - text_to_speech::SpeechMessage, -}; +use usecase::set_help_message_to_activity::set_help_message_to_activity; +#[cfg(test)] +use usecase::text_to_speech::SpeechMessage; #[cfg(feature = "tts")] use usecase::{speech_welcome_see_you::speech_greeting, text_to_speech::text_to_speech}; + pub struct Handler; #[async_trait] @@ -61,10 +60,9 @@ impl EventHandler for Handler { } }; - let result = match command_result { + match command_result { SlashCommandResult::Simple(None) => { command.delete_response(&ctx.http).await.unwrap(); - return; } SlashCommandResult::Simple(Some(message)) => { command @@ -73,22 +71,12 @@ impl EventHandler for Handler { EditInteractionResponse::default().content(message), ) .await + .unwrap(); } SlashCommandResult::Embed(embed) => { command .edit_response(&ctx.http, EditInteractionResponse::default().embed(*embed)) .await - } - }; - match result { - Ok(_) => (), - Err(e) => { - command - .edit_response( - &ctx, - EditInteractionResponse::new().content(format!("{:?}", e)), - ) - .await .unwrap(); } } @@ -140,7 +128,10 @@ impl EventHandler for Handler { let member = match state.voice_member().await { Ok(m) => m, Err(e) => { - println!("Error getting member: {:?}", e); + sentry::capture_message( + &format!("Failed to get voice member: {:?}", e), + sentry::Level::Error, + ); return; } }; @@ -149,7 +140,10 @@ impl EventHandler for Handler { let role = match member.role(&ctx).await { Ok(r) => r, Err(e) => { - println!("Error getting role: {:?}", e); + sentry::capture_message( + &format!("Failed to get member role: {:?}", e), + sentry::Level::Error, + ); return; } }; @@ -157,7 +151,10 @@ impl EventHandler for Handler { if let Role::Me = role { if let ChangeOfStates::Leave = change { if let Err(e) = voice.remove().await { - println!("Error removing voice: {:?}", e); + sentry::capture_message( + &format!("Failed to remove voice: {:?}", e), + sentry::Level::Error, + ); } println!("removed"); }; @@ -168,19 +165,35 @@ impl EventHandler for Handler { println!("new_voice_state: {:?}", new_voice_state); #[cfg(feature = "tts")] - let _ = speech_greeting(&ctx, &voice, &change, &member.user).await; - let _ = leave_if_alone(&ctx, &voice).await; + if let Err(e) = speech_greeting(&ctx, &voice, &change, &member.user).await { + sentry::capture_message( + &format!("Failed to speech greeting: {:?}", e), + sentry::Level::Error, + ); + } + if let Err(e) = leave_if_alone(&ctx, &voice).await { + sentry::capture_message( + &format!("Failed to check/handle leave if alone: {:?}", e), + sentry::Level::Error, + ); + } } } -async fn leave_if_alone(ctx: &Context, voice: &Voice) { +async fn leave_if_alone(ctx: &Context, voice: &Voice) -> Result<(), String> { use crate::model::voice::Error; match voice.is_alone(ctx).await { - Ok(true) => voice.remove().await.unwrap(), - Ok(false) => (), + Ok(true) => { + voice + .remove() + .await + .map_err(|e| format!("Failed to remove voice: {:?}", e))?; + Ok(()) + } + Ok(false) => Ok(()), Err(e) => match e { - Error::ConnectionNotFound => (), - Error::NotInVoiceChannel => (), + Error::ConnectionNotFound => Ok(()), + Error::NotInVoiceChannel => Ok(()), }, } } @@ -188,12 +201,12 @@ async fn leave_if_alone(ctx: &Context, voice: &Voice) { #[cfg(test)] mod tests { use super::*; - use mockall::predicate::*; use mockall::mock; + use mockall::predicate::*; mock! { pub Voice { - async fn speech(&self, msg: SpeechMessage); + async fn speech(&self, msg: SpeechMessage) -> Result<(), String>; } } @@ -218,10 +231,7 @@ mod tests { // 最初からAFKの状態のケース let old_deaf = true; let new_deaf = true; - assert!( - !(!old_deaf && new_deaf), - "既にAFKの状態で誤検知しています" - ); + assert!(!(!old_deaf && new_deaf), "既にAFKの状態で誤検知しています"); } #[tokio::test] @@ -235,7 +245,7 @@ mod tests { value: "おやすみなさい".to_string(), })) .times(1) - .return_once(|_| ()); + .return_once(|_| Ok(())); // おはようございますのテスト mock_voice @@ -244,17 +254,17 @@ mod tests { value: "おはようございます".to_string(), })) .times(1) - .return_once(|_| ()); + .return_once(|_| Ok(())); // メッセージの内容を確認 let goodnight_msg = SpeechMessage { value: "おやすみなさい".to_string(), }; - mock_voice.speech(goodnight_msg).await; + mock_voice.speech(goodnight_msg).await.unwrap(); let morning_msg = SpeechMessage { value: "おはようございます".to_string(), }; - mock_voice.speech(morning_msg).await; + mock_voice.speech(morning_msg).await.unwrap(); } } diff --git a/src/handler/model/speaker.rs b/src/handler/model/speaker.rs index ca39093..c8c2736 100644 --- a/src/handler/model/speaker.rs +++ b/src/handler/model/speaker.rs @@ -49,11 +49,14 @@ impl CurrentVoiceState { Some(prev_state) => { if self.state.channel_id.is_none() { ChangeOfStates::Leave - } else if let (Some(prev_channel), Some(curr_channel)) = (prev_state.channel_id, self.state.channel_id) { + } else if let (Some(prev_channel), Some(curr_channel)) = + (prev_state.channel_id, self.state.channel_id) + { if prev_channel == curr_channel { ChangeOfStates::Stay } else if let Some(guild_id) = self.state.guild_id { - let afk_channel_id = ctx.cache + let afk_channel_id = ctx + .cache .guild(guild_id) .and_then(|g| g.afk_metadata.clone()) .map(|m| m.afk_channel_id); @@ -61,7 +64,9 @@ impl CurrentVoiceState { if let Some(afk_channel_id) = afk_channel_id { if prev_channel != afk_channel_id && curr_channel == afk_channel_id { ChangeOfStates::EnterAFK - } else if prev_channel == afk_channel_id && curr_channel != afk_channel_id { + } else if prev_channel == afk_channel_id + && curr_channel != afk_channel_id + { ChangeOfStates::LeaveAFK } else { ChangeOfStates::Stay diff --git a/src/handler/model/voice/mod.rs b/src/handler/model/voice/mod.rs index adda265..0c577b9 100644 --- a/src/handler/model/voice/mod.rs +++ b/src/handler/model/voice/mod.rs @@ -7,26 +7,44 @@ pub use crate::model::Voice; use polly::types::VoiceId; use serenity::async_trait; use songbird::input::Input; -use tracing; #[async_trait] #[cfg_attr(feature = "mock", mockall::automock)] impl Speaker for Voice { #[cfg(feature = "aws")] - async fn speech(&self, msg: SpeechMessage) { + async fn speech(&self, msg: SpeechMessage) -> Result<(), String> { if let Ok(handler) = self.handler().await { let file_path = SpeechFilePath::new(SoundPath::new(GuildPath::new(&self.guild_id))); let speech_file = match generate_speech_file(&msg.value, VoiceId::Mizuki, &file_path, false).await { Ok(file) => file, Err(e) => { - tracing::error!("Failed to generate speech file: {:?}", e); - return; + let err = format!("Failed to generate speech file: {:?}", e); + sentry::capture_message(&err, sentry::Level::Error); + return Err(err); } }; - let input = get_input_from_local(speech_file).await; - println!("play_input: {:?}", msg.value); - play_input(&handler, input).await; + + match get_input_from_local(speech_file).await { + Ok(input) => { + println!("play_input: {:?}", msg.value); + if let Err(e) = play_input(&handler, input).await { + let err = format!("Failed to play input: {:?}", e); + sentry::capture_message(&err, sentry::Level::Error); + return Err(err); + } + Ok(()) + } + Err(e) => { + let err = format!("Failed to get input from local: {:?}", e); + sentry::capture_message(&err, sentry::Level::Error); + Err(err) + } + } + } else { + let err = "Failed to get voice handler".to_string(); + sentry::capture_message(&err, sentry::Level::Error); + Err(err) } } fn guild_id(&self) -> serenity::model::id::GuildId { @@ -34,24 +52,26 @@ impl Speaker for Voice { } } -async fn get_input_from_local(file_path: String) -> Input { +async fn get_input_from_local(file_path: String) -> Result { use songbird::input::codecs::{CODEC_REGISTRY, PROBE}; - let in_memory = tokio::fs::read(file_path).await.unwrap(); + let in_memory = tokio::fs::read(file_path).await?; let in_memory_input: songbird::input::Input = songbird::input::Input::from(in_memory); in_memory_input .make_playable_async(&CODEC_REGISTRY, &PROBE) .await - .unwrap() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) } async fn play_input( handler_lock: &std::sync::Arc>, input: Input, -) { +) -> Result<(), String> { let mut handler = handler_lock.lock().await; - let audio = handler.enqueue_input(input).await; - audio.set_volume(constants::volume::VOICE).unwrap(); + audio + .set_volume(constants::volume::VOICE) + .map_err(|e| format!("Failed to set volume: {:?}", e))?; + Ok(()) } // #[cfg(test)] diff --git a/src/handler/usecase/interface.rs b/src/handler/usecase/interface.rs index c02e15f..ea3b321 100644 --- a/src/handler/usecase/interface.rs +++ b/src/handler/usecase/interface.rs @@ -5,7 +5,7 @@ use serenity::async_trait; #[async_trait] pub trait Speaker { #[cfg(feature = "aws")] - async fn speech(&self, msg: SpeechMessage); + async fn speech(&self, msg: SpeechMessage) -> Result<(), String>; fn guild_id(&self) -> serenity::model::id::GuildId; } diff --git a/src/handler/usecase/speech_welcome_see_you.rs b/src/handler/usecase/speech_welcome_see_you.rs index 5fb37aa..950f655 100644 --- a/src/handler/usecase/speech_welcome_see_you.rs +++ b/src/handler/usecase/speech_welcome_see_you.rs @@ -7,14 +7,20 @@ use serenity::model::prelude::User; use serenity::client::Context; #[cfg(feature = "tts")] -pub async fn speech_greeting(ctx: &Context, voice: &Voice, change: &ChangeOfStates, user: &User) { +pub async fn speech_greeting( + ctx: &Context, + voice: &Voice, + change: &ChangeOfStates, + user: &User, +) -> Result<(), String> { let name = match user.nick_in(ctx, voice.guild_id()).await { Some(n) => n, None => user.name.clone(), }; if let Some(message) = greeting_word(change, &name) { - voice.speech(message).await + voice.speech(message).await?; } + Ok(()) } #[cfg(feature = "tts")] diff --git a/src/handler/usecase/text_to_speech/text_to_speech_base.rs b/src/handler/usecase/text_to_speech/text_to_speech_base.rs index fe57400..01ad986 100644 --- a/src/handler/usecase/text_to_speech/text_to_speech_base.rs +++ b/src/handler/usecase/text_to_speech/text_to_speech_base.rs @@ -27,7 +27,12 @@ pub async fn text_to_speech(speaker: Box, msg: Messag if is_ignore_channel(speech_options.read_channel_id, &msg) { return; } - speaker.speech(msg.to_speech_message(speech_options)).await; + if let Err(e) = speaker.speech(msg.to_speech_message(speech_options)).await { + sentry::capture_message( + &format!("Failed to speech message: {:?}", e), + sentry::Level::Error, + ); + } } #[cfg(feature = "tts")] @@ -52,7 +57,7 @@ mod tests { async fn test_text_to_speech() { let mut speaker = MockSpeaker::new(); let msg = message_factory("some message"); - speaker.expect_speech().times(1).return_const(()); + speaker.expect_speech().times(1).return_const(Ok(())); speaker .expect_guild_id() .times(1) From 9b726aec40cc5bd683c9581908d0ba88e5eb8208 Mon Sep 17 00:00:00 2001 From: tktcorporation Date: Sun, 29 Dec 2024 05:58:38 +0000 Subject: [PATCH 3/3] =?UTF-8?q?:dizzy:=20=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=83=AC=E3=83=9D=E3=83=BC=E3=83=88=E3=81=AE=E9=96=A2=E6=95=B0?= =?UTF-8?q?=E3=82=92=E3=81=BE=E3=81=A8=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/usecase/services/play/service.rs | 2 +- src/error.rs | 9 ++++++ src/handler/error.rs | 7 +++++ src/handler/mod.rs | 30 +++++-------------- src/handler/model/voice/mod.rs | 17 ++++++----- src/handler/usecase/interface.rs | 7 +++-- .../text_to_speech/text_to_speech_base.rs | 10 +++---- src/infrastructure/aws/tts.rs | 23 +++++++++----- .../path_router/shared_sound.rs | 6 ++++ src/lib.rs | 6 ++++ src/main.rs | 19 ++++-------- src/model/mod.rs | 14 +++++++-- 12 files changed, 87 insertions(+), 63 deletions(-) create mode 100644 src/error.rs create mode 100644 src/handler/error.rs create mode 100644 src/lib.rs diff --git a/src/commands/usecase/services/play/service.rs b/src/commands/usecase/services/play/service.rs index dc0e622..c7a63ad 100644 --- a/src/commands/usecase/services/play/service.rs +++ b/src/commands/usecase/services/play/service.rs @@ -1,7 +1,7 @@ use serenity::{client::Context, model}; use crate::constants; -use crate::HttpKey; +use crate::model::HttpKey; use super::Error; use super::TrackTiming; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ea881cf --- /dev/null +++ b/src/error.rs @@ -0,0 +1,9 @@ +use sentry; + +pub fn report_error(message: &str) { + sentry::capture_message(message, sentry::Level::Error); +} + +pub fn format_err(context: &str, err: T) -> String { + format!("{}: {:?}", context, err) +} diff --git a/src/handler/error.rs b/src/handler/error.rs new file mode 100644 index 0000000..d9de3ef --- /dev/null +++ b/src/handler/error.rs @@ -0,0 +1,7 @@ +pub fn report_error(message: &str) { + sentry::capture_message(message, sentry::Level::Error); +} + +pub fn format_err(context: &str, err: T) -> String { + format!("{}: {:?}", context, err) +} diff --git a/src/handler/mod.rs b/src/handler/mod.rs index d1101f4..8f7437d 100644 --- a/src/handler/mod.rs +++ b/src/handler/mod.rs @@ -11,7 +11,9 @@ use serenity::{ use serenity::model::gateway::Ready; +mod error; mod model; +use error::{format_err, report_error}; use model::context::Context as Ctx; use crate::commands::slash_commands::{SlashCommandResult, SlashCommands}; @@ -27,7 +29,6 @@ use usecase::text_to_speech::SpeechMessage; #[cfg(feature = "tts")] use usecase::{speech_welcome_see_you::speech_greeting, text_to_speech::text_to_speech}; - pub struct Handler; #[async_trait] @@ -128,10 +129,7 @@ impl EventHandler for Handler { let member = match state.voice_member().await { Ok(m) => m, Err(e) => { - sentry::capture_message( - &format!("Failed to get voice member: {:?}", e), - sentry::Level::Error, - ); + report_error(&format_err("Failed to get voice member", e)); return; } }; @@ -140,10 +138,7 @@ impl EventHandler for Handler { let role = match member.role(&ctx).await { Ok(r) => r, Err(e) => { - sentry::capture_message( - &format!("Failed to get member role: {:?}", e), - sentry::Level::Error, - ); + report_error(&format_err("Failed to get member role", e)); return; } }; @@ -151,10 +146,7 @@ impl EventHandler for Handler { if let Role::Me = role { if let ChangeOfStates::Leave = change { if let Err(e) = voice.remove().await { - sentry::capture_message( - &format!("Failed to remove voice: {:?}", e), - sentry::Level::Error, - ); + report_error(&format_err("Failed to remove voice", e)); } println!("removed"); }; @@ -166,16 +158,10 @@ impl EventHandler for Handler { #[cfg(feature = "tts")] if let Err(e) = speech_greeting(&ctx, &voice, &change, &member.user).await { - sentry::capture_message( - &format!("Failed to speech greeting: {:?}", e), - sentry::Level::Error, - ); + report_error(&format_err("Failed to speech greeting", e)); } if let Err(e) = leave_if_alone(&ctx, &voice).await { - sentry::capture_message( - &format!("Failed to check/handle leave if alone: {:?}", e), - sentry::Level::Error, - ); + report_error(&format_err("Failed to check/handle leave if alone", e)); } } } @@ -231,7 +217,7 @@ mod tests { // 最初からAFKの状態のケース let old_deaf = true; let new_deaf = true; - assert!(!(!old_deaf && new_deaf), "既にAFKの状態で誤検知しています"); + assert!(old_deaf || !new_deaf, "既にAFKの状態で誤検知しています"); } #[tokio::test] diff --git a/src/handler/model/voice/mod.rs b/src/handler/model/voice/mod.rs index 0c577b9..e9df62a 100644 --- a/src/handler/model/voice/mod.rs +++ b/src/handler/model/voice/mod.rs @@ -1,5 +1,6 @@ use super::super::usecase::{interface::Speaker, text_to_speech::SpeechMessage}; use crate::constants; +use crate::handler::error::{format_err, report_error}; #[cfg(feature = "aws")] use crate::infrastructure::tts::generate_speech_file; use crate::infrastructure::{GuildPath, SoundPath, SpeechFilePath}; @@ -11,7 +12,7 @@ use songbird::input::Input; #[async_trait] #[cfg_attr(feature = "mock", mockall::automock)] impl Speaker for Voice { - #[cfg(feature = "aws")] + #[cfg(feature = "tts")] async fn speech(&self, msg: SpeechMessage) -> Result<(), String> { if let Ok(handler) = self.handler().await { let file_path = SpeechFilePath::new(SoundPath::new(GuildPath::new(&self.guild_id))); @@ -19,8 +20,8 @@ impl Speaker for Voice { match generate_speech_file(&msg.value, VoiceId::Mizuki, &file_path, false).await { Ok(file) => file, Err(e) => { - let err = format!("Failed to generate speech file: {:?}", e); - sentry::capture_message(&err, sentry::Level::Error); + let err = format_err("Failed to generate speech file", e); + report_error(&err); return Err(err); } }; @@ -29,21 +30,21 @@ impl Speaker for Voice { Ok(input) => { println!("play_input: {:?}", msg.value); if let Err(e) = play_input(&handler, input).await { - let err = format!("Failed to play input: {:?}", e); - sentry::capture_message(&err, sentry::Level::Error); + let err = format_err("Failed to play input", e); + report_error(&err); return Err(err); } Ok(()) } Err(e) => { - let err = format!("Failed to get input from local: {:?}", e); - sentry::capture_message(&err, sentry::Level::Error); + let err = format_err("Failed to get input from local", e); + report_error(&err); Err(err) } } } else { let err = "Failed to get voice handler".to_string(); - sentry::capture_message(&err, sentry::Level::Error); + report_error(&err); Err(err) } } diff --git a/src/handler/usecase/interface.rs b/src/handler/usecase/interface.rs index ea3b321..421e7d8 100644 --- a/src/handler/usecase/interface.rs +++ b/src/handler/usecase/interface.rs @@ -1,12 +1,13 @@ +#[cfg(feature = "tts")] use super::text_to_speech::SpeechMessage; -use serenity::async_trait; +use serenity::{async_trait, model::id::GuildId}; #[cfg_attr(test, mockall::automock)] #[async_trait] pub trait Speaker { - #[cfg(feature = "aws")] + #[cfg(feature = "tts")] async fn speech(&self, msg: SpeechMessage) -> Result<(), String>; - fn guild_id(&self) -> serenity::model::id::GuildId; + fn guild_id(&self) -> GuildId; } #[cfg_attr(test, mockall::automock)] diff --git a/src/handler/usecase/text_to_speech/text_to_speech_base.rs b/src/handler/usecase/text_to_speech/text_to_speech_base.rs index 01ad986..9c935cf 100644 --- a/src/handler/usecase/text_to_speech/text_to_speech_base.rs +++ b/src/handler/usecase/text_to_speech/text_to_speech_base.rs @@ -5,6 +5,8 @@ use super::config; #[cfg(feature = "tts")] use super::text_to_speech_message::Message; #[cfg(feature = "tts")] +use crate::handler::error::{format_err, report_error}; +#[cfg(feature = "tts")] use crate::infrastructure::GuildPath; #[derive(Debug, PartialEq, Eq)] @@ -28,10 +30,7 @@ pub async fn text_to_speech(speaker: Box, msg: Messag return; } if let Err(e) = speaker.speech(msg.to_speech_message(speech_options)).await { - sentry::capture_message( - &format!("Failed to speech message: {:?}", e), - sentry::Level::Error, - ); + report_error(&format_err("Failed to speech message", e)); } } @@ -47,9 +46,8 @@ fn is_ignore_channel(read_channel_id: Option, msg: &Message) -> bool { #[cfg(test)] mod tests { - #[cfg(test)] - use super::super::super::interface::MockSpeaker; use super::*; + use crate::handler::usecase::interface::MockSpeaker; use regex::Regex; use serenity::model::{channel::Message as SerenityMessage, id::GuildId}; diff --git a/src/infrastructure/aws/tts.rs b/src/infrastructure/aws/tts.rs index 8f43d41..50678e6 100644 --- a/src/infrastructure/aws/tts.rs +++ b/src/infrastructure/aws/tts.rs @@ -12,14 +12,23 @@ use super::SpeechFilePath; /// ## Examples /// /// ```no_run +/// use polly::types::VoiceId; +/// use std::path::{Path, PathBuf}; +/// use discord_tts_bot::infrastructure::tts::generate_speech_file; +/// use discord_tts_bot::infrastructure::SpeechFilePath; +/// +/// # async fn run() -> Result<(), Box> { +/// let path = PathBuf::from("sample"); +/// let file_path = SpeechFilePath::from(path); /// let result = generate_speech_file( -/// String::from("おはようございます"), -/// VoiceId::Mizuki, -/// "sample", -/// true, -/// ) -/// .await; -/// Path::new(result.unwrap()).exists(); // true or false +/// "おはようございます", +/// VoiceId::Mizuki, +/// &file_path, +/// true, +/// ).await?; +/// assert!(Path::new(&result).exists()); +/// # Ok(()) +/// # } /// ``` pub async fn generate_speech_file( content: &str, diff --git a/src/infrastructure/path_router/shared_sound.rs b/src/infrastructure/path_router/shared_sound.rs index 05491ee..d3df317 100644 --- a/src/infrastructure/path_router/shared_sound.rs +++ b/src/infrastructure/path_router/shared_sound.rs @@ -2,6 +2,12 @@ pub struct Path { value: std::path::PathBuf, } +impl Default for Path { + fn default() -> Self { + Self::new() + } +} + impl Path { pub fn new() -> Self { let value: std::path::PathBuf = super::root_path().join("sounds"); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d90f62e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod commands; +pub mod constants; +pub mod error; +pub mod handler; +pub mod infrastructure; +pub mod model; diff --git a/src/main.rs b/src/main.rs index dc7f99b..04558c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,25 +2,16 @@ use std::env; use reqwest::Client as HttpClient; use serenity::{client::Client, prelude::GatewayIntents}; -use songbird::typemap::TypeMapKey; - use songbird::SerenityInit; -mod handler; -use handler::Handler; - -mod infrastructure; +use discord_tts_bot::handler::Handler; +use discord_tts_bot::model::HttpKey; mod commands; - -mod model; - mod constants; - -pub struct HttpKey; -impl TypeMapKey for HttpKey { - type Value = HttpClient; -} +mod handler; +mod infrastructure; +mod model; #[tokio::main] async fn main() { diff --git a/src/model/mod.rs b/src/model/mod.rs index dd7355a..a2952c5 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1,5 +1,15 @@ +use reqwest::Client; +use serenity::prelude::TypeMapKey; + +pub mod message; pub mod voice; -pub use voice::Voice; -mod message; + #[cfg(feature = "tts")] pub use message::Message; +pub use voice::Voice; + +pub struct HttpKey; + +impl TypeMapKey for HttpKey { + type Value = Client; +}