diff --git a/src/command/join.rs b/src/command/join.rs index 35c86e7..dc3aee9 100644 --- a/src/command/join.rs +++ b/src/command/join.rs @@ -1,12 +1,23 @@ use std::sync::LazyLock; -use twilight_model::application::{ - command::{Command, CommandType}, - interaction::{Interaction, application_command::CommandData}, +use snafu::{OptionExt, Snafu}; +use twilight_model::{ + application::{ + command::{Command, CommandType}, + interaction::Interaction, + }, + channel::message::{Embed, MessageFlags}, + http::interaction::{InteractionResponse, InteractionResponseType}, + id::{ + Id, + marker::{ChannelMarker, GuildMarker}, + }, +}; +use twilight_util::builder::{ + InteractionResponseDataBuilder, command::CommandBuilder, embed::EmbedBuilder, }; -use twilight_util::builder::command::CommandBuilder; -use crate::command::State; +use crate::{VCs, command::State}; const NAME: &str = "join"; const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)"; @@ -18,24 +29,103 @@ pub static COMMAND: LazyLock = LazyLock::new(|| { .build() }); +#[derive(Debug, Snafu)] +enum GetGuildAndChannelIdError { + /// this command was not used inside a guild (Discord server) + NotInGuild, + + /// there is no user who invoked this command + NoUser, + + /// there are no voice chats in this guild + NoVCsInGuild, + + /// the user is not in a voice chat in this guild + UserNotInVC, +} + +impl From for Embed { + fn from(error: GetGuildAndChannelIdError) -> Embed { + match error { + GetGuildAndChannelIdError::NotInGuild => { + EmbedBuilder::new().title("Use this in a server").description("This bot can't find a VC to join if the command is used outside of a server (you might've used it in a DM?).").validate().unwrap().build() + } + GetGuildAndChannelIdError::NoUser => EmbedBuilder::new().title("Not invoked by a user").description("This command works by joining the same VC as the user, but this bot didn't receive any user data. So did no user invoke it?! (This error should be impossible!)").validate().unwrap().build(), + GetGuildAndChannelIdError::NoVCsInGuild => { + EmbedBuilder::new().title("No VCs in this server").description("This bot can't find a VC to join because there aren't any in this server right now.").validate().unwrap().build() + }, + GetGuildAndChannelIdError::UserNotInVC => { + + EmbedBuilder::new().title("You're not in a VC").description("This bot can't follow you into VC if you aren't in one in this server.").validate().unwrap().build() + }, + } + } +} + +#[tracing::instrument] +fn get_guild_and_channel_id( + interaction: &Interaction, + vcs: &VCs, +) -> Result<(Id, Id), GetGuildAndChannelIdError> { + let guild_id = interaction.guild_id.context(NotInGuildSnafu)?; + + let user_id = interaction + .member + .as_ref() + .and_then(|member| member.user.as_ref().map(|user| user.id)) + .context(NoUserSnafu)?; + + let guild_vcs = vcs.get(&guild_id).context(NoVCsInGuildSnafu)?; + + let &channel_id = guild_vcs.get_left_for(&user_id).context(UserNotInVCSnafu)?; + + Ok((guild_id, channel_id)) +} + #[tracing::instrument] pub async fn handle(state: State, interaction: Interaction) { let vcs = state.vcs; - let guild_id = interaction.guild_id.expect("TODO"); - // let user_id = data.user.map(|user| user.id).expect("TODO"); + let (guild_id, channel_id) = match get_guild_and_channel_id(&interaction, &vcs) { + Ok((guild_id, channel_id)) => (guild_id, channel_id), + Err(error) => { + state + .discord_client + .interaction(state.discord_application_id) + .create_response( + interaction.id, + &interaction.token, + &InteractionResponse { + kind: InteractionResponseType::ChannelMessageWithSource, + data: Some( + InteractionResponseDataBuilder::new() + .embeds([error.into()]) + .flags(MessageFlags::EPHEMERAL) + .build(), + ), + }, + ) + .await + .expect("TODO"); - let user_id = interaction - .member - .and_then(|member| member.user.map(|user| user.id)) + return; + } + }; + + state + .discord_client + .interaction(state.discord_application_id) + .create_response( + interaction.id, + &interaction.token, + &InteractionResponse { + kind: InteractionResponseType::DeferredChannelMessageWithSource, + data: None, + }, + ) + .await .expect("TODO"); - let guild_vcs = vcs.get(&guild_id).expect("TODO"); - tracing::error!(?guild_vcs, "TODO"); - - let (&channel_id, user_in_vc_data) = guild_vcs.get_left_and_data_for(&user_id).expect("TODO"); - tracing::error!(?user_in_vc_data, "TODO"); - let call = tokio::spawn({ let songbird = state.songbird.clone(); async move { songbird.join(guild_id, channel_id).await } @@ -44,5 +134,20 @@ pub async fn handle(state: State, interaction: Interaction) { .unwrap() .expect("TODO"); + let channel_mention = format!("<#{channel_id}>"); + + state + .discord_client + .interaction(state.discord_application_id) + .update_response( + &interaction.token, + ).embeds(Some(&[ + EmbedBuilder::new().title("Joined VC with intent to record").description(&format!("This bot joined {channel_mention} and intends to start recording after a responsible disclosure and consent.")).validate().unwrap().build() + ])) + .await + .expect("TODO"); + tracing::error!(?call, "TODO"); + + let call_guard = call.lock().await; } diff --git a/src/command/mod.rs b/src/command/mod.rs index 3254fd6..1083702 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,88 +1,93 @@ -use std::{fmt::Debug, sync::Arc}; - -use futures::future::BoxFuture; -use patricia_tree::StringPatriciaMap; -use songbird::Songbird; -use twilight_model::application::{command::Command, interaction::Interaction}; - -use crate::VCs; - -mod join; -mod leave; -mod opt_out; - -#[derive(Debug, Clone)] -pub struct State { - pub vcs: Arc, - pub songbird: Arc, -} - -type Return = (); -type BoxedHandler = Box BoxFuture<'static, Return>>; - -fn box_handler(handler: Handler) -> BoxedHandler -where - Fut: Future + Send + 'static, - Handler: Fn(State, Interaction) -> Fut + 'static, -{ - Box::new(move |state, interaction| Box::pin(handler(state, interaction))) -} - -pub fn all() -> Vec<(&'static Command, BoxedHandler)> { - vec![ - (&join::COMMAND, box_handler(join::handle)), - (&leave::COMMAND, box_handler(leave::handle)), - (&opt_out::COMMAND, box_handler(opt_out::handle)), - ] -} - -#[derive(Default)] -pub struct Router { - map: StringPatriciaMap, -} - -impl Router { - fn add_route<'s, 'a, Fut, Handler>(&'s mut self, name: &'a str, handler: Handler) - where - Fut: Future + Send + 'static, - Handler: Fn(State, Interaction) -> Fut + 'static, - { - self.add_route_already_boxed(name, box_handler(handler)); - } - - fn add_route_already_boxed<'s, 'a>(&'s mut self, name: &'a str, boxed_handler: BoxedHandler) { - self.map.insert(name, boxed_handler); - } - - pub async fn handle( - &self, - state: State, - command_name: &str, - interaction: Interaction, - ) -> Option { - let handler = self.map.get(command_name)?; - - Some(handler(state, interaction).await) - } -} - -impl<'a> FromIterator<(&'a Command, BoxedHandler)> for Router { - #[inline] - fn from_iter>(iter: T) -> Self { - let mut this = Self::default(); - - this.extend(iter); - - this - } -} - -impl<'a> Extend<(&'a Command, BoxedHandler)> for Router { - #[inline] - fn extend>(&mut self, iter: T) { - for (command, boxed_handler) in iter { - let name = &command.name; - self.add_route_already_boxed(name, boxed_handler); - } - } -} +use std::{fmt::Debug, sync::Arc}; + +use futures::future::BoxFuture; +use patricia_tree::StringPatriciaMap; +use songbird::Songbird; +use twilight_model::{ + application::{command::Command, interaction::Interaction}, + id::{Id, marker::ApplicationMarker}, +}; + +use crate::VCs; + +mod join; +mod leave; +mod opt_out; + +#[derive(Debug, Clone)] +pub struct State { + pub discord_client: Arc, + pub discord_application_id: Id, + pub songbird: Arc, + pub vcs: Arc, +} + +type Return = (); +type BoxedHandler = Box BoxFuture<'static, Return>>; + +fn box_handler(handler: Handler) -> BoxedHandler +where + Fut: Future + Send + 'static, + Handler: Fn(State, Interaction) -> Fut + 'static, +{ + Box::new(move |state, interaction| Box::pin(handler(state, interaction))) +} + +pub fn all() -> Vec<(&'static Command, BoxedHandler)> { + vec![ + (&join::COMMAND, box_handler(join::handle)), + (&leave::COMMAND, box_handler(leave::handle)), + (&opt_out::COMMAND, box_handler(opt_out::handle)), + ] +} + +#[derive(Default)] +pub struct Router { + map: StringPatriciaMap, +} + +impl Router { + fn add_route<'s, 'a, Fut, Handler>(&'s mut self, name: &'a str, handler: Handler) + where + Fut: Future + Send + 'static, + Handler: Fn(State, Interaction) -> Fut + 'static, + { + self.add_route_already_boxed(name, box_handler(handler)); + } + + fn add_route_already_boxed<'s, 'a>(&'s mut self, name: &'a str, boxed_handler: BoxedHandler) { + self.map.insert(name, boxed_handler); + } + + pub async fn handle( + &self, + state: State, + command_name: &str, + interaction: Interaction, + ) -> Option { + let handler = self.map.get(command_name)?; + + Some(handler(state, interaction).await) + } +} + +impl<'a> FromIterator<(&'a Command, BoxedHandler)> for Router { + #[inline] + fn from_iter>(iter: T) -> Self { + let mut this = Self::default(); + + this.extend(iter); + + this + } +} + +impl<'a> Extend<(&'a Command, BoxedHandler)> for Router { + #[inline] + fn extend>(&mut self, iter: T) { + for (command, boxed_handler) in iter { + let name = &command.name; + self.add_route_already_boxed(name, boxed_handler); + } + } +} diff --git a/src/main.rs b/src/main.rs index 4eaef2e..ab3c88e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -132,7 +132,7 @@ async fn main() -> Result<(), MainError> { .await .expect("couldn't get current Discord application"); // TODO - let application_id = current_application.id; + let discord_application_id = current_application.id; let shard_id = ShardId::new(0, 1); let intents = Intents::GUILD_VOICE_STATES; @@ -150,7 +150,7 @@ async fn main() -> Result<(), MainError> { let event_types = EventTypeFlags::GUILD_VOICE_STATES | EventTypeFlags::INTERACTION_CREATE; let mut next_event = shard.next_event(event_types); - let interaction_client = discord_client.interaction(application_id); + let interaction_client = discord_client.interaction(discord_application_id); let commands = all_commands(); @@ -172,11 +172,17 @@ async fn main() -> Result<(), MainError> { let command_router = CommandRouter::from_iter(commands); let vcs = initialize_vcs(&discord_client).await; + + let discord_client = Arc::new(discord_client); + let songbird = Arc::new(songbird); let vcs = Arc::new(vcs); - let songbird = Arc::new(songbird); - - let state = State { vcs, songbird }; + let state = State { + discord_application_id, + discord_client, + songbird, + vcs, + }; while let Some(event_res) = next_event.await { match event_res {