From c6fa8aea9d413b56937fa7e024e2fbd0b5f17ebd Mon Sep 17 00:00:00 2001 From: Ladislav Hano Date: Sun, 8 Dec 2024 15:41:50 +0100 Subject: [PATCH] feat: connect command implementation --- src/commands/mod.rs | 2 + src/commands/voice/mod.rs | 1 + src/commands/voice/spotify/mod.rs | 1 + src/commands/voice/spotify/player.rs | 159 +++++++++++++++++++++++++++ src/main.rs | 25 +++-- src/util/utilities.rs | 27 ++++- 6 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 src/commands/voice/mod.rs create mode 100644 src/commands/voice/spotify/mod.rs create mode 100644 src/commands/voice/spotify/player.rs diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 92a8ea2..1095c46 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,10 +2,12 @@ pub use moover::*; pub use notice::*; pub use other::*; pub use user_interactions::*; +pub use voice::*; pub mod moover; pub mod notice; pub mod user_interactions; pub mod other; +pub mod voice; pub mod channel_test; \ No newline at end of file diff --git a/src/commands/voice/mod.rs b/src/commands/voice/mod.rs new file mode 100644 index 0000000..9c66493 --- /dev/null +++ b/src/commands/voice/mod.rs @@ -0,0 +1 @@ +pub mod spotify; \ No newline at end of file diff --git a/src/commands/voice/spotify/mod.rs b/src/commands/voice/spotify/mod.rs new file mode 100644 index 0000000..d44230b --- /dev/null +++ b/src/commands/voice/spotify/mod.rs @@ -0,0 +1 @@ +pub mod player; \ No newline at end of file diff --git a/src/commands/voice/spotify/player.rs b/src/commands/voice/spotify/player.rs new file mode 100644 index 0000000..37e9a16 --- /dev/null +++ b/src/commands/voice/spotify/player.rs @@ -0,0 +1,159 @@ +use poise; +use serenity::{all::ChannelId, async_trait}; +use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler}; +use songbird::TrackEvent; + +use crate::util::debug::send_error; +use crate::{types::{Context, Error}, util::utilities::get_channel_by_name}; + +use serenity::all::User; + +#[poise::command( + slash_command, + description_localized("en-US", "Play song") +)] +pub async fn play(ctx: Context<'_>, + #[description = "Song: "] + _url: String +) -> Result<(), Error> { + + ctx.reply("Done!").await?; + Ok(()) +} + + + +#[poise::command( + slash_command, + description_localized("en-US", "Headpat all your friends!") +)] +pub async fn headpat(ctx: Context<'_>, + #[description = "Who is the lucky one?"] + user: User +) -> Result<(), Error> { + let _title = "HEADPATS!"; + let _desc = format!("{} headpats {}", ctx.author(), user); + // send_with_embed(ctx, "headpat", &title, &desc).await?; + ctx.reply("Done!").await?; + Ok(()) +} + + +#[poise::command( + slash_command, + description_localized("en-US", "Connect to channel") +)] +pub async fn connect(ctx: Context<'_>, + #[autocomplete = "autocomplete_channel"] + #[description = "Voice channel name: "] + name: Option +) -> Result<(), Error> { + + if ctx.guild().is_none() { + ctx.reply("Can't use this outside of guild").await?; + return Ok(()) + } + + let channel: ChannelId; + if name.is_none() || name.clone().unwrap() == "" { + // Ugly one liner since I don't know how to do this another way yet + // TODO fix please + let Some(voice_channel) = ctx.guild().unwrap().voice_states.get(&ctx.author().id).and_then(|voice_state| voice_state.channel_id) else { + ctx.reply("You must be in a voice channel or specify explicit voice channel by name").await?; + return Ok(()) + }; + + channel = voice_channel; + } + else { + channel = match get_channel_by_name(ctx.guild().unwrap(), name.unwrap()) { + Some(channel) => channel, + None => { + ctx.reply("Channel with this name does not exist").await?; + return Ok(()); + } + }; + } + + let manager = songbird::get(ctx.serenity_context()) + .await + .expect("Songbird Voice client placed in at initialisation.") + .clone(); + + if let Ok(handler_lock) = manager.join(ctx.guild_id().unwrap(), channel).await { + let mut handler = handler_lock.lock().await; + handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); + } + + ctx.reply("Done!").await?; + Ok(()) +} + + +struct TrackErrorNotifier; + +#[async_trait] +impl VoiceEventHandler for TrackErrorNotifier { + async fn act(&self, ctx: &EventContext<'_>) -> Option { + if let EventContext::Track(track_list) = ctx { + for (state, handle) in *track_list { + println!( + "Track {:?} encountered an error: {:?}", + handle.uuid(), + state.playing + ); + } + } + + None + } +} + +async fn autocomplete_channel( + ctx: Context<'_>, + _partial: &str, +) -> Vec { + + use crate::util::utilities::get_vc_names; + match ctx.guild() { + Some(guild) => get_vc_names(guild), + None => [].to_vec() + } +} + +#[poise::command( + slash_command, + description_localized("en-US", "Disconnect from voice channel") +)] +async fn disconnect( + ctx: Context<'_> +) -> Result<(), Error> { + + let Some(guild_id) = ctx.guild_id() else { + ctx.reply("Can't use this outside of guild").await?; + return Ok(()); + }; + + let manager = songbird::get(ctx.serenity_context()) + .await + .expect("Songbird Voice client placed in at initialisation.") + .clone(); + + let has_handler = manager.get(guild_id).is_some(); + + if ! has_handler { + ctx.reply("I am not connected to a channel!").await?; + return Ok(()) + } + + match manager.remove(guild_id).await { + Ok(()) => { + ctx.reply("Disconnected").await?; + return Ok(()) + } + Err(e) => { + let _ = send_error(ctx.serenity_context().http.clone(), e.to_string()).await; + return Ok(()) + } + } +} diff --git a/src/main.rs b/src/main.rs index 59da021..abfaf56 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,6 @@ use std::time::Duration; use std::error; use std::env; -use poise::samples::register_globally; - -use poise::samples::register_in_guild; use serenity::async_trait; use serenity::prelude::GatewayIntents; use serenity::client::Context; @@ -17,6 +14,7 @@ use serenity::Client; use dotenv::dotenv; +use songbird::SerenityInit; use tokio_cron_scheduler::{JobScheduler, Job}; mod message_handler; @@ -85,7 +83,9 @@ async fn main() -> anyhow::Result<()> { let options: poise::FrameworkOptions<(), Box> = poise::FrameworkOptions { commands: vec![ commands::say(), - commands::hug() + commands::hug(), + // commands::spotify::player::play(), + commands::spotify::player::connect(), ], prefix_options: poise::PrefixFrameworkOptions { prefix: Some("/".into()), @@ -107,14 +107,20 @@ async fn main() -> anyhow::Result<()> { let framework = poise::Framework::builder() .setup(move |ctx, _ready, framework| { Box::pin(async move { + + #[cfg(feature="GUILD_COMMAND")] { + use poise::samples::register_in_guild; + use serenity::all::GuildId; + let debug_guild_id = env::var("DEBUG_GUILD_ID") .context("DEBUG_GUILD_ID not found in env")? .parse::().unwrap(); - - register_in_guild(ctx, &framework.options().commands, debug_guild_id).await?; + + register_in_guild(ctx, &framework.options().commands, GuildId::new(debug_guild_id)).await?; } #[cfg(not(feature="GUILD_COMMAND"))] { + use poise::samples::register_globally; register_globally(ctx, &framework.options().commands).await?; } Ok(()) @@ -130,11 +136,16 @@ async fn main() -> anyhow::Result<()> { let token = env::var(token_str).context("TOKEN not found in env")?; - let intents = GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT; + let intents = GatewayIntents::GUILD_MESSAGES + | GatewayIntents::MESSAGE_CONTENT + | GatewayIntents::GUILD_VOICE_STATES + | GatewayIntents::GUILD_MESSAGE_REACTIONS + | GatewayIntents::GUILD_MESSAGE_TYPING; let mut client = Client::builder(&token, intents) .event_handler(Handler) .framework(framework) + .register_songbird() .await .context("Failed to build client")?; diff --git a/src/util/utilities.rs b/src/util/utilities.rs index 8b5f470..3bac776 100644 --- a/src/util/utilities.rs +++ b/src/util/utilities.rs @@ -1,7 +1,8 @@ use std::sync::Arc; -use serenity::{all::{ChannelId, CreateMessage, GuildId, Message}, http::Http}; +use serenity::{all::{ChannelId, ChannelType, CreateMessage, GuildChannel, GuildId, GuildRef, Message}, http::Http}; +use crate::types; use anyhow::Context; pub async fn get_system_channel(guild_id: GuildId, http: &Http) -> anyhow::Result { @@ -13,4 +14,28 @@ pub async fn replace_msg(http: Arc, msg: Message, content: String) -> Resu msg.delete(http.clone()).await?; return ChannelId::new(msg.channel_id.get()).send_message(http.clone(), CreateMessage::new().content(content)).await; +} + +pub fn get_vc_names(guild: GuildRef) -> Vec { + + let mut result: Vec = [].to_vec(); + for (_, channel) in &guild.channels { + if channel.kind == ChannelType::Voice { + result.push(channel.name.clone()); + } + } + + result +} + +pub fn get_channel_by_name(guild: GuildRef, name: String) -> Option { + let mut result = None; + for (_, channel) in &guild.channels { + if channel.name == name { + result = Some(channel.id); + break; + } + } + + result } \ No newline at end of file