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, } #[tracing::instrument] pub fn get_user_and_guild_and_voice_channel_id( bot_user_id: Id, interaction: &Interaction, vcs: &VCs, ) -> Result<(Id, Id, Id), GetGuildAndVoiceChannelIdError> { let user_id = interaction .member .as_ref() .and_then(|member| member.user.as_ref().map(|user| user.id)) .context(NoUserSnafu)?; let guild_id = interaction.guild_id.context(NotInGuildSnafu)?; let &voice_channel_id = vcs .get(&guild_id) .context(BotNotInVCSnafu)? .get_left_for(&bot_user_id) .context(BotNotInVCSnafu)?; Ok((user_id, 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 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() }, } } #[tracing::instrument] pub async fn handle(state: State, interaction: Interaction) { let user_and_guild_and_voice_channel_id_res = get_user_and_guild_and_voice_channel_id( state.discord_user_id, &interaction, &state.vcs_watcher.borrow(), ); let (user_id, guild_id, voice_channel_id) = match user_and_guild_and_voice_channel_id_res { Ok((user_id, guild_id, voice_channel_id)) => (user_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; } }; if user_id != state.discord_bot_owner_user_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("No permission to make this bot leave").description("Only the owner of this bot is allowed to make the bot leave RIGHT NOW. You might be looking for the opt out command.").validate().unwrap().build() ]) .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"); }