From 872464bd31ec10458bf84ce3cfbea791c5c03ec7 Mon Sep 17 00:00:00 2001 From: Johannes Heuel Date: Sat, 17 Feb 2024 14:34:55 +0100 Subject: [PATCH] use interactions instead of chat commands --- src/commands/delete.rs | 85 ++++++++++++++++++------- src/commands/join.rs | 69 +++++++++++++++++---- src/commands/leave.rs | 16 +++-- src/commands/mod.rs | 5 +- src/commands/pause.rs | 34 +++++++--- src/commands/play.rs | 50 ++++++++++----- src/commands/queue.rs | 37 ++++++++--- src/commands/resume.rs | 35 ++++++++--- src/commands/stop.rs | 35 ++++++++--- src/handler.rs | 137 +++++++++++++++++++++++++++-------------- src/main.rs | 2 +- 11 files changed, 369 insertions(+), 136 deletions(-) diff --git a/src/commands/delete.rs b/src/commands/delete.rs index ad119c1..39392c3 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,43 +1,84 @@ use std::{env, error::Error, num::NonZeroU64, time::Duration}; use tokio::time::sleep; -use tracing::info; -use twilight_model::{channel::Message, id::Id}; +use tracing::{debug, info}; +use twilight_model::{ + application::interaction::Interaction, + channel::message::MessageFlags, + http::interaction::{InteractionResponse, InteractionResponseType}, + id::Id, +}; +use twilight_util::builder::InteractionResponseDataBuilder; use crate::state::State; pub(crate) async fn delete( - msg: Message, + interaction: Interaction, state: State, + count: i64, ) -> Result<(), Box> { + debug!( + "delete command in guild {:?} in channel {:?} by {:?}", + interaction.guild_id, + interaction.channel, + interaction.author(), + ); + let admin = env::var("ADMIN")?.parse::()?; - if msg.author.id != Id::from(NonZeroU64::new(admin).unwrap()) { + if interaction.author_id() != Some(Id::from(NonZeroU64::new(admin).unwrap())) { + let interaction_response_data = InteractionResponseDataBuilder::new() + .content("You do not have permissions to delete messages.") + .flags(MessageFlags::EPHEMERAL) + .build(); + + let response = InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(interaction_response_data), + }; + + state + .http + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) + .await?; + return Ok(()); } - let n = msg - .content - .split(' ') - .last() - .unwrap() - .parse::() - .unwrap_or(1); - if n > 100 { + let Some(channel) = interaction.channel else { return Ok(()); - } + }; + let Some(message_id) = channel.last_message_id else { + return Ok(()); + }; + + let count = count.max(1).min(100) as u16; + + let interaction_response_data = InteractionResponseDataBuilder::new() + .content(format!("Deleting {count} messages.")) + .flags(MessageFlags::EPHEMERAL) + .build(); + + let response = InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(interaction_response_data), + }; + + state + .http + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) + .await?; + let messages = state .http - .channel_messages(msg.channel_id) - .before(msg.id) - .limit(n)? + .channel_messages(channel.id) + .before(message_id.cast()) + .limit(count)? .await? .model() .await?; - state.http.delete_message(msg.channel_id, msg.id).await?; for message in messages { - info!("Delete message: {:?}: {:?}", message.author.name, message); - state - .http - .delete_message(msg.channel_id, message.id) - .await?; + debug!("Delete message: {:?}: {:?}", message.author.name, message); + state.http.delete_message(channel.id, message.id).await?; sleep(Duration::from_secs(5)).await; } Ok(()) diff --git a/src/commands/join.rs b/src/commands/join.rs index 5dfb195..9da415f 100644 --- a/src/commands/join.rs +++ b/src/commands/join.rs @@ -1,34 +1,81 @@ -use std::{error::Error, num::NonZeroU64}; -use twilight_model::channel::Message; - use crate::state::State; +use std::error::Error; +use tracing::debug; +use twilight_model::{ + application::interaction::Interaction, + channel::message::MessageFlags, + http::interaction::{InteractionResponse, InteractionResponseType}, + id::{ + marker::{GuildMarker, UserMarker}, + Id, + }, +}; +use twilight_util::builder::InteractionResponseDataBuilder; -pub(crate) async fn join( - msg: Message, +pub(crate) async fn join_channel( state: State, + guild_id: Id, + user_id: Id, ) -> Result<(), Box> { - let guild_id = msg.guild_id.ok_or("No guild id attached to the message.")?; - let user_id = msg.author.id; + debug!("join user {:?} in guild {:?}", user_id, guild_id); + let channel_id = state .cache .voice_state(user_id, guild_id) .ok_or("Cannot get voice state for user")? .channel_id(); - let channel_id = - NonZeroU64::new(channel_id.into()).ok_or("Joined voice channel must have nonzero ID.")?; // join the voice channel state .songbird - .join(guild_id, channel_id) + .join(guild_id.cast(), channel_id) .await .map_err(|e| format!("Could not join voice channel: {:?}", e))?; // signal that we are not listening - if let Some(call_lock) = state.songbird.get(guild_id) { + if let Some(call_lock) = state.songbird.get(guild_id.cast()) { let mut call = call_lock.lock().await; call.deafen(true).await?; } Ok(()) } + +pub(crate) async fn join( + interaction: Interaction, + state: State, +) -> Result<(), Box> { + debug!( + "join command in guild {:?} in channel {:?} by {:?}", + interaction.guild_id, + interaction.channel, + interaction.author(), + ); + + let Some(guild_id) = interaction.guild_id else { + return Ok(()); + }; + let Some(author_id) = interaction.author_id() else { + return Ok(()); + }; + + join_channel(state.clone(), guild_id, author_id).await?; + + let interaction_response_data = InteractionResponseDataBuilder::new() + .content("Bin da Brudi!") + .flags(MessageFlags::EPHEMERAL) + .build(); + + let response = InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(interaction_response_data), + }; + + state + .http + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) + .await?; + + Ok(()) +} diff --git a/src/commands/leave.rs b/src/commands/leave.rs index f0f6138..a936d08 100644 --- a/src/commands/leave.rs +++ b/src/commands/leave.rs @@ -1,17 +1,21 @@ use crate::state::State; use std::error::Error; -use twilight_model::channel::Message; +use twilight_model::application::interaction::Interaction; pub(crate) async fn leave( - msg: Message, + interaction: Interaction, state: State, ) -> Result<(), Box> { tracing::debug!( - "leave command in channel {} by {}", - msg.channel_id, - msg.author.name + "leave command n guild {:?} in channel {:?} by {:?}", + interaction.guild_id, + interaction.channel, + interaction.author(), ); - let guild_id = msg.guild_id.unwrap(); + + let Some(guild_id) = interaction.guild_id else { + return Ok(()); + }; state.songbird.leave(guild_id).await?; Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index cb82b6f..d29ab67 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -23,7 +23,7 @@ mod delete; pub(crate) use delete::delete; use twilight_model::application::command::CommandType; -use twilight_util::builder::command::{CommandBuilder, StringBuilder}; +use twilight_util::builder::command::{CommandBuilder, IntegerBuilder, StringBuilder}; pub(crate) fn get_chat_commands() -> Vec { vec![ @@ -36,5 +36,8 @@ pub(crate) fn get_chat_commands() -> Vec Result<(), Box> { tracing::debug!( - "pause command in channel {} by {}", - msg.channel_id, - msg.author.name + "pause command in guild {:?} in channel {:?} by {:?}", + interaction.guild_id, + interaction.channel, + interaction.author(), ); - let guild_id = msg.guild_id.unwrap(); + let Some(guild_id) = interaction.guild_id else { + return Ok(()); + }; if let Some(call_lock) = state.songbird.get(guild_id) { let call = call_lock.lock().await; call.queue().pause()?; } + let interaction_response_data = InteractionResponseDataBuilder::new() + .content("Paused the track") + .flags(MessageFlags::EPHEMERAL) + .build(); + + let response = InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(interaction_response_data), + }; + state .http - .create_message(msg.channel_id) - .content("Paused the track")? + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) .await?; Ok(()) diff --git a/src/commands/play.rs b/src/commands/play.rs index 827a36e..13306a3 100644 --- a/src/commands/play.rs +++ b/src/commands/play.rs @@ -1,4 +1,4 @@ -use crate::commands::join; +use crate::commands::join::join_channel; use crate::metadata::{Metadata, MetadataMap}; use crate::state::State; use serde_json::Value; @@ -7,23 +7,31 @@ use std::io::{BufRead, BufReader}; use std::{error::Error, ops::Sub, time::Duration}; use tokio::process::Command; use tracing::debug; -use twilight_model::channel::Message; +use twilight_model::application::interaction::Interaction; +use twilight_model::channel::message::MessageFlags; +use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType}; +use twilight_util::builder::InteractionResponseDataBuilder; use url::Url; pub(crate) async fn play( - msg: Message, + interaction: Interaction, state: State, query: String, ) -> Result<(), Box> { - tracing::debug!( - "play command in channel {} by {}", - msg.channel_id, - msg.author.name + debug!( + "play command in channel {:?} by {:?}", + interaction.channel, + interaction.author(), ); - join(msg.clone(), state.clone()).await?; + let Some(user_id) = interaction.author_id() else { + return Ok(()); + }; + let Some(guild_id) = interaction.guild_id else { + return Ok(()); + }; - let guild_id = msg.guild_id.unwrap(); + join_channel(state.clone(), guild_id, user_id).await?; // handle keyword queries let query = if Url::parse(&query).is_err() { @@ -32,6 +40,24 @@ pub(crate) async fn play( query }; + debug!("query: {:?}", query); + + let interaction_response_data = InteractionResponseDataBuilder::new() + .content("Adding tracks to the queue ...") + .flags(MessageFlags::EPHEMERAL) + .build(); + + let response = InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(interaction_response_data), + }; + + state + .http + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) + .await?; + // handle playlist links let urls = if query.contains("list=") { get_playlist_urls(query).await? @@ -62,12 +88,6 @@ pub(crate) async fn play( duration: metadata.duration, }); } - } else { - state - .http - .create_message(msg.channel_id) - .content("Cannot find any results")? - .await?; } } diff --git a/src/commands/queue.rs b/src/commands/queue.rs index 2dda61c..55a473c 100644 --- a/src/commands/queue.rs +++ b/src/commands/queue.rs @@ -1,18 +1,26 @@ +use twilight_model::http::interaction::InteractionResponseType; +use twilight_model::{ + application::interaction::Interaction, channel::message::MessageFlags, + http::interaction::InteractionResponse, +}; +use twilight_util::builder::InteractionResponseDataBuilder; + use crate::{metadata::MetadataMap, state::State}; use std::error::Error; -use twilight_model::channel::Message; pub(crate) async fn queue( - msg: Message, + interaction: Interaction, state: State, ) -> Result<(), Box> { tracing::debug!( - "queue command in channel {} by {}", - msg.channel_id, - msg.author.name + "queue command in guild {:?} in channel {:?} by {:?}", + interaction.guild_id, + interaction.channel, + interaction.author(), ); - let guild_id = msg.guild_id.unwrap(); - + let Some(guild_id) = interaction.guild_id else { + return Ok(()); + }; if let Some(call_lock) = state.songbird.get(guild_id) { let call = call_lock.lock().await; let queue = call.queue().current_queue(); @@ -48,10 +56,21 @@ pub(crate) async fn queue( } message.push_str("`\n"); } + + let interaction_response_data = InteractionResponseDataBuilder::new() + .content(&message) + .flags(MessageFlags::EPHEMERAL) + .build(); + + let response = InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(interaction_response_data), + }; + state .http - .create_message(msg.channel_id) - .content(&message)? + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) .await?; } Ok(()) diff --git a/src/commands/resume.rs b/src/commands/resume.rs index f907754..3f2049a 100644 --- a/src/commands/resume.rs +++ b/src/commands/resume.rs @@ -1,28 +1,45 @@ use crate::state::State; use std::error::Error; -use twilight_model::channel::Message; +use twilight_model::{ + application::interaction::Interaction, + channel::message::MessageFlags, + http::interaction::{InteractionResponse, InteractionResponseType}, +}; +use twilight_util::builder::InteractionResponseDataBuilder; pub(crate) async fn resume( - msg: Message, + interaction: Interaction, state: State, ) -> Result<(), Box> { tracing::debug!( - "resume command in channel {} by {}", - msg.channel_id, - msg.author.name + "resume command in guild {:?} in channel {:?} by {:?}", + interaction.guild_id, + interaction.channel, + interaction.author(), ); - let guild_id = msg.guild_id.unwrap(); - + let Some(guild_id) = interaction.guild_id else { + return Ok(()); + }; if let Some(call_lock) = state.songbird.get(guild_id) { let call = call_lock.lock().await; call.queue().resume()?; } + let interaction_response_data = InteractionResponseDataBuilder::new() + .content("Resumed the track") + .flags(MessageFlags::EPHEMERAL) + .build(); + + let response = InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(interaction_response_data), + }; + state .http - .create_message(msg.channel_id) - .content("Resumed the track")? + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) .await?; Ok(()) diff --git a/src/commands/stop.rs b/src/commands/stop.rs index f41a7d3..c7d1ae6 100644 --- a/src/commands/stop.rs +++ b/src/commands/stop.rs @@ -1,28 +1,47 @@ +use twilight_model::{ + application::interaction::Interaction, + channel::message::MessageFlags, + http::interaction::{InteractionResponse, InteractionResponseType}, +}; +use twilight_util::builder::InteractionResponseDataBuilder; + use crate::state::State; use std::error::Error; -use twilight_model::channel::Message; pub(crate) async fn stop( - msg: Message, + interaction: Interaction, state: State, ) -> Result<(), Box> { tracing::debug!( - "stop command in channel {} by {}", - msg.channel_id, - msg.author.name + "stop command in guild {:?} in channel {:?} by {:?}", + interaction.guild_id, + interaction.channel, + interaction.author(), ); - let guild_id = msg.guild_id.unwrap(); + let Some(guild_id) = interaction.guild_id else { + return Ok(()); + }; if let Some(call_lock) = state.songbird.get(guild_id) { let call = call_lock.lock().await; call.queue().stop(); } + let interaction_response_data = InteractionResponseDataBuilder::new() + .content("Stopped the track and cleared the queue") + .flags(MessageFlags::EPHEMERAL) + .build(); + + let response = InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some(interaction_response_data), + }; + state .http - .create_message(msg.channel_id) - .content("Stopped the track and cleared the queue")? + .interaction(interaction.application_id) + .create_response(interaction.id, &interaction.token, &response) .await?; Ok(()) diff --git a/src/handler.rs b/src/handler.rs index 6e5a3ba..ba09ea4 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -4,47 +4,25 @@ use crate::state::State; use futures::Future; use std::error::Error; use std::sync::Arc; +use tracing::debug; +use twilight_model::application::interaction::application_command::CommandOptionValue; +use twilight_model::application::interaction::{Interaction, InteractionData}; use twilight_gateway::Event; -use twilight_model::channel::Message; -enum ChatCommand { - Play(Message, String), - Stop(Message), - Pause(Message), - Resume(Message), - Leave(Message), - Join(Message), - Queue(Message), - Delete(Message), +#[derive(Debug)] +enum InteractionCommand { + Play(Interaction, String), + Stop(Interaction), + Pause(Interaction), + Resume(Interaction), + Leave(Interaction), + Join(Interaction), + Queue(Interaction), + Delete(Interaction, i64), NotImplemented, } -fn parse_command(event: Event) -> Option { - match event { - Event::MessageCreate(msg_create) => { - if msg_create.guild_id.is_none() || !msg_create.content.starts_with('!') { - return None; - } - let split: Vec<&str> = msg_create.content.splitn(2, ' ').collect(); - match split.as_slice() { - ["!play", query] => { - Some(ChatCommand::Play(msg_create.0.clone(), query.to_string())) - } - ["!stop"] | ["!stop", _] => Some(ChatCommand::Stop(msg_create.0)), - ["!pause"] | ["!pause", _] => Some(ChatCommand::Pause(msg_create.0)), - ["!resume"] | ["!resume", _] => Some(ChatCommand::Resume(msg_create.0)), - ["!leave"] | ["!leave", _] => Some(ChatCommand::Leave(msg_create.0)), - ["!join"] | ["!join", _] => Some(ChatCommand::Join(msg_create.0)), - ["!queue"] | ["!queue", _] => Some(ChatCommand::Queue(msg_create.0)), - ["!delete"] | ["!delete", _] => Some(ChatCommand::Delete(msg_create.0)), - _ => Some(ChatCommand::NotImplemented), - } - } - _ => None, - } -} - fn spawn( fut: impl Future>> + Send + 'static, ) { @@ -63,17 +41,84 @@ impl Handler { pub(crate) fn new(state: State) -> Self { Self { state } } - pub(crate) async fn act(&mut self, event: Event) { - match parse_command(event) { - Some(ChatCommand::Play(msg, query)) => spawn(play(msg, Arc::clone(&self.state), query)), - Some(ChatCommand::Stop(msg)) => spawn(stop(msg, Arc::clone(&self.state))), - Some(ChatCommand::Pause(msg)) => spawn(pause(msg, Arc::clone(&self.state))), - Some(ChatCommand::Resume(msg)) => spawn(resume(msg, Arc::clone(&self.state))), - Some(ChatCommand::Leave(msg)) => spawn(leave(msg, Arc::clone(&self.state))), - Some(ChatCommand::Join(msg)) => spawn(join(msg, Arc::clone(&self.state))), - Some(ChatCommand::Queue(msg)) => spawn(queue(msg, Arc::clone(&self.state))), - Some(ChatCommand::Delete(msg)) => spawn(delete(msg, Arc::clone(&self.state))), + pub(crate) async fn act(&self, event: Event) { + let interaction_command = match event { + Event::InteractionCreate(interaction) => { + debug!("interaction: {:?}", &interaction); + match &interaction.data { + Some(InteractionData::ApplicationCommand(command)) => { + 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( + interaction.0.clone(), + query.clone(), + ) + } else { + InteractionCommand::NotImplemented + } + } else { + InteractionCommand::NotImplemented + } + } + "stop" => InteractionCommand::Stop(interaction.0.clone()), + "pause" => InteractionCommand::Pause(interaction.0.clone()), + "resume" => InteractionCommand::Resume(interaction.0.clone()), + "leave" => InteractionCommand::Leave(interaction.0.clone()), + "join" => InteractionCommand::Join(interaction.0.clone()), + "queue" => InteractionCommand::Queue(interaction.0.clone()), + "delete" => { + if let Some(count_option) = + command.options.iter().find(|opt| opt.name == "count") + { + if let CommandOptionValue::Integer(count) = count_option.value { + InteractionCommand::Delete(interaction.0.clone(), count) + } else { + InteractionCommand::NotImplemented + } + } else { + InteractionCommand::NotImplemented + } + } + _ => InteractionCommand::NotImplemented, + } + } + _ => InteractionCommand::NotImplemented, + } + } + _ => InteractionCommand::NotImplemented, + }; + debug!("{:?}", interaction_command); + match interaction_command { + InteractionCommand::Play(interaction, query) => { + spawn(play(interaction, Arc::clone(&self.state), query)) + } + InteractionCommand::Stop(interaction) => { + spawn(stop(interaction, Arc::clone(&self.state))) + } + InteractionCommand::Pause(interaction) => { + spawn(pause(interaction, Arc::clone(&self.state))) + } + InteractionCommand::Resume(interaction) => { + spawn(resume(interaction, Arc::clone(&self.state))) + } + InteractionCommand::Leave(interaction) => { + spawn(leave(interaction, Arc::clone(&self.state))) + } + InteractionCommand::Join(interaction) => { + spawn(join(interaction, Arc::clone(&self.state))) + } + InteractionCommand::Queue(interaction) => { + spawn(queue(interaction, Arc::clone(&self.state))) + } + InteractionCommand::Delete(interaction, count) => { + spawn(delete(interaction, Arc::clone(&self.state), count)) + } _ => {} - } + }; } } diff --git a/src/main.rs b/src/main.rs index 2323fd5..2f1dfbb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,7 @@ async fn main() -> Result<(), Box> { info!("Ready to receive events"); - let mut handler = Handler::new(Arc::clone(&state)); + let handler = Handler::new(Arc::clone(&state)); let mut stop_rx = signal_handler(); let mut stream = ShardEventStream::new(shards.iter_mut()); loop {