Bring back the buttons
This commit is contained in:
		
							parent
							
								
									22dde966b5
								
							
						
					
					
						commit
						da1d0d1fec
					
				
							
								
								
									
										15
									
								
								.githooks/pre-commit
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										15
									
								
								.githooks/pre-commit
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,15 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| RED='\033[0;31m' | ||||
| BRED='\033[1;31m' | ||||
| NC='\033[0m' | ||||
| 
 | ||||
| diff=$(cargo clippy --all -- -D warnings -D clippy::unwrap_used) | ||||
| result=$? | ||||
| 
 | ||||
| if [[ ${result} -ne 0 ]] ; then | ||||
|   echo -e "\n${BRED}Cannot commit:${NC} There are some clippy issues in your code, check the above output for any errors." | ||||
|   exit 1 | ||||
| fi | ||||
| 
 | ||||
| exit 0 | ||||
							
								
								
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -916,9 +916,9 @@ dependencies = [ | ||||
| 
 | ||||
| [[package]] | ||||
| name = "http" | ||||
| version = "0.2.8" | ||||
| version = "0.2.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" | ||||
| checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" | ||||
| dependencies = [ | ||||
|  "bytes", | ||||
|  "fnv", | ||||
| @ -2459,9 +2459,9 @@ dependencies = [ | ||||
| 
 | ||||
| [[package]] | ||||
| name = "slab" | ||||
| version = "0.4.7" | ||||
| version = "0.4.8" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" | ||||
| checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" | ||||
| dependencies = [ | ||||
|  "autocfg", | ||||
| ] | ||||
| @ -2770,9 +2770,9 @@ dependencies = [ | ||||
| 
 | ||||
| [[package]] | ||||
| name = "tokio-stream" | ||||
| version = "0.1.11" | ||||
| version = "0.1.12" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" | ||||
| checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" | ||||
| dependencies = [ | ||||
|  "futures-core", | ||||
|  "pin-project-lite", | ||||
|  | ||||
| @ -11,7 +11,7 @@ use crate::{ | ||||
| 
 | ||||
| pub const NAME: &str = "help"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     respond_message( | ||||
|       &ctx, | ||||
|  | ||||
| @ -14,7 +14,7 @@ use crate::{ | ||||
| 
 | ||||
| pub const NAME: &str = "link"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     let data = ctx.data.read().await; | ||||
|     let database = data.get::<Database>().expect("to contain a value"); | ||||
|  | ||||
| @ -19,7 +19,7 @@ use crate::{ | ||||
| 
 | ||||
| pub const NAME: &str = "rename"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     let data = ctx.data.read().await; | ||||
|     let database = data.get::<Database>().expect("to contain a value"); | ||||
|  | ||||
| @ -14,7 +14,7 @@ use crate::{ | ||||
| 
 | ||||
| pub const NAME: &str = "unlink"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     let data = ctx.data.read().await; | ||||
|     let database = data.get::<Database>().expect("to contain a value"); | ||||
|  | ||||
| @ -11,7 +11,7 @@ use crate::{bot::commands::CommandOutput, consts::VERSION, utils::embed::Status} | ||||
| 
 | ||||
| pub const NAME: &str = "version"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     if let Err(why) = command | ||||
|       .create_interaction_response(&ctx.http, |response| { | ||||
|  | ||||
| @ -5,7 +5,10 @@ use serenity::{ | ||||
|   builder::{CreateApplicationCommand, CreateApplicationCommands}, | ||||
|   model::application::command::Command, | ||||
|   model::prelude::{ | ||||
|     interaction::{application_command::ApplicationCommandInteraction, InteractionResponseType}, | ||||
|     interaction::{ | ||||
|       application_command::ApplicationCommandInteraction, | ||||
|       message_component::MessageComponentInteraction, InteractionResponseType, | ||||
|     }, | ||||
|     GuildId, | ||||
|   }, | ||||
|   prelude::{Context, TypeMapKey}, | ||||
| @ -44,6 +47,28 @@ pub async fn respond_message( | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| pub async fn respond_component_message( | ||||
|   ctx: &Context, | ||||
|   component: &MessageComponentInteraction, | ||||
|   options: EmbedMessageOptions, | ||||
|   ephemeral: bool, | ||||
| ) { | ||||
|   if let Err(why) = component | ||||
|     .create_interaction_response(&ctx.http, |response| { | ||||
|       response | ||||
|         .kind(InteractionResponseType::ChannelMessageWithSource) | ||||
|         .interaction_response_data(|message| { | ||||
|           message | ||||
|             .embed(|embed| make_embed_message(embed, options)) | ||||
|             .ephemeral(ephemeral) | ||||
|         }) | ||||
|     }) | ||||
|     .await | ||||
|   { | ||||
|     error!("Error sending message: {:?}", why); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| pub async fn update_message( | ||||
|   ctx: &Context, | ||||
|   command: &ApplicationCommandInteraction, | ||||
| @ -78,6 +103,7 @@ pub async fn defer_message( | ||||
| 
 | ||||
| pub type CommandOutput = Pin<Box<dyn Future<Output = ()> + Send>>; | ||||
| pub type CommandExecutor = fn(Context, ApplicationCommandInteraction) -> CommandOutput; | ||||
| pub type ComponentExecutor = fn(Context, MessageComponentInteraction) -> CommandOutput; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct CommandManager { | ||||
| @ -87,7 +113,8 @@ pub struct CommandManager { | ||||
| #[derive(Clone)] | ||||
| pub struct CommandInfo { | ||||
|   pub name: String, | ||||
|   pub executor: CommandExecutor, | ||||
|   pub command_executor: CommandExecutor, | ||||
|   pub component_executor: Option<ComponentExecutor>, | ||||
|   pub register: fn(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, | ||||
| } | ||||
| 
 | ||||
| @ -100,50 +127,71 @@ impl CommandManager { | ||||
|     // Debug-only commands
 | ||||
|     #[cfg(debug_assertions)] | ||||
|     { | ||||
|       instance.insert_command(ping::NAME, ping::register, ping::run); | ||||
|       instance.insert_command(token::NAME, token::register, token::run); | ||||
|       instance.insert(ping::NAME, ping::register, ping::command, None); | ||||
|       instance.insert(token::NAME, token::register, token::command, None); | ||||
|     } | ||||
| 
 | ||||
|     // Core commands
 | ||||
|     instance.insert_command(core::help::NAME, core::help::register, core::help::run); | ||||
|     instance.insert_command( | ||||
|     instance.insert( | ||||
|       core::help::NAME, | ||||
|       core::help::register, | ||||
|       core::help::command, | ||||
|       None, | ||||
|     ); | ||||
|     instance.insert( | ||||
|       core::version::NAME, | ||||
|       core::version::register, | ||||
|       core::version::run, | ||||
|       core::version::command, | ||||
|       None, | ||||
|     ); | ||||
|     instance.insert_command(core::link::NAME, core::link::register, core::link::run); | ||||
|     instance.insert_command( | ||||
|     instance.insert( | ||||
|       core::link::NAME, | ||||
|       core::link::register, | ||||
|       core::link::command, | ||||
|       None, | ||||
|     ); | ||||
|     instance.insert( | ||||
|       core::unlink::NAME, | ||||
|       core::unlink::register, | ||||
|       core::unlink::run, | ||||
|       core::unlink::command, | ||||
|       None, | ||||
|     ); | ||||
|     instance.insert_command( | ||||
|     instance.insert( | ||||
|       core::rename::NAME, | ||||
|       core::rename::register, | ||||
|       core::rename::run, | ||||
|       core::rename::command, | ||||
|       None, | ||||
|     ); | ||||
| 
 | ||||
|     // Music commands
 | ||||
|     instance.insert_command(music::join::NAME, music::join::register, music::join::run); | ||||
|     instance.insert_command( | ||||
|     instance.insert( | ||||
|       music::join::NAME, | ||||
|       music::join::register, | ||||
|       music::join::command, | ||||
|       None, | ||||
|     ); | ||||
|     instance.insert( | ||||
|       music::leave::NAME, | ||||
|       music::leave::register, | ||||
|       music::leave::run, | ||||
|       music::leave::command, | ||||
|       None, | ||||
|     ); | ||||
|     instance.insert_command( | ||||
|     instance.insert( | ||||
|       music::playing::NAME, | ||||
|       music::playing::register, | ||||
|       music::playing::run, | ||||
|       music::playing::command, | ||||
|       Some(music::playing::component), | ||||
|     ); | ||||
| 
 | ||||
|     instance | ||||
|   } | ||||
| 
 | ||||
|   pub fn insert_command( | ||||
|   pub fn insert( | ||||
|     &mut self, | ||||
|     name: impl Into<String>, | ||||
|     register: fn(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, | ||||
|     executor: CommandExecutor, | ||||
|     command_executor: CommandExecutor, | ||||
|     component_executor: Option<ComponentExecutor>, | ||||
|   ) { | ||||
|     let name = name.into(); | ||||
| 
 | ||||
| @ -152,12 +200,13 @@ impl CommandManager { | ||||
|       CommandInfo { | ||||
|         name, | ||||
|         register, | ||||
|         executor, | ||||
|         command_executor, | ||||
|         component_executor, | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   pub async fn register_commands(&self, ctx: &Context) { | ||||
|   pub async fn register(&self, ctx: &Context) { | ||||
|     let cmds = &self.commands; | ||||
| 
 | ||||
|     debug!( | ||||
| @ -196,11 +245,12 @@ impl CommandManager { | ||||
|     .expect("Failed to create global commands"); | ||||
|   } | ||||
| 
 | ||||
|   // On slash command interaction
 | ||||
|   pub async fn execute_command(&self, ctx: &Context, interaction: ApplicationCommandInteraction) { | ||||
|     let command = self.commands.get(&interaction.data.name); | ||||
| 
 | ||||
|     if let Some(command) = command { | ||||
|       (command.executor)(ctx.clone(), interaction.clone()).await; | ||||
|       (command.command_executor)(ctx.clone(), interaction.clone()).await; | ||||
|     } else { | ||||
|       // Command does not exist
 | ||||
|       if let Err(why) = interaction | ||||
| @ -219,6 +269,39 @@ impl CommandManager { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // On message component interaction (e.g. button)
 | ||||
|   pub async fn execute_component(&self, ctx: &Context, interaction: MessageComponentInteraction) { | ||||
|     let command = match interaction.data.custom_id.split("::").next() { | ||||
|       Some(command) => command, | ||||
|       None => return, | ||||
|     }; | ||||
| 
 | ||||
|     let command = self.commands.get(command); | ||||
| 
 | ||||
|     if let Some(command) = command { | ||||
|       if let Some(executor) = command.component_executor { | ||||
|         executor(ctx.clone(), interaction.clone()).await; | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if let Err(why) = interaction | ||||
|       .create_interaction_response(&ctx.http, |response| { | ||||
|         response | ||||
|           .kind(InteractionResponseType::ChannelMessageWithSource) | ||||
|           .interaction_response_data(|message| { | ||||
|             message | ||||
|               .content("Woops, that interaction doesn't exist") | ||||
|               .ephemeral(true) | ||||
|           }) | ||||
|       }) | ||||
|       .await | ||||
|     { | ||||
|       error!("Failed to respond to interaction: {}", why); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| impl TypeMapKey for CommandManager { | ||||
|  | ||||
| @ -13,7 +13,7 @@ use crate::{ | ||||
| 
 | ||||
| pub const NAME: &str = "join"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     let guild = ctx | ||||
|       .cache | ||||
|  | ||||
| @ -12,7 +12,7 @@ use crate::{ | ||||
| 
 | ||||
| pub const NAME: &str = "leave"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     let data = ctx.data.read().await; | ||||
|     let session_manager = data | ||||
|  | ||||
| @ -1,16 +1,25 @@ | ||||
| use librespot::core::spotify_id::SpotifyAudioType; | ||||
| use std::time::Duration; | ||||
| 
 | ||||
| use librespot::core::spotify_id::{SpotifyAudioType, SpotifyId}; | ||||
| use log::error; | ||||
| use serenity::{ | ||||
|   builder::CreateApplicationCommand, | ||||
|   model::prelude::interaction::{ | ||||
|     application_command::ApplicationCommandInteraction, InteractionResponseType, | ||||
|   builder::{CreateApplicationCommand, CreateButton, CreateComponents, CreateEmbed}, | ||||
|   model::{ | ||||
|     prelude::{ | ||||
|       component::ButtonStyle, | ||||
|       interaction::{ | ||||
|         application_command::ApplicationCommandInteraction, | ||||
|         message_component::MessageComponentInteraction, InteractionResponseType, | ||||
|       }, | ||||
|     }, | ||||
|     user::User, | ||||
|   }, | ||||
|   prelude::Context, | ||||
| }; | ||||
| 
 | ||||
| use crate::{ | ||||
|   bot::commands::{respond_message, CommandOutput}, | ||||
|   session::manager::SessionManager, | ||||
|   bot::commands::{respond_component_message, respond_message, CommandOutput}, | ||||
|   session::{manager::SessionManager, pbi::PlaybackInfo}, | ||||
|   utils::{ | ||||
|     self, | ||||
|     embed::{EmbedBuilder, Status}, | ||||
| @ -19,7 +28,7 @@ use crate::{ | ||||
| 
 | ||||
| pub const NAME: &str = "playing"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     let not_playing = async { | ||||
|       respond_message( | ||||
| @ -27,7 +36,7 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu | ||||
|         &command, | ||||
|         EmbedBuilder::new() | ||||
|           .title("Cannot get track info") | ||||
|           .icon_url("https://tabler-icons.io/static/tabler-icons/icons/ban.svg") | ||||
|           .icon_url("https://spoticord.com/forbidden.png") | ||||
|           .description("I'm currently not playing any music in this server") | ||||
|           .status(Status::Error) | ||||
|           .build(), | ||||
| @ -82,6 +91,404 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     // Get owner of session
 | ||||
|     let owner = match utils::discord::get_user(&ctx, owner).await { | ||||
|       Some(user) => user, | ||||
|       None => { | ||||
|         // This shouldn't happen
 | ||||
| 
 | ||||
|         error!("Could not find user with ID: {owner}"); | ||||
| 
 | ||||
|         respond_message( | ||||
|           &ctx, | ||||
|           &command, | ||||
|           EmbedBuilder::new() | ||||
|             .title("[INTERNAL ERROR] Cannot get track info") | ||||
|             .description(format!( | ||||
|               "Could not find user with ID `{}`\nThis is an issue with the bot!", | ||||
|               owner | ||||
|             )) | ||||
|             .status(Status::Error) | ||||
|             .build(), | ||||
|           true, | ||||
|         ) | ||||
|         .await; | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     // Get metadata
 | ||||
|     let (title, description, audio_type, thumbnail) = get_metadata(spotify_id, &pbi); | ||||
| 
 | ||||
|     if let Err(why) = command | ||||
|       .create_interaction_response(&ctx.http, |response| { | ||||
|         response | ||||
|           .kind(InteractionResponseType::ChannelMessageWithSource) | ||||
|           .interaction_response_data(|message| { | ||||
|             message | ||||
|               .set_embed(build_playing_embed( | ||||
|                 title, | ||||
|                 audio_type, | ||||
|                 spotify_id, | ||||
|                 description, | ||||
|                 owner, | ||||
|                 thumbnail, | ||||
|               )) | ||||
|               .components(|components| create_button(components, pbi.is_playing)) | ||||
|           }) | ||||
|       }) | ||||
|       .await | ||||
|     { | ||||
|       error!("Error sending message: {why:?}"); | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| pub fn component(ctx: Context, mut interaction: MessageComponentInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     let error_message = |title: &'static str, description: &'static str| async { | ||||
|       respond_component_message( | ||||
|         &ctx, | ||||
|         &interaction, | ||||
|         EmbedBuilder::new() | ||||
|           .title(title.to_string()) | ||||
|           .icon_url("https://spoticord.com/forbidden.png") | ||||
|           .description(description.to_string()) | ||||
|           .status(Status::Error) | ||||
|           .build(), | ||||
|         true, | ||||
|       ) | ||||
|       .await; | ||||
|     }; | ||||
| 
 | ||||
|     let error_edit = |title: &'static str, description: &'static str| { | ||||
|       let mut interaction = interaction.clone(); | ||||
|       let ctx = ctx.clone(); | ||||
| 
 | ||||
|       async move { | ||||
|         interaction.defer(&ctx.http).await.ok(); | ||||
| 
 | ||||
|         if let Err(why) = interaction | ||||
|           .message | ||||
|           .edit(&ctx, |message| { | ||||
|             message.embed(|embed| { | ||||
|               embed | ||||
|                 .description(description) | ||||
|                 .author(|author| { | ||||
|                   author | ||||
|                     .name(title) | ||||
|                     .icon_url("https://spoticord.com/forbidden.png") | ||||
|                 }) | ||||
|                 .color(Status::Error) | ||||
|             }) | ||||
|           }) | ||||
|           .await | ||||
|         { | ||||
|           error!("Failed to update playing message: {why}"); | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     let data = ctx.data.read().await; | ||||
|     let session_manager = data | ||||
|       .get::<SessionManager>() | ||||
|       .expect("to contain a value") | ||||
|       .clone(); | ||||
| 
 | ||||
|     // Check if session still exists
 | ||||
|     let mut session = match session_manager | ||||
|       .get_session(interaction.guild_id.expect("to contain a value")) | ||||
|       .await | ||||
|     { | ||||
|       Some(session) => session, | ||||
|       None => { | ||||
|         error_edit( | ||||
|           "Cannot perform action", | ||||
|           "I'm currently not playing any music in this server", | ||||
|         ) | ||||
|         .await; | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     // Check if the session contains an owner
 | ||||
|     let owner = match session.owner().await { | ||||
|       Some(owner) => owner, | ||||
|       None => { | ||||
|         error_edit( | ||||
|           "Cannot change playback state", | ||||
|           "I'm currently not playing any music in this server", | ||||
|         ) | ||||
|         .await; | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     // Get Playback Info from session
 | ||||
|     let pbi = match session.playback_info().await { | ||||
|       Some(pbi) => pbi, | ||||
|       None => { | ||||
|         error_edit( | ||||
|           "Cannot change playback state", | ||||
|           "I'm currently not playing any music in this server", | ||||
|         ) | ||||
|         .await; | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     // Check if the user is the owner of the session
 | ||||
|     if owner != interaction.user.id { | ||||
|       error_message( | ||||
|         "Cannot change playback state", | ||||
|         "You must be the host to use the media buttons", | ||||
|       ) | ||||
|       .await; | ||||
| 
 | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Get owner of session
 | ||||
|     let owner = match utils::discord::get_user(&ctx, owner).await { | ||||
|       Some(user) => user, | ||||
|       None => { | ||||
|         // This shouldn't happen
 | ||||
| 
 | ||||
|         error!("Could not find user with ID: {owner}"); | ||||
| 
 | ||||
|         respond_component_message( | ||||
|           &ctx, | ||||
|           &interaction, | ||||
|           EmbedBuilder::new() | ||||
|             .title("[INTERNAL ERROR] Cannot get track info") | ||||
|             .description(format!( | ||||
|               "Could not find user with ID `{}`\nThis is an issue with the bot!", | ||||
|               owner | ||||
|             )) | ||||
|             .status(Status::Error) | ||||
|             .build(), | ||||
|           true, | ||||
|         ) | ||||
|         .await; | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     // Send the desired command to the session
 | ||||
|     let success = match interaction.data.custom_id.as_str() { | ||||
|       "playing::btn_pause_play" => { | ||||
|         if pbi.is_playing { | ||||
|           session.pause().await.is_ok() | ||||
|         } else { | ||||
|           session.resume().await.is_ok() | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       "playing::btn_previous_track" => session.previous().await.is_ok(), | ||||
| 
 | ||||
|       "playing::btn_next_track" => session.next().await.is_ok(), | ||||
| 
 | ||||
|       _ => { | ||||
|         error!("Unknown custom_id: {}", interaction.data.custom_id); | ||||
|         false | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     if !success { | ||||
|       error_message( | ||||
|         "Cannot change playback state", | ||||
|         "An error occurred while trying to change the playback state", | ||||
|       ) | ||||
|       .await; | ||||
| 
 | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     interaction.defer(&ctx.http).await.ok(); | ||||
|     tokio::time::sleep(Duration::from_millis( | ||||
|       if interaction.data.custom_id == "playing::btn_pause_play" { | ||||
|         0 | ||||
|       } else { | ||||
|         2500 | ||||
|       }, | ||||
|     )) | ||||
|     .await; | ||||
|     update_embed(&mut interaction, &ctx, owner).await; | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { | ||||
|   command | ||||
|     .name(NAME) | ||||
|     .description("Display which song is currently being played") | ||||
| } | ||||
| 
 | ||||
| fn create_button(components: &mut CreateComponents, playing: bool) -> &mut CreateComponents { | ||||
|   let mut prev_btn = CreateButton::default(); | ||||
|   prev_btn | ||||
|     .style(ButtonStyle::Primary) | ||||
|     .label("<<") | ||||
|     .custom_id("playing::btn_previous_track"); | ||||
| 
 | ||||
|   let mut toggle_btn = CreateButton::default(); | ||||
|   toggle_btn | ||||
|     .style(ButtonStyle::Secondary) | ||||
|     .label(if playing { "Pause" } else { "Play" }) | ||||
|     .custom_id("playing::btn_pause_play"); | ||||
| 
 | ||||
|   let mut next_btn = CreateButton::default(); | ||||
|   next_btn | ||||
|     .style(ButtonStyle::Primary) | ||||
|     .label(">>") | ||||
|     .custom_id("playing::btn_next_track"); | ||||
| 
 | ||||
|   components.create_action_row(|ar| { | ||||
|     ar.add_button(prev_btn) | ||||
|       .add_button(toggle_btn) | ||||
|       .add_button(next_btn) | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| async fn update_embed(interaction: &mut MessageComponentInteraction, ctx: &Context, owner: User) { | ||||
|   let error_edit = |title: &'static str, description: &'static str| { | ||||
|     let mut interaction = interaction.clone(); | ||||
|     let ctx = ctx.clone(); | ||||
| 
 | ||||
|     async move { | ||||
|       interaction.defer(&ctx.http).await.ok(); | ||||
| 
 | ||||
|       if let Err(why) = interaction | ||||
|         .message | ||||
|         .edit(&ctx, |message| { | ||||
|           message.embed(|embed| { | ||||
|             embed | ||||
|               .description(description) | ||||
|               .author(|author| { | ||||
|                 author | ||||
|                   .name(title) | ||||
|                   .icon_url("https://spoticord.com/forbidden.png") | ||||
|               }) | ||||
|               .color(Status::Error) | ||||
|           }) | ||||
|         }) | ||||
|         .await | ||||
|       { | ||||
|         error!("Failed to update playing message: {why}"); | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   let data = ctx.data.read().await; | ||||
|   let session_manager = data | ||||
|     .get::<SessionManager>() | ||||
|     .expect("to contain a value") | ||||
|     .clone(); | ||||
| 
 | ||||
|   // Check if session still exists
 | ||||
|   let session = match session_manager | ||||
|     .get_session(interaction.guild_id.expect("to contain a value")) | ||||
|     .await | ||||
|   { | ||||
|     Some(session) => session, | ||||
|     None => { | ||||
|       error_edit( | ||||
|         "Cannot perform action", | ||||
|         "I'm currently not playing any music in this server", | ||||
|       ) | ||||
|       .await; | ||||
| 
 | ||||
|       return; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // Get Playback Info from session
 | ||||
|   let pbi = match session.playback_info().await { | ||||
|     Some(pbi) => pbi, | ||||
|     None => { | ||||
|       error_edit( | ||||
|         "Cannot change playback state", | ||||
|         "I'm currently not playing any music in this server", | ||||
|       ) | ||||
|       .await; | ||||
| 
 | ||||
|       return; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   let spotify_id = match pbi.spotify_id { | ||||
|     Some(spotify_id) => spotify_id, | ||||
|     None => { | ||||
|       error_edit( | ||||
|         "Cannot change playback state", | ||||
|         "I'm currently not playing any music in this server", | ||||
|       ) | ||||
|       .await; | ||||
| 
 | ||||
|       return; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   let (title, description, audio_type, thumbnail) = get_metadata(spotify_id, &pbi); | ||||
| 
 | ||||
|   if let Err(why) = interaction | ||||
|     .message | ||||
|     .edit(&ctx, |message| { | ||||
|       message | ||||
|         .set_embed(build_playing_embed( | ||||
|           title, | ||||
|           audio_type, | ||||
|           spotify_id, | ||||
|           description, | ||||
|           owner, | ||||
|           thumbnail, | ||||
|         )) | ||||
|         .components(|components| create_button(components, pbi.is_playing)); | ||||
| 
 | ||||
|       message | ||||
|     }) | ||||
|     .await | ||||
|   { | ||||
|     error!("Failed to update playing message: {why}"); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| fn build_playing_embed( | ||||
|   title: impl Into<String>, | ||||
|   audio_type: impl Into<String>, | ||||
|   spotify_id: SpotifyId, | ||||
|   description: impl Into<String>, | ||||
|   owner: User, | ||||
|   thumbnail: impl Into<String>, | ||||
| ) -> CreateEmbed { | ||||
|   let mut embed = CreateEmbed::default(); | ||||
|   embed | ||||
|     .author(|author| { | ||||
|       author | ||||
|         .name("Currently Playing") | ||||
|         .icon_url("https://spoticord.com/spotify-logo.png") | ||||
|     }) | ||||
|     .title(title.into()) | ||||
|     .url(format!( | ||||
|       "https://open.spotify.com/{}/{}", | ||||
|       audio_type.into(), | ||||
|       spotify_id | ||||
|         .to_base62() | ||||
|         .expect("to be able to convert to base62") | ||||
|     )) | ||||
|     .description(description.into()) | ||||
|     .footer(|footer| footer.text(&owner.name).icon_url(owner.face())) | ||||
|     .thumbnail(thumbnail.into()) | ||||
|     .color(Status::Info); | ||||
| 
 | ||||
|   embed | ||||
| } | ||||
| 
 | ||||
| fn get_metadata(spotify_id: SpotifyId, pbi: &PlaybackInfo) -> (String, String, String, String) { | ||||
|   // Get audio type
 | ||||
|   let audio_type = if spotify_id.audio_type == SpotifyAudioType::Track { | ||||
|     "track" | ||||
| @ -92,8 +499,8 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu | ||||
|   // Create title
 | ||||
|   let title = format!( | ||||
|     "{} - {}", | ||||
|       pbi.get_artists().expect("to contain a value"), | ||||
|       pbi.get_name().expect("to contain a value") | ||||
|     pbi.get_artists().as_deref().unwrap_or("ID"), | ||||
|     pbi.get_name().as_deref().unwrap_or("ID") | ||||
|   ); | ||||
| 
 | ||||
|   // Create description
 | ||||
| @ -119,72 +526,8 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu | ||||
|     utils::time_to_str(pbi.duration_ms / 1000) | ||||
|   )); | ||||
| 
 | ||||
|     // Get owner of session
 | ||||
|     let owner = match utils::discord::get_user(&ctx, owner).await { | ||||
|       Some(user) => user, | ||||
|       None => { | ||||
|         // This shouldn't happen
 | ||||
| 
 | ||||
|         error!("Could not find user with id {}", owner); | ||||
| 
 | ||||
|         respond_message( | ||||
|           &ctx, | ||||
|           &command, | ||||
|           EmbedBuilder::new() | ||||
|             .title("[INTERNAL ERROR] Cannot get track info") | ||||
|             .description(format!( | ||||
|               "Could not find user with id {}\nThis is an issue with the bot!", | ||||
|               owner | ||||
|             )) | ||||
|             .status(Status::Error) | ||||
|             .build(), | ||||
|           true, | ||||
|         ) | ||||
|         .await; | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|   // Get the thumbnail image
 | ||||
|   let thumbnail = pbi.get_thumbnail_url().expect("to contain a value"); | ||||
| 
 | ||||
|     if let Err(why) = command | ||||
|       .create_interaction_response(&ctx.http, |response| { | ||||
|         response | ||||
|           .kind(InteractionResponseType::ChannelMessageWithSource) | ||||
|           .interaction_response_data(|message| { | ||||
|             message.embed(|embed| { | ||||
|               embed | ||||
|                 .author(|author| { | ||||
|                   author | ||||
|                     .name("Currently Playing") | ||||
|                     .icon_url("https://spoticord.com/spotify-logo.png") | ||||
|                 }) | ||||
|                 .title(title) | ||||
|                 .url(format!( | ||||
|                   "https://open.spotify.com/{}/{}", | ||||
|                   audio_type, | ||||
|                   spotify_id | ||||
|                     .to_base62() | ||||
|                     .expect("to be able to convert to base62") | ||||
|                 )) | ||||
|                 .description(description) | ||||
|                 .footer(|footer| footer.text(&owner.name).icon_url(owner.face())) | ||||
|                 .thumbnail(&thumbnail) | ||||
|                 .color(Status::Info as u64) | ||||
|             }) | ||||
|           }) | ||||
|       }) | ||||
|       .await | ||||
|     { | ||||
|       error!("Error sending message: {:?}", why); | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| 
 | ||||
| pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { | ||||
|   command | ||||
|     .name(NAME) | ||||
|     .description("Display which song is currently being played") | ||||
|   (title, description, audio_type.to_string(), thumbnail) | ||||
| } | ||||
|  | ||||
| @ -11,7 +11,7 @@ use super::CommandOutput; | ||||
| 
 | ||||
| pub const NAME: &str = "ping"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     info!("Pong!"); | ||||
| 
 | ||||
|  | ||||
| @ -12,7 +12,7 @@ use super::CommandOutput; | ||||
| 
 | ||||
| pub const NAME: &str = "token"; | ||||
| 
 | ||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
| pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||
|   Box::pin(async move { | ||||
|     let data = ctx.data.read().await; | ||||
|     let db = data.get::<Database>().expect("to contain a value"); | ||||
|  | ||||
| @ -3,7 +3,13 @@ | ||||
| use log::*; | ||||
| use serenity::{ | ||||
|   async_trait, | ||||
|   model::prelude::{interaction::Interaction, Activity, GuildId, Ready}, | ||||
|   model::prelude::{ | ||||
|     interaction::{ | ||||
|       application_command::ApplicationCommandInteraction, | ||||
|       message_component::MessageComponentInteraction, Interaction, | ||||
|     }, | ||||
|     Activity, GuildId, Ready, | ||||
|   }, | ||||
|   prelude::{Context, EventHandler}, | ||||
| }; | ||||
| 
 | ||||
| @ -11,6 +17,23 @@ use crate::consts::MOTD; | ||||
| 
 | ||||
| use super::commands::CommandManager; | ||||
| 
 | ||||
| // If the GUILD_ID environment variable is set, only allow commands from that guild
 | ||||
| macro_rules! enforce_guild { | ||||
|   ($interaction:ident) => { | ||||
|     if let Ok(guild_id) = std::env::var("GUILD_ID") { | ||||
|       if let Ok(guild_id) = guild_id.parse::<u64>() { | ||||
|         let guild_id = GuildId(guild_id); | ||||
| 
 | ||||
|         if let Some(interaction_guild_id) = $interaction.guild_id { | ||||
|           if guild_id != interaction_guild_id { | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| // Handler struct with a command parameter, an array of dictionary which takes a string and function
 | ||||
| pub struct Handler; | ||||
| 
 | ||||
| @ -25,7 +48,7 @@ impl EventHandler for Handler { | ||||
| 
 | ||||
|     // Set this to true only when a command is removed/updated/created
 | ||||
|     if false { | ||||
|       command_manager.register_commands(&ctx).await; | ||||
|       command_manager.register(&ctx).await; | ||||
|     } | ||||
| 
 | ||||
|     ctx.set_activity(Activity::listening(MOTD)).await; | ||||
| @ -35,18 +58,17 @@ impl EventHandler for Handler { | ||||
| 
 | ||||
|   // INTERACTION_CREATE event, emitted when the bot receives an interaction (slash command, button, etc.)
 | ||||
|   async fn interaction_create(&self, ctx: Context, interaction: Interaction) { | ||||
|     if let Interaction::ApplicationCommand(command) = interaction { | ||||
|       if let Ok(guild_id) = std::env::var("GUILD_ID") { | ||||
|         if let Ok(guild_id) = guild_id.parse::<u64>() { | ||||
|           let guild_id = GuildId(guild_id); | ||||
|     match interaction { | ||||
|       Interaction::ApplicationCommand(command) => self.handle_command(ctx, command).await, | ||||
|       Interaction::MessageComponent(component) => self.handle_component(ctx, component).await, | ||||
|       _ => {} | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|           if let Some(interaction_guild_id) = command.guild_id { | ||||
|             if guild_id != interaction_guild_id { | ||||
|               return; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| impl Handler { | ||||
|   async fn handle_command(&self, ctx: Context, command: ApplicationCommandInteraction) { | ||||
|     enforce_guild!(command); | ||||
| 
 | ||||
|     // Commands must only be executed inside of guilds
 | ||||
| 
 | ||||
| @ -65,7 +87,6 @@ impl EventHandler for Handler { | ||||
|                error!("Failed to send run-in-guild-only error message: {}", why); | ||||
|              } | ||||
| 
 | ||||
|           trace!("interaction_create END2"); | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| @ -82,5 +103,41 @@ impl EventHandler for Handler { | ||||
| 
 | ||||
|     command_manager.execute_command(&ctx, command).await; | ||||
|   } | ||||
| 
 | ||||
|   async fn handle_component(&self, ctx: Context, component: MessageComponentInteraction) { | ||||
|     enforce_guild!(component); | ||||
| 
 | ||||
|     // Components can only be interacted with inside of guilds
 | ||||
| 
 | ||||
|     let guild_id = match component.guild_id { | ||||
|       Some(guild_id) => guild_id, | ||||
|       None => { | ||||
|         if let Err(why) = component | ||||
|              .create_interaction_response(&ctx.http, |response| { | ||||
|                response | ||||
|                  .kind(serenity::model::prelude::interaction::InteractionResponseType::ChannelMessageWithSource) | ||||
|                  .interaction_response_data(|message| { | ||||
|                    message.content("You can only interact with components inside of a server") | ||||
|                  }) | ||||
|              }) | ||||
|              .await { | ||||
|                error!("Failed to send run-in-guild-only error message: {}", why); | ||||
|              } | ||||
| 
 | ||||
|         return; | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     trace!( | ||||
|       "Received component interaction: command={} user={} guild={}", | ||||
|       component.data.custom_id, | ||||
|       component.user.id, | ||||
|       guild_id | ||||
|     ); | ||||
| 
 | ||||
|     let data = ctx.data.read().await; | ||||
|     let command_manager = data.get::<CommandManager>().expect("to contain a value"); | ||||
| 
 | ||||
|     command_manager.execute_component(&ctx, component).await; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -31,4 +31,16 @@ pub enum IpcPacket { | ||||
| 
 | ||||
|   /// Sent when the user has switched their Spotify device away from Spoticord
 | ||||
|   Stopped, | ||||
| 
 | ||||
|   /// Request the player to advance to the next track
 | ||||
|   Next, | ||||
| 
 | ||||
|   /// Request the player to go back to the previous track
 | ||||
|   Previous, | ||||
| 
 | ||||
|   /// Request the player to pause playback
 | ||||
|   Pause, | ||||
| 
 | ||||
|   /// Request the player to resume playback
 | ||||
|   Resume, | ||||
| } | ||||
|  | ||||
| @ -31,7 +31,7 @@ pub struct SpoticordPlayer { | ||||
| } | ||||
| 
 | ||||
| impl SpoticordPlayer { | ||||
|   pub fn create(client: ipc::Client) -> Self { | ||||
|   pub fn new(client: ipc::Client) -> Self { | ||||
|     Self { | ||||
|       client, | ||||
|       session: None, | ||||
| @ -223,6 +223,30 @@ impl SpoticordPlayer { | ||||
|     tokio::spawn(spirc_task); | ||||
|   } | ||||
| 
 | ||||
|   pub fn next(&mut self) { | ||||
|     if let Some(spirc) = &self.spirc { | ||||
|       spirc.next(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   pub fn previous(&mut self) { | ||||
|     if let Some(spirc) = &self.spirc { | ||||
|       spirc.prev(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   pub fn pause(&mut self) { | ||||
|     if let Some(spirc) = &self.spirc { | ||||
|       spirc.pause(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   pub fn resume(&mut self) { | ||||
|     if let Some(spirc) = &self.spirc { | ||||
|       spirc.play(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   pub fn stop(&mut self) { | ||||
|     if let Some(spirc) = self.spirc.take() { | ||||
|       spirc.shutdown(); | ||||
| @ -240,7 +264,7 @@ pub async fn main() { | ||||
|   let client = ipc::Client::connect(tx_name, rx_name).expect("Failed to connect to IPC"); | ||||
| 
 | ||||
|   // Create the player
 | ||||
|   let mut player = SpoticordPlayer::create(client.clone()); | ||||
|   let mut player = SpoticordPlayer::new(client.clone()); | ||||
| 
 | ||||
|   loop { | ||||
|     let message = match client.try_recv() { | ||||
| @ -274,6 +298,22 @@ pub async fn main() { | ||||
|         player.stop(); | ||||
|       } | ||||
| 
 | ||||
|       IpcPacket::Next => { | ||||
|         player.next(); | ||||
|       } | ||||
| 
 | ||||
|       IpcPacket::Previous => { | ||||
|         player.previous(); | ||||
|       } | ||||
| 
 | ||||
|       IpcPacket::Pause => { | ||||
|         player.pause(); | ||||
|       } | ||||
| 
 | ||||
|       IpcPacket::Resume => { | ||||
|         player.resume(); | ||||
|       } | ||||
| 
 | ||||
|       IpcPacket::Quit => { | ||||
|         debug!("Received quit packet, exiting"); | ||||
| 
 | ||||
|  | ||||
| @ -1,3 +1,6 @@ | ||||
| pub mod manager; | ||||
| pub mod pbi; | ||||
| 
 | ||||
| use self::{ | ||||
|   manager::{SessionCreateError, SessionManager}, | ||||
|   pbi::PlaybackInfo, | ||||
| @ -30,9 +33,6 @@ use std::{ | ||||
| }; | ||||
| use tokio::sync::Mutex; | ||||
| 
 | ||||
| pub mod manager; | ||||
| mod pbi; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct SpoticordSession(Arc<RwLock<InnerSpoticordSession>>); | ||||
| 
 | ||||
| @ -149,6 +149,42 @@ impl SpoticordSession { | ||||
|     Ok(()) | ||||
|   } | ||||
| 
 | ||||
|   /// Advance to the next track
 | ||||
|   pub async fn next(&mut self) -> Result<(), IpcError> { | ||||
|     if let Some(ref client) = self.0.read().await.client { | ||||
|       return client.send(IpcPacket::Next); | ||||
|     } | ||||
| 
 | ||||
|     Ok(()) | ||||
|   } | ||||
| 
 | ||||
|   /// Rewind to the previous track
 | ||||
|   pub async fn previous(&mut self) -> Result<(), IpcError> { | ||||
|     if let Some(ref client) = self.0.read().await.client { | ||||
|       return client.send(IpcPacket::Previous); | ||||
|     } | ||||
| 
 | ||||
|     Ok(()) | ||||
|   } | ||||
| 
 | ||||
|   /// Pause the current track
 | ||||
|   pub async fn pause(&mut self) -> Result<(), IpcError> { | ||||
|     if let Some(ref client) = self.0.read().await.client { | ||||
|       return client.send(IpcPacket::Pause); | ||||
|     } | ||||
| 
 | ||||
|     Ok(()) | ||||
|   } | ||||
| 
 | ||||
|   /// Resume the current track
 | ||||
|   pub async fn resume(&mut self) -> Result<(), IpcError> { | ||||
|     if let Some(ref client) = self.0.read().await.client { | ||||
|       return client.send(IpcPacket::Resume); | ||||
|     } | ||||
| 
 | ||||
|     Ok(()) | ||||
|   } | ||||
| 
 | ||||
|   async fn create_player(&mut self, ctx: &Context) -> Result<(), SessionCreateError> { | ||||
|     let owner_id = match self.owner().await { | ||||
|       Some(owner_id) => owner_id, | ||||
|  | ||||
| @ -8,6 +8,12 @@ pub enum Status { | ||||
|   None = 0, | ||||
| } | ||||
| 
 | ||||
| impl From<Status> for serenity::utils::Colour { | ||||
|   fn from(value: Status) -> Self { | ||||
|     Self(value as u32) | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| #[derive(Default)] | ||||
| pub struct EmbedMessageOptions { | ||||
|   pub title: Option<String>, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DaXcess
						DaXcess