use crate::{VCs, call::join_and_record, command::State}; use snafu::{OptionExt as _, Snafu}; use std::sync::LazyLock; 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, }; const NAME: &str = "join"; const DESCRIPTION: &str = "The bot will join the same VC as you (with intention to record)"; pub static COMMAND: LazyLock = LazyLock::new(|| { CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput) .validate() .expect("command wasn't correct") .build() }); #[derive(Debug, Snafu)] enum GetGuildAndVoiceChannelIdError { /// this command was not used inside a guild (Discord server) NotInGuild, /// there is no user who invoked this command NoUser, /// the user is not in a voice chat in this guild UserNotInVC, } #[tracing::instrument] fn get_guild_and_voice_channel_id( interaction: &Interaction, vcs: &VCs, ) -> Result<(Id, Id), GetGuildAndVoiceChannelIdError> { 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 &voice_channel_id = vcs .get(&guild_id) .context(UserNotInVCSnafu)? .get_left_for(&user_id) .context(UserNotInVCSnafu)?; Ok((guild_id, voice_channel_id)) } fn get_guild_and_vc_error_to_embed(error: GetGuildAndVoiceChannelIdError) -> Embed { match error { GetGuildAndVoiceChannelIdError::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() }, GetGuildAndVoiceChannelIdError::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() }, GetGuildAndVoiceChannelIdError::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(skip(state))] pub async fn handle(state: State, interaction: Interaction) { let guild_and_voice_channel_id_res = { get_guild_and_voice_channel_id(&interaction, &state.vcs_sender.borrow()) }; let (guild_id, voice_channel_id) = match guild_and_voice_channel_id_res { Ok((guild_id, voice_channel_id)) => (guild_id, voice_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([get_guild_and_vc_error_to_embed(error)]) .flags(MessageFlags::EPHEMERAL) .build(), ), }, ) .await .expect("TODO"); return; } }; state .discord_client .interaction(state.discord_application_id) .create_response( interaction.id, &interaction.token, &InteractionResponse { kind: InteractionResponseType::DeferredChannelMessageWithSource, data: None, }, ) .await .expect("TODO"); match join_and_record() .audio_channels(state.audio_channels) .audio_sample_rate(state.audio_sample_rate) .guild_id(guild_id) .recording_data(state.recording_data) .songbird(&state.songbird) .user_data_manager(state.user_data_manager) .voice_channel_id(voice_channel_id) .call() .await { Ok(()) => { let channel_mention = format!("<#{voice_channel_id}>"); let info_mention = format!( "", state.discord_info_command_name, state.discord_info_command_id ); let opt_in_mention = format!( "", state.discord_opt_in_command_name, state.discord_opt_in_command_id ); let opt_out_mention = format!( "", state.discord_opt_out_command_name, state.discord_opt_out_command_id ); state .discord_client .interaction(state.discord_application_id) .update_response( &interaction.token, ).embeds(Some(&[ EmbedBuilder::new() .title("Joined VC to record") .description(format!("This bot joined {channel_mention} and intends to record. You can opt out with {opt_out_mention} or explicitly opt in with {opt_in_mention} (I'd appreciate this one). Please use {info_mention} for more information about this bot.")) .validate() .unwrap() .build() ])) .await .expect("TODO"); } Err(join_error) => { tracing::error!(?join_error); let _ = state.songbird.remove(guild_id).await; } } }