use crate::VCs; use crate::command::State; use snafu::{OptionExt, Snafu}; use std::sync::LazyLock; use twilight_model::channel::message::{Embed, MessageFlags}; use twilight_model::http::interaction::{InteractionResponse, InteractionResponseType}; use twilight_model::id::marker::UserMarker; use twilight_model::{ application::{ command::{Command, CommandType}, interaction::Interaction, }, id::{ Id, marker::{ChannelMarker, GuildMarker}, }, }; use twilight_util::builder::InteractionResponseDataBuilder; use twilight_util::builder::command::CommandBuilder; use twilight_util::builder::embed::EmbedBuilder; const NAME: &str = "leave"; const DESCRIPTION: &str = "The bot will leave the VC it's in (so it won't record anyone anymore)"; pub static COMMAND: LazyLock = LazyLock::new(|| { CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput) .validate() .expect("command wasn't correct") .build() }); #[derive(Debug, Snafu)] pub enum GetGuildAndVoiceChannelIdError { /// this command was not used inside a guild (Discord server) NotInGuild, /// there is no user who invoked this command NoUser, /// the bot is not in a voice chat in this guild BotNotInVC, /// the user is not in a voice chat with the bot in this guild UserNotInVCWithBot, } #[tracing::instrument] pub fn get_user_and_guild_and_voice_channel_id( bot_user_id: 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 &bot_voice_channel_id = vcs .get(&guild_id) .context(BotNotInVCSnafu)? .get_left_for(&bot_user_id) .context(BotNotInVCSnafu)?; let &user_voice_channel_id = vcs .get(&guild_id) .context(UserNotInVCWithBotSnafu)? .get_left_for(&user_id) .context(UserNotInVCWithBotSnafu)?; if user_voice_channel_id != bot_voice_channel_id { return Err(GetGuildAndVoiceChannelIdError::UserNotInVCWithBot); } Ok((guild_id, bot_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 tell which VC to leave 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::BotNotInVC => { EmbedBuilder::new().title("Not in a VC").description("This bot can't leave VC if it isn't in one in this server.").validate().unwrap().build() }, GetGuildAndVoiceChannelIdError::UserNotInVCWithBot => { EmbedBuilder::new().title("Not in a VC with the Bot").description("You have to be in the VC with the bot to make it leave (to prevent griefing and abuse).").validate().unwrap().build() }, } } #[tracing::instrument] pub async fn handle(state: State, interaction: Interaction) { let guild_and_voice_channel_id_result = { get_user_and_guild_and_voice_channel_id( state.discord_user_id, &interaction, &state.vcs_sender.borrow(), ) }; let (guild_id, voice_channel_id) = match guild_and_voice_channel_id_result { 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.songbird.leave(guild_id).await.expect("TODO"); tracing::error!("TODO: successfully left the call"); let channel_mention = format!("<#{voice_channel_id}>"); state .discord_client .interaction(state.discord_application_id) .create_response(interaction.id, &interaction.token, &InteractionResponse { kind: InteractionResponseType::ChannelMessageWithSource, data: Some( InteractionResponseDataBuilder::new() .embeds([ EmbedBuilder::new() .title("Left VC") .description(format!( "This bot left {channel_mention} (and is thereby unable to record anymore)." )) .validate() .unwrap() .build() ]) .flags(MessageFlags::EPHEMERAL) .build(), ), },) .await .expect("TODO"); }