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]] | [[package]] | ||||||
| name = "http" | name = "http" | ||||||
| version = "0.2.8" | version = "0.2.9" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "bytes", |  "bytes", | ||||||
|  "fnv", |  "fnv", | ||||||
| @ -2459,9 +2459,9 @@ dependencies = [ | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "slab" | name = "slab" | ||||||
| version = "0.4.7" | version = "0.4.8" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "autocfg", |  "autocfg", | ||||||
| ] | ] | ||||||
| @ -2770,9 +2770,9 @@ dependencies = [ | |||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "tokio-stream" | name = "tokio-stream" | ||||||
| version = "0.1.11" | version = "0.1.12" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" | checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "futures-core", |  "futures-core", | ||||||
|  "pin-project-lite", |  "pin-project-lite", | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ use crate::{ | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "help"; | 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 { |   Box::pin(async move { | ||||||
|     respond_message( |     respond_message( | ||||||
|       &ctx, |       &ctx, | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ use crate::{ | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "link"; | 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 { |   Box::pin(async move { | ||||||
|     let data = ctx.data.read().await; |     let data = ctx.data.read().await; | ||||||
|     let database = data.get::<Database>().expect("to contain a value"); |     let database = data.get::<Database>().expect("to contain a value"); | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ use crate::{ | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "rename"; | 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 { |   Box::pin(async move { | ||||||
|     let data = ctx.data.read().await; |     let data = ctx.data.read().await; | ||||||
|     let database = data.get::<Database>().expect("to contain a value"); |     let database = data.get::<Database>().expect("to contain a value"); | ||||||
|  | |||||||
| @ -14,7 +14,7 @@ use crate::{ | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "unlink"; | 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 { |   Box::pin(async move { | ||||||
|     let data = ctx.data.read().await; |     let data = ctx.data.read().await; | ||||||
|     let database = data.get::<Database>().expect("to contain a value"); |     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 const NAME: &str = "version"; | ||||||
| 
 | 
 | ||||||
| pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { | ||||||
|   Box::pin(async move { |   Box::pin(async move { | ||||||
|     if let Err(why) = command |     if let Err(why) = command | ||||||
|       .create_interaction_response(&ctx.http, |response| { |       .create_interaction_response(&ctx.http, |response| { | ||||||
|  | |||||||
| @ -5,7 +5,10 @@ use serenity::{ | |||||||
|   builder::{CreateApplicationCommand, CreateApplicationCommands}, |   builder::{CreateApplicationCommand, CreateApplicationCommands}, | ||||||
|   model::application::command::Command, |   model::application::command::Command, | ||||||
|   model::prelude::{ |   model::prelude::{ | ||||||
|     interaction::{application_command::ApplicationCommandInteraction, InteractionResponseType}, |     interaction::{ | ||||||
|  |       application_command::ApplicationCommandInteraction, | ||||||
|  |       message_component::MessageComponentInteraction, InteractionResponseType, | ||||||
|  |     }, | ||||||
|     GuildId, |     GuildId, | ||||||
|   }, |   }, | ||||||
|   prelude::{Context, TypeMapKey}, |   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( | pub async fn update_message( | ||||||
|   ctx: &Context, |   ctx: &Context, | ||||||
|   command: &ApplicationCommandInteraction, |   command: &ApplicationCommandInteraction, | ||||||
| @ -78,6 +103,7 @@ pub async fn defer_message( | |||||||
| 
 | 
 | ||||||
| pub type CommandOutput = Pin<Box<dyn Future<Output = ()> + Send>>; | pub type CommandOutput = Pin<Box<dyn Future<Output = ()> + Send>>; | ||||||
| pub type CommandExecutor = fn(Context, ApplicationCommandInteraction) -> CommandOutput; | pub type CommandExecutor = fn(Context, ApplicationCommandInteraction) -> CommandOutput; | ||||||
|  | pub type ComponentExecutor = fn(Context, MessageComponentInteraction) -> CommandOutput; | ||||||
| 
 | 
 | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| pub struct CommandManager { | pub struct CommandManager { | ||||||
| @ -87,7 +113,8 @@ pub struct CommandManager { | |||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| pub struct CommandInfo { | pub struct CommandInfo { | ||||||
|   pub name: String, |   pub name: String, | ||||||
|   pub executor: CommandExecutor, |   pub command_executor: CommandExecutor, | ||||||
|  |   pub component_executor: Option<ComponentExecutor>, | ||||||
|   pub register: fn(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, |   pub register: fn(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -100,50 +127,71 @@ impl CommandManager { | |||||||
|     // Debug-only commands
 |     // Debug-only commands
 | ||||||
|     #[cfg(debug_assertions)] |     #[cfg(debug_assertions)] | ||||||
|     { |     { | ||||||
|       instance.insert_command(ping::NAME, ping::register, ping::run); |       instance.insert(ping::NAME, ping::register, ping::command, None); | ||||||
|       instance.insert_command(token::NAME, token::register, token::run); |       instance.insert(token::NAME, token::register, token::command, None); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Core commands
 |     // Core commands
 | ||||||
|     instance.insert_command(core::help::NAME, core::help::register, core::help::run); |     instance.insert( | ||||||
|     instance.insert_command( |       core::help::NAME, | ||||||
|  |       core::help::register, | ||||||
|  |       core::help::command, | ||||||
|  |       None, | ||||||
|  |     ); | ||||||
|  |     instance.insert( | ||||||
|       core::version::NAME, |       core::version::NAME, | ||||||
|       core::version::register, |       core::version::register, | ||||||
|       core::version::run, |       core::version::command, | ||||||
|  |       None, | ||||||
|     ); |     ); | ||||||
|     instance.insert_command(core::link::NAME, core::link::register, core::link::run); |     instance.insert( | ||||||
|     instance.insert_command( |       core::link::NAME, | ||||||
|  |       core::link::register, | ||||||
|  |       core::link::command, | ||||||
|  |       None, | ||||||
|  |     ); | ||||||
|  |     instance.insert( | ||||||
|       core::unlink::NAME, |       core::unlink::NAME, | ||||||
|       core::unlink::register, |       core::unlink::register, | ||||||
|       core::unlink::run, |       core::unlink::command, | ||||||
|  |       None, | ||||||
|     ); |     ); | ||||||
|     instance.insert_command( |     instance.insert( | ||||||
|       core::rename::NAME, |       core::rename::NAME, | ||||||
|       core::rename::register, |       core::rename::register, | ||||||
|       core::rename::run, |       core::rename::command, | ||||||
|  |       None, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     // Music commands
 |     // Music commands
 | ||||||
|     instance.insert_command(music::join::NAME, music::join::register, music::join::run); |     instance.insert( | ||||||
|     instance.insert_command( |       music::join::NAME, | ||||||
|  |       music::join::register, | ||||||
|  |       music::join::command, | ||||||
|  |       None, | ||||||
|  |     ); | ||||||
|  |     instance.insert( | ||||||
|       music::leave::NAME, |       music::leave::NAME, | ||||||
|       music::leave::register, |       music::leave::register, | ||||||
|       music::leave::run, |       music::leave::command, | ||||||
|  |       None, | ||||||
|     ); |     ); | ||||||
|     instance.insert_command( |     instance.insert( | ||||||
|       music::playing::NAME, |       music::playing::NAME, | ||||||
|       music::playing::register, |       music::playing::register, | ||||||
|       music::playing::run, |       music::playing::command, | ||||||
|  |       Some(music::playing::component), | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     instance |     instance | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   pub fn insert_command( |   pub fn insert( | ||||||
|     &mut self, |     &mut self, | ||||||
|     name: impl Into<String>, |     name: impl Into<String>, | ||||||
|     register: fn(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, |     register: fn(&mut CreateApplicationCommand) -> &mut CreateApplicationCommand, | ||||||
|     executor: CommandExecutor, |     command_executor: CommandExecutor, | ||||||
|  |     component_executor: Option<ComponentExecutor>, | ||||||
|   ) { |   ) { | ||||||
|     let name = name.into(); |     let name = name.into(); | ||||||
| 
 | 
 | ||||||
| @ -152,12 +200,13 @@ impl CommandManager { | |||||||
|       CommandInfo { |       CommandInfo { | ||||||
|         name, |         name, | ||||||
|         register, |         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; |     let cmds = &self.commands; | ||||||
| 
 | 
 | ||||||
|     debug!( |     debug!( | ||||||
| @ -196,11 +245,12 @@ impl CommandManager { | |||||||
|     .expect("Failed to create global commands"); |     .expect("Failed to create global commands"); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // On slash command interaction
 | ||||||
|   pub async fn execute_command(&self, ctx: &Context, interaction: ApplicationCommandInteraction) { |   pub async fn execute_command(&self, ctx: &Context, interaction: ApplicationCommandInteraction) { | ||||||
|     let command = self.commands.get(&interaction.data.name); |     let command = self.commands.get(&interaction.data.name); | ||||||
| 
 | 
 | ||||||
|     if let Some(command) = command { |     if let Some(command) = command { | ||||||
|       (command.executor)(ctx.clone(), interaction.clone()).await; |       (command.command_executor)(ctx.clone(), interaction.clone()).await; | ||||||
|     } else { |     } else { | ||||||
|       // Command does not exist
 |       // Command does not exist
 | ||||||
|       if let Err(why) = interaction |       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 { | impl TypeMapKey for CommandManager { | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ use crate::{ | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "join"; | 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 { |   Box::pin(async move { | ||||||
|     let guild = ctx |     let guild = ctx | ||||||
|       .cache |       .cache | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ use crate::{ | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "leave"; | 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 { |   Box::pin(async move { | ||||||
|     let data = ctx.data.read().await; |     let data = ctx.data.read().await; | ||||||
|     let session_manager = data |     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 log::error; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|   builder::CreateApplicationCommand, |   builder::{CreateApplicationCommand, CreateButton, CreateComponents, CreateEmbed}, | ||||||
|   model::prelude::interaction::{ |   model::{ | ||||||
|     application_command::ApplicationCommandInteraction, InteractionResponseType, |     prelude::{ | ||||||
|  |       component::ButtonStyle, | ||||||
|  |       interaction::{ | ||||||
|  |         application_command::ApplicationCommandInteraction, | ||||||
|  |         message_component::MessageComponentInteraction, InteractionResponseType, | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     user::User, | ||||||
|   }, |   }, | ||||||
|   prelude::Context, |   prelude::Context, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use crate::{ | use crate::{ | ||||||
|   bot::commands::{respond_message, CommandOutput}, |   bot::commands::{respond_component_message, respond_message, CommandOutput}, | ||||||
|   session::manager::SessionManager, |   session::{manager::SessionManager, pbi::PlaybackInfo}, | ||||||
|   utils::{ |   utils::{ | ||||||
|     self, |     self, | ||||||
|     embed::{EmbedBuilder, Status}, |     embed::{EmbedBuilder, Status}, | ||||||
| @ -19,7 +28,7 @@ use crate::{ | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "playing"; | 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 { |   Box::pin(async move { | ||||||
|     let not_playing = async { |     let not_playing = async { | ||||||
|       respond_message( |       respond_message( | ||||||
| @ -27,7 +36,7 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu | |||||||
|         &command, |         &command, | ||||||
|         EmbedBuilder::new() |         EmbedBuilder::new() | ||||||
|           .title("Cannot get track info") |           .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") |           .description("I'm currently not playing any music in this server") | ||||||
|           .status(Status::Error) |           .status(Status::Error) | ||||||
|           .build(), |           .build(), | ||||||
| @ -82,50 +91,13 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu | |||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // Get audio type
 |  | ||||||
|     let audio_type = if spotify_id.audio_type == SpotifyAudioType::Track { |  | ||||||
|       "track" |  | ||||||
|     } else { |  | ||||||
|       "episode" |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     // Create title
 |  | ||||||
|     let title = format!( |  | ||||||
|       "{} - {}", |  | ||||||
|       pbi.get_artists().expect("to contain a value"), |  | ||||||
|       pbi.get_name().expect("to contain a value") |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     // Create description
 |  | ||||||
|     let mut description = String::new(); |  | ||||||
| 
 |  | ||||||
|     let position = pbi.get_position(); |  | ||||||
|     let spot = position * 20 / pbi.duration_ms; |  | ||||||
| 
 |  | ||||||
|     description.push_str(if pbi.is_playing { "▶️ " } else { "⏸️ " }); |  | ||||||
| 
 |  | ||||||
|     for i in 0..20 { |  | ||||||
|       if i == spot { |  | ||||||
|         description.push('🔵'); |  | ||||||
|       } else { |  | ||||||
|         description.push('▬'); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     description.push_str("\n:alarm_clock: "); |  | ||||||
|     description.push_str(&format!( |  | ||||||
|       "{} / {}", |  | ||||||
|       utils::time_to_str(position / 1000), |  | ||||||
|       utils::time_to_str(pbi.duration_ms / 1000) |  | ||||||
|     )); |  | ||||||
| 
 |  | ||||||
|     // Get owner of session
 |     // Get owner of session
 | ||||||
|     let owner = match utils::discord::get_user(&ctx, owner).await { |     let owner = match utils::discord::get_user(&ctx, owner).await { | ||||||
|       Some(user) => user, |       Some(user) => user, | ||||||
|       None => { |       None => { | ||||||
|         // This shouldn't happen
 |         // This shouldn't happen
 | ||||||
| 
 | 
 | ||||||
|         error!("Could not find user with id {}", owner); |         error!("Could not find user with ID: {owner}"); | ||||||
| 
 | 
 | ||||||
|         respond_message( |         respond_message( | ||||||
|           &ctx, |           &ctx, | ||||||
| @ -133,7 +105,7 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu | |||||||
|           EmbedBuilder::new() |           EmbedBuilder::new() | ||||||
|             .title("[INTERNAL ERROR] Cannot get track info") |             .title("[INTERNAL ERROR] Cannot get track info") | ||||||
|             .description(format!( |             .description(format!( | ||||||
|               "Could not find user with id {}\nThis is an issue with the bot!", |               "Could not find user with ID `{}`\nThis is an issue with the bot!", | ||||||
|               owner |               owner | ||||||
|             )) |             )) | ||||||
|             .status(Status::Error) |             .status(Status::Error) | ||||||
| @ -146,45 +118,416 @@ pub fn run(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutpu | |||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     // Get the thumbnail image
 |     // Get metadata
 | ||||||
|     let thumbnail = pbi.get_thumbnail_url().expect("to contain a value"); |     let (title, description, audio_type, thumbnail) = get_metadata(spotify_id, &pbi); | ||||||
| 
 | 
 | ||||||
|     if let Err(why) = command |     if let Err(why) = command | ||||||
|       .create_interaction_response(&ctx.http, |response| { |       .create_interaction_response(&ctx.http, |response| { | ||||||
|         response |         response | ||||||
|           .kind(InteractionResponseType::ChannelMessageWithSource) |           .kind(InteractionResponseType::ChannelMessageWithSource) | ||||||
|           .interaction_response_data(|message| { |           .interaction_response_data(|message| { | ||||||
|             message.embed(|embed| { |             message | ||||||
|               embed |               .set_embed(build_playing_embed( | ||||||
|                 .author(|author| { |                 title, | ||||||
|                   author |                 audio_type, | ||||||
|                     .name("Currently Playing") |                 spotify_id, | ||||||
|                     .icon_url("https://spoticord.com/spotify-logo.png") |                 description, | ||||||
|                 }) |                 owner, | ||||||
|                 .title(title) |                 thumbnail, | ||||||
|                 .url(format!( |               )) | ||||||
|                   "https://open.spotify.com/{}/{}", |               .components(|components| create_button(components, pbi.is_playing)) | ||||||
|                   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 |       .await | ||||||
|     { |     { | ||||||
|       error!("Error sending message: {:?}", why); |       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 { | pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { | ||||||
|   command |   command | ||||||
|     .name(NAME) |     .name(NAME) | ||||||
|     .description("Display which song is currently being played") |     .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" | ||||||
|  |   } else { | ||||||
|  |     "episode" | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   // Create title
 | ||||||
|  |   let title = format!( | ||||||
|  |     "{} - {}", | ||||||
|  |     pbi.get_artists().as_deref().unwrap_or("ID"), | ||||||
|  |     pbi.get_name().as_deref().unwrap_or("ID") | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   // Create description
 | ||||||
|  |   let mut description = String::new(); | ||||||
|  | 
 | ||||||
|  |   let position = pbi.get_position(); | ||||||
|  |   let spot = position * 20 / pbi.duration_ms; | ||||||
|  | 
 | ||||||
|  |   description.push_str(if pbi.is_playing { "▶️ " } else { "⏸️ " }); | ||||||
|  | 
 | ||||||
|  |   for i in 0..20 { | ||||||
|  |     if i == spot { | ||||||
|  |       description.push('🔵'); | ||||||
|  |     } else { | ||||||
|  |       description.push('▬'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   description.push_str("\n:alarm_clock: "); | ||||||
|  |   description.push_str(&format!( | ||||||
|  |     "{} / {}", | ||||||
|  |     utils::time_to_str(position / 1000), | ||||||
|  |     utils::time_to_str(pbi.duration_ms / 1000) | ||||||
|  |   )); | ||||||
|  | 
 | ||||||
|  |   // Get the thumbnail image
 | ||||||
|  |   let thumbnail = pbi.get_thumbnail_url().expect("to contain a value"); | ||||||
|  | 
 | ||||||
|  |   (title, description, audio_type.to_string(), thumbnail) | ||||||
|  | } | ||||||
|  | |||||||
| @ -11,7 +11,7 @@ use super::CommandOutput; | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "ping"; | 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 { |   Box::pin(async move { | ||||||
|     info!("Pong!"); |     info!("Pong!"); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ use super::CommandOutput; | |||||||
| 
 | 
 | ||||||
| pub const NAME: &str = "token"; | 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 { |   Box::pin(async move { | ||||||
|     let data = ctx.data.read().await; |     let data = ctx.data.read().await; | ||||||
|     let db = data.get::<Database>().expect("to contain a value"); |     let db = data.get::<Database>().expect("to contain a value"); | ||||||
|  | |||||||
| @ -3,7 +3,13 @@ | |||||||
| use log::*; | use log::*; | ||||||
| use serenity::{ | use serenity::{ | ||||||
|   async_trait, |   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}, |   prelude::{Context, EventHandler}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| @ -11,6 +17,23 @@ use crate::consts::MOTD; | |||||||
| 
 | 
 | ||||||
| use super::commands::CommandManager; | 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
 | // Handler struct with a command parameter, an array of dictionary which takes a string and function
 | ||||||
| pub struct Handler; | pub struct Handler; | ||||||
| 
 | 
 | ||||||
| @ -25,7 +48,7 @@ impl EventHandler for Handler { | |||||||
| 
 | 
 | ||||||
|     // Set this to true only when a command is removed/updated/created
 |     // Set this to true only when a command is removed/updated/created
 | ||||||
|     if false { |     if false { | ||||||
|       command_manager.register_commands(&ctx).await; |       command_manager.register(&ctx).await; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ctx.set_activity(Activity::listening(MOTD)).await; |     ctx.set_activity(Activity::listening(MOTD)).await; | ||||||
| @ -35,52 +58,86 @@ impl EventHandler for Handler { | |||||||
| 
 | 
 | ||||||
|   // INTERACTION_CREATE event, emitted when the bot receives an interaction (slash command, button, etc.)
 |   // INTERACTION_CREATE event, emitted when the bot receives an interaction (slash command, button, etc.)
 | ||||||
|   async fn interaction_create(&self, ctx: Context, interaction: Interaction) { |   async fn interaction_create(&self, ctx: Context, interaction: Interaction) { | ||||||
|     if let Interaction::ApplicationCommand(command) = interaction { |     match interaction { | ||||||
|       if let Ok(guild_id) = std::env::var("GUILD_ID") { |       Interaction::ApplicationCommand(command) => self.handle_command(ctx, command).await, | ||||||
|         if let Ok(guild_id) = guild_id.parse::<u64>() { |       Interaction::MessageComponent(component) => self.handle_component(ctx, component).await, | ||||||
|           let guild_id = GuildId(guild_id); |       _ => {} | ||||||
| 
 |  | ||||||
|           if let Some(interaction_guild_id) = command.guild_id { |  | ||||||
|             if guild_id != interaction_guild_id { |  | ||||||
|               return; |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       // Commands must only be executed inside of guilds
 |  | ||||||
| 
 |  | ||||||
|       let guild_id = match command.guild_id { |  | ||||||
|         Some(guild_id) => guild_id, |  | ||||||
|         None => { |  | ||||||
|           if let Err(why) = command |  | ||||||
|           .create_interaction_response(&ctx.http, |response| { |  | ||||||
|             response |  | ||||||
|               .kind(serenity::model::prelude::interaction::InteractionResponseType::ChannelMessageWithSource) |  | ||||||
|               .interaction_response_data(|message| { |  | ||||||
|                 message.content("You can only execute commands inside of a server") |  | ||||||
|               }) |  | ||||||
|           }) |  | ||||||
|           .await { |  | ||||||
|             error!("Failed to send run-in-guild-only error message: {}", why); |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           trace!("interaction_create END2"); |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|       trace!( |  | ||||||
|         "Received command interaction: command={} user={} guild={}", |  | ||||||
|         command.data.name, |  | ||||||
|         command.user.id, |  | ||||||
|         guild_id |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|       let data = ctx.data.read().await; |  | ||||||
|       let command_manager = data.get::<CommandManager>().expect("to contain a value"); |  | ||||||
| 
 |  | ||||||
|       command_manager.execute_command(&ctx, command).await; |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | impl Handler { | ||||||
|  |   async fn handle_command(&self, ctx: Context, command: ApplicationCommandInteraction) { | ||||||
|  |     enforce_guild!(command); | ||||||
|  | 
 | ||||||
|  |     // Commands must only be executed inside of guilds
 | ||||||
|  | 
 | ||||||
|  |     let guild_id = match command.guild_id { | ||||||
|  |       Some(guild_id) => guild_id, | ||||||
|  |       None => { | ||||||
|  |         if let Err(why) = command | ||||||
|  |              .create_interaction_response(&ctx.http, |response| { | ||||||
|  |                response | ||||||
|  |                  .kind(serenity::model::prelude::interaction::InteractionResponseType::ChannelMessageWithSource) | ||||||
|  |                  .interaction_response_data(|message| { | ||||||
|  |                    message.content("You can only execute commands inside of a server") | ||||||
|  |                  }) | ||||||
|  |              }) | ||||||
|  |              .await { | ||||||
|  |                error!("Failed to send run-in-guild-only error message: {}", why); | ||||||
|  |              } | ||||||
|  | 
 | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     trace!( | ||||||
|  |       "Received command interaction: command={} user={} guild={}", | ||||||
|  |       command.data.name, | ||||||
|  |       command.user.id, | ||||||
|  |       guild_id | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     let data = ctx.data.read().await; | ||||||
|  |     let command_manager = data.get::<CommandManager>().expect("to contain a value"); | ||||||
|  | 
 | ||||||
|  |     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
 |   /// Sent when the user has switched their Spotify device away from Spoticord
 | ||||||
|   Stopped, |   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 { | impl SpoticordPlayer { | ||||||
|   pub fn create(client: ipc::Client) -> Self { |   pub fn new(client: ipc::Client) -> Self { | ||||||
|     Self { |     Self { | ||||||
|       client, |       client, | ||||||
|       session: None, |       session: None, | ||||||
| @ -223,6 +223,30 @@ impl SpoticordPlayer { | |||||||
|     tokio::spawn(spirc_task); |     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) { |   pub fn stop(&mut self) { | ||||||
|     if let Some(spirc) = self.spirc.take() { |     if let Some(spirc) = self.spirc.take() { | ||||||
|       spirc.shutdown(); |       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"); |   let client = ipc::Client::connect(tx_name, rx_name).expect("Failed to connect to IPC"); | ||||||
| 
 | 
 | ||||||
|   // Create the player
 |   // Create the player
 | ||||||
|   let mut player = SpoticordPlayer::create(client.clone()); |   let mut player = SpoticordPlayer::new(client.clone()); | ||||||
| 
 | 
 | ||||||
|   loop { |   loop { | ||||||
|     let message = match client.try_recv() { |     let message = match client.try_recv() { | ||||||
| @ -274,6 +298,22 @@ pub async fn main() { | |||||||
|         player.stop(); |         player.stop(); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|  |       IpcPacket::Next => { | ||||||
|  |         player.next(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       IpcPacket::Previous => { | ||||||
|  |         player.previous(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       IpcPacket::Pause => { | ||||||
|  |         player.pause(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       IpcPacket::Resume => { | ||||||
|  |         player.resume(); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       IpcPacket::Quit => { |       IpcPacket::Quit => { | ||||||
|         debug!("Received quit packet, exiting"); |         debug!("Received quit packet, exiting"); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,3 +1,6 @@ | |||||||
|  | pub mod manager; | ||||||
|  | pub mod pbi; | ||||||
|  | 
 | ||||||
| use self::{ | use self::{ | ||||||
|   manager::{SessionCreateError, SessionManager}, |   manager::{SessionCreateError, SessionManager}, | ||||||
|   pbi::PlaybackInfo, |   pbi::PlaybackInfo, | ||||||
| @ -30,9 +33,6 @@ use std::{ | |||||||
| }; | }; | ||||||
| use tokio::sync::Mutex; | use tokio::sync::Mutex; | ||||||
| 
 | 
 | ||||||
| pub mod manager; |  | ||||||
| mod pbi; |  | ||||||
| 
 |  | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| pub struct SpoticordSession(Arc<RwLock<InnerSpoticordSession>>); | pub struct SpoticordSession(Arc<RwLock<InnerSpoticordSession>>); | ||||||
| 
 | 
 | ||||||
| @ -149,6 +149,42 @@ impl SpoticordSession { | |||||||
|     Ok(()) |     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> { |   async fn create_player(&mut self, ctx: &Context) -> Result<(), SessionCreateError> { | ||||||
|     let owner_id = match self.owner().await { |     let owner_id = match self.owner().await { | ||||||
|       Some(owner_id) => owner_id, |       Some(owner_id) => owner_id, | ||||||
|  | |||||||
| @ -8,6 +8,12 @@ pub enum Status { | |||||||
|   None = 0, |   None = 0, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | impl From<Status> for serenity::utils::Colour { | ||||||
|  |   fn from(value: Status) -> Self { | ||||||
|  |     Self(value as u32) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #[derive(Default)] | #[derive(Default)] | ||||||
| pub struct EmbedMessageOptions { | pub struct EmbedMessageOptions { | ||||||
|   pub title: Option<String>, |   pub title: Option<String>, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DaXcess
						DaXcess