diff --git a/src/commands/leave.rs b/src/commands/leave.rs index 3f53ff6..8d5f31b 100644 --- a/src/commands/leave.rs +++ b/src/commands/leave.rs @@ -1,10 +1,40 @@ use crate::state::{State, StateRef}; +use anyhow::Context; use std::{error::Error, sync::Arc}; use twilight_model::{ gateway::payload::incoming::InteractionCreate, id::{marker::GuildMarker, Id}, }; +pub(crate) async fn leave_if_alone( + guild_id: Id, + state: State, +) -> Result<(), Box> { + let user = state + .cache + .current_user() + .context("Cannot get current user")?; + let user_voice_state = state + .cache + .voice_state(user.id, guild_id) + .context("Cannot get voice state")?; + let channel = state + .cache + .channel(user_voice_state.channel_id()) + .context("Cannot get channel")?; + let channel_voice_states = state + .cache + .voice_channel_states(channel.id) + .context("Cannot get voice channel")?; + let count = channel_voice_states.count(); + + // count is 1 if the bot is the only one in the channel + if count == 1 { + leave_channel(guild_id, Arc::clone(&state)).await?; + } + Ok(()) +} + pub(crate) async fn leave_channel( guild_id: Id, state: Arc, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4213714..9533fe3 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -3,7 +3,7 @@ pub(crate) use join::join; mod leave; pub(crate) use leave::leave; -pub(crate) use leave::leave_channel; +pub(crate) use leave::leave_if_alone; mod pause; pub(crate) use pause::pause; diff --git a/src/handler.rs b/src/handler.rs index 36a5af2..e693dd4 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,75 +1,19 @@ use crate::commands::queue::{build_action_row, build_queue_embeds, TRACKS_PER_PAGE}; use crate::commands::{ - delete, join, leave, leave_channel, loop_queue, pause, play, queue, resume, skip, stop, + delete, join, leave, leave_if_alone, loop_queue, pause, play, queue, resume, skip, stop, }; +use crate::interaction_commands::InteractionCommand; use crate::state::State; -use futures::Future; -use std::error::Error; +use crate::utils::spawn; +use anyhow::Context; use std::sync::Arc; -use tracing::debug; use twilight_gateway::Event; -use twilight_model::application::interaction::application_command::{ - CommandData, CommandOptionValue, -}; +use twilight_model::application::interaction::message_component::MessageComponentInteractionData; use twilight_model::application::interaction::InteractionData; -use twilight_model::gateway::payload::incoming::VoiceStateUpdate; +use twilight_model::gateway::payload::incoming::InteractionCreate; use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType}; use twilight_util::builder::InteractionResponseDataBuilder; -#[derive(Debug)] -enum InteractionCommand { - Play(String), - Stop, - Pause, - Skip, - Loop, - Resume, - Leave, - Join, - Queue, - NotImplemented, -} - -fn spawn( - fut: impl Future>> + Send + 'static, -) { - tokio::spawn(async move { - if let Err(why) = fut.await { - tracing::debug!("handler error: {:?}", why); - } - }); -} - -pub(crate) async fn leave_if_alone( - update: VoiceStateUpdate, - state: State, -) -> Result<(), Box> { - let guild_id = update.guild_id.ok_or("Guild ID not found")?; - let user = state - .cache - .current_user() - .ok_or("Cannot get current user")?; - let user_voice_state = state - .cache - .voice_state(user.id, guild_id) - .ok_or("Cannot get voice state")?; - let channel = state - .cache - .channel(user_voice_state.channel_id()) - .ok_or("Cannot get channel")?; - let channel_voice_states = state - .cache - .voice_channel_states(channel.id) - .ok_or("Cannot get voice channel")?; - let count = channel_voice_states.count(); - - // count is 1 if the bot is the only one in the channel - if count == 1 { - leave_channel(guild_id, Arc::clone(&state)).await?; - } - Ok(()) -} - pub(crate) struct Handler { state: State, } @@ -79,6 +23,13 @@ impl Handler { Self { state } } pub(crate) async fn act(&self, event: Event) -> anyhow::Result<()> { + self.handle_messages(&event).await?; + self.handle_voice_state_update(&event).await?; + self.handle_interaction(&event).await?; + Ok(()) + } + + async fn handle_messages(&self, event: &Event) -> anyhow::Result<()> { match event { Event::MessageCreate(message) if message.content.starts_with('!') => { if message.content.contains("!delete") { @@ -86,123 +37,102 @@ impl Handler { } Ok(()) } + _ => Ok(()), + } + } + + async fn handle_voice_state_update(&self, event: &Event) -> anyhow::Result<()> { + match event { Event::VoiceStateUpdate(update) => { - spawn(leave_if_alone(*update.clone(), Arc::clone(&self.state))); + let guild_id = update.guild_id.context("Guild ID not found")?; + spawn(leave_if_alone(guild_id, Arc::clone(&self.state))); Ok(()) } - Event::InteractionCreate(interaction) => { - tracing::info!("interaction: {:?}", &interaction); - match &interaction.data { - Some(InteractionData::ApplicationCommand(command)) => { - let interaction_command = parse_interaction_command(command); - debug!("{:?}", interaction_command); - match interaction_command { - InteractionCommand::Play(query) => { - spawn(play(interaction, Arc::clone(&self.state), query)) - } - InteractionCommand::Stop => { - spawn(stop(interaction, Arc::clone(&self.state))) - } - InteractionCommand::Pause => { - spawn(pause(interaction, Arc::clone(&self.state))) - } - InteractionCommand::Skip => { - spawn(skip(interaction, Arc::clone(&self.state))) - } - InteractionCommand::Loop => { - spawn(loop_queue(interaction, Arc::clone(&self.state))) - } - InteractionCommand::Resume => { - spawn(resume(interaction, Arc::clone(&self.state))) - } - InteractionCommand::Leave => { - spawn(leave(interaction, Arc::clone(&self.state))) - } - InteractionCommand::Join => { - spawn(join(interaction, Arc::clone(&self.state))) - } - InteractionCommand::Queue => { - spawn(queue(interaction, Arc::clone(&self.state))) - } - _ => {} - } - Ok(()) - } - Some(InteractionData::MessageComponent(data)) => { - tracing::info!("message component: {:?}", data); + _ => Ok(()), + } + } - if !data.custom_id.starts_with("page:") { - return Ok(()); - } - let page = data - .custom_id - .trim_start_matches("page:") - .parse::() - .unwrap_or(0); - tracing::info!("page: {:?}", page); - - if let Some(guild_id) = interaction.guild_id { - let mut queue = Vec::new(); - if let Some(call_lock) = self.state.songbird.get(guild_id) { - let call = call_lock.lock().await; - queue = call.queue().current_queue(); - } - let n_pages = (queue.len() + TRACKS_PER_PAGE - 1) / TRACKS_PER_PAGE; - let page = page.min(n_pages - 1).max(0); - let embeds = build_queue_embeds(&queue, page).await; - let action_row = build_action_row(page, n_pages); - - let interaction_response_data = InteractionResponseDataBuilder::new() - .embeds(embeds) - .components(action_row) - .build(); - let response = InteractionResponse { - kind: InteractionResponseType::UpdateMessage, - data: Some(interaction_response_data), - }; - self.state - .http - .interaction(interaction.application_id) - .create_response(interaction.id, &interaction.token, &response) - .await?; - Ok(()) - } else { - Ok(()) - } - } - _ => Ok(()), + async fn handle_interaction(&self, event: &Event) -> anyhow::Result<()> { + match event { + Event::InteractionCreate(interaction) => match &interaction.data { + Some(InteractionData::ApplicationCommand(command)) => { + self.handle_application_command(command.clone().into(), interaction.clone()) } + Some(InteractionData::MessageComponent(data)) => { + self.handle_message_component(data, interaction.clone()) + .await + } + _ => Ok(()), + }, + _ => Ok(()), + } + } + + fn handle_application_command( + &self, + command: InteractionCommand, + interaction: Box, + ) -> anyhow::Result<()> { + { + match command { + InteractionCommand::Play(query) => { + spawn(play(interaction, Arc::clone(&self.state), query)) + } + InteractionCommand::Stop => spawn(stop(interaction, Arc::clone(&self.state))), + InteractionCommand::Pause => spawn(pause(interaction, Arc::clone(&self.state))), + InteractionCommand::Skip => spawn(skip(interaction, Arc::clone(&self.state))), + InteractionCommand::Loop => spawn(loop_queue(interaction, Arc::clone(&self.state))), + InteractionCommand::Resume => spawn(resume(interaction, Arc::clone(&self.state))), + InteractionCommand::Leave => spawn(leave(interaction, Arc::clone(&self.state))), + InteractionCommand::Join => spawn(join(interaction, Arc::clone(&self.state))), + InteractionCommand::Queue => spawn(queue(interaction, Arc::clone(&self.state))), + _ => {} } - event => { - tracing::info!("unhandled event: {:?}", event); - Ok(()) + Ok(()) + } + } + + async fn handle_message_component( + &self, + data: &MessageComponentInteractionData, + interaction: Box, + ) -> anyhow::Result<()> { + if !data.custom_id.starts_with("page:") { + return Ok(()); + } + let page = data + .custom_id + .trim_start_matches("page:") + .parse::() + .unwrap_or(0); + + if let Some(guild_id) = interaction.guild_id { + let mut queue = Vec::new(); + if let Some(call_lock) = self.state.songbird.get(guild_id) { + let call = call_lock.lock().await; + queue = call.queue().current_queue(); } + let n_pages = (queue.len() + TRACKS_PER_PAGE - 1) / TRACKS_PER_PAGE; + let page = page.min(n_pages - 1).max(0); + let embeds = build_queue_embeds(&queue, page).await; + let action_row = build_action_row(page, n_pages); + + let interaction_response_data = InteractionResponseDataBuilder::new() + .embeds(embeds) + .components(action_row) + .build(); + let response = InteractionResponse { + kind: InteractionResponseType::UpdateMessage, + data: Some(interaction_response_data), + }; + self.state + .http + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) + .await?; + Ok(()) + } else { + Ok(()) } } } - -fn parse_interaction_command(command: &CommandData) -> InteractionCommand { - debug!("command: {:?}", command); - match command.name.as_str() { - "play" => { - if let Some(query_option) = command.options.iter().find(|opt| opt.name == "query") { - if let CommandOptionValue::String(query) = &query_option.value { - InteractionCommand::Play(query.clone()) - } else { - InteractionCommand::NotImplemented - } - } else { - InteractionCommand::NotImplemented - } - } - "stop" => InteractionCommand::Stop, - "pause" => InteractionCommand::Pause, - "skip" => InteractionCommand::Skip, - "loop" => InteractionCommand::Loop, - "resume" => InteractionCommand::Resume, - "leave" => InteractionCommand::Leave, - "join" => InteractionCommand::Join, - "queue" => InteractionCommand::Queue, - _ => InteractionCommand::NotImplemented, - } -} diff --git a/src/interaction_commands.rs b/src/interaction_commands.rs new file mode 100644 index 0000000..1f62c7b --- /dev/null +++ b/src/interaction_commands.rs @@ -0,0 +1,44 @@ +use twilight_model::application::interaction::application_command::{ + CommandData, CommandOptionValue, +}; + +#[derive(Debug)] +pub(crate) enum InteractionCommand { + Play(String), + Stop, + Pause, + Skip, + Loop, + Resume, + Leave, + Join, + Queue, + NotImplemented, +} + +impl From> for InteractionCommand { + fn from(command: Box) -> InteractionCommand { + match command.name.as_str() { + "play" => { + if let Some(query_option) = command.options.iter().find(|opt| opt.name == "query") { + if let CommandOptionValue::String(query) = &query_option.value { + InteractionCommand::Play(query.clone()) + } else { + InteractionCommand::NotImplemented + } + } else { + InteractionCommand::NotImplemented + } + } + "stop" => InteractionCommand::Stop, + "pause" => InteractionCommand::Pause, + "skip" => InteractionCommand::Skip, + "loop" => InteractionCommand::Loop, + "resume" => InteractionCommand::Resume, + "leave" => InteractionCommand::Leave, + "join" => InteractionCommand::Join, + "queue" => InteractionCommand::Queue, + _ => InteractionCommand::NotImplemented, + } + } +} diff --git a/src/main.rs b/src/main.rs index ed9b954..9ffa39e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,11 @@ mod handler; use handler::Handler; mod colors; mod commands; +mod interaction_commands; mod metadata; mod signal; mod state; +mod utils; use crate::commands::get_chat_commands; use dotenv::dotenv; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..fb6cf37 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,12 @@ +use futures::Future; +use std::error::Error; + +pub(crate) fn spawn( + fut: impl Future>> + Send + 'static, +) { + tokio::spawn(async move { + if let Err(why) = fut.await { + tracing::debug!("handler error: {:?}", why); + } + }); +}