Skip to content

Commit

Permalink
Merge pull request #521 from tktcorporation/35/feature/afk-message
Browse files Browse the repository at this point in the history
✨ AFK への出入り時にgreetingメッセージを読み上げるようにする
  • Loading branch information
tktcorporation authored Dec 29, 2024
2 parents 96f0c92 + 9b726ae commit 8987e09
Show file tree
Hide file tree
Showing 17 changed files with 282 additions and 78 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/commands/usecase/services/play/service.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use sentry;

pub fn report_error(message: &str) {
sentry::capture_message(message, sentry::Level::Error);
}

pub fn format_err<T: std::fmt::Debug>(context: &str, err: T) -> String {
format!("{}: {:?}", context, err)
}
7 changes: 7 additions & 0 deletions src/handler/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub fn report_error(message: &str) {
sentry::capture_message(message, sentry::Level::Error);
}

pub fn format_err<T: std::fmt::Debug>(context: &str, err: T) -> String {
format!("{}: {:?}", context, err)
}
146 changes: 120 additions & 26 deletions src/handler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -21,6 +23,8 @@ use model::{
};
pub mod usecase;
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};
Expand Down Expand Up @@ -57,10 +61,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
Expand All @@ -69,22 +72,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();
}
}
Expand Down Expand Up @@ -131,32 +124,133 @@ impl EventHandler for Handler {
old_voice_state: Option<voice::VoiceState>,
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) => {
report_error(&format_err("Failed to get voice 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) => {
report_error(&format_err("Failed to get member role", e));
return;
}
};

if let Role::Me = role {
if let ChangeOfStates::Leave = change {
voice.remove().await.unwrap();
if let Err(e) = voice.remove().await {
report_error(&format_err("Failed to remove 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;
if let Err(e) = speech_greeting(&ctx, &voice, &change, &member.user).await {
report_error(&format_err("Failed to speech greeting", e));
}
if let Err(e) = leave_if_alone(&ctx, &voice).await {
report_error(&format_err("Failed to check/handle leave if alone", e));
}
}
}

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(()),
},
}
}

#[cfg(test)]
mod tests {
use super::*;
use mockall::mock;
use mockall::predicate::*;

mock! {
pub Voice {
async fn speech(&self, msg: SpeechMessage) -> Result<(), String>;
}
}

#[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(|_| Ok(()));

// おはようございますのテスト
mock_voice
.expect_speech()
.with(eq(SpeechMessage {
value: "おはようございます".to_string(),
}))
.times(1)
.return_once(|_| Ok(()));

// メッセージの内容を確認
let goodnight_msg = SpeechMessage {
value: "おやすみなさい".to_string(),
};
mock_voice.speech(goodnight_msg).await.unwrap();

let morning_msg = SpeechMessage {
value: "おはようございます".to_string(),
};
mock_voice.speech(morning_msg).await.unwrap();
}
}
40 changes: 35 additions & 5 deletions src/handler/model/speaker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Role, String> {
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)
}
}

Expand All @@ -43,12 +43,40 @@ 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
}
Expand All @@ -62,6 +90,8 @@ pub enum ChangeOfStates {
Join,
Leave,
Stay,
EnterAFK,
LeaveAFK,
}

pub enum Role {
Expand Down
Loading

0 comments on commit 8987e09

Please sign in to comment.