use std::sync::LazyLock; use async_compression::futures::bufread::BrotliDecoder; use capnp::message::ReaderOptions; use futures::{AsyncReadExt, TryStreamExt}; use opendal::ErrorKind; use snafu::{OptionExt, Snafu}; use twilight_model::{ application::{ command::{Command, CommandType}, interaction::Interaction, }, channel::message::{Embed, MessageFlags}, http::interaction::{InteractionResponse, InteractionResponseType}, id::{Id, marker::UserMarker}, }; use twilight_util::builder::{ InteractionResponseDataBuilder, command::CommandBuilder, embed::{EmbedAuthorBuilder, EmbedBuilder, EmbedFieldBuilder}, }; use crate::{bot_capnp, command::State}; const NAME: &str = "debug"; const DESCRIPTION: &str = "(Only the bot owner can use this) Show various information for debugging purposes"; pub static COMMAND: LazyLock = LazyLock::new(|| { CommandBuilder::new(NAME, DESCRIPTION, CommandType::ChatInput) .validate() .expect("command wasn't correct") .build() }); #[derive(Debug, Snafu)] enum NoPermission { /// there isn't a user who invoked this command NoUser, /// the user isn't allowed to use this command because they're not the bot owner NotInvokedByBotOwner, } fn no_permission_to_embed(error: NoPermission) -> Embed { match error { NoPermission::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() }, NoPermission::NotInvokedByBotOwner => { EmbedBuilder::new().title("No permission to see debug info").description("Only the owner of this bot is allowed to see its information for debugging purposes.").validate().unwrap().build() }, } } fn check_permission( interaction: &Interaction, bot_owner_user_id: Id, ) -> Result<(), NoPermission> { let user_id = interaction .member .as_ref() .and_then(|member| member.user.as_ref().map(|user| user.id)) .context(NoUserSnafu)?; if user_id != bot_owner_user_id { return Err(NoPermission::NotInvokedByBotOwner); } Ok(()) } #[tracing::instrument] pub async fn handle(state: State, interaction: Interaction) { if let Err(no_permission) = check_permission(&interaction, 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([no_permission_to_embed(no_permission)]) .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::ChannelMessageWithSource, data: Some( InteractionResponseDataBuilder::new() .flags(MessageFlags::EPHEMERAL) .content("some debug info is coming your way!") .build(), ), }, ) .await .expect("TODO"); let heat_script_description = { let compressed_result = state .bot_data .reader("data.bin.brotli") .await .expect("TODO") .into_futures_async_read(..) .await; let mut buf = Vec::default(); let mut message = capnp::message::TypedBuilder::::new_default(); let fallback = message.init_root(); let message_reader; let mut bot_data = fallback.into_reader(); match compressed_result { Ok(compressed) => { let mut decompressed = BrotliDecoder::new(compressed); decompressed.read_to_end(&mut buf).await.expect("TODO"); message_reader = capnp::serialize_packed::read_message(&*buf, ReaderOptions::default()) .expect("TODO"); bot_data = message_reader .get_root::() .expect("TODO"); } Err(error) if error.kind() == ErrorKind::NotFound => { tracing::error!("TODO: proceeding with fallback"); } Err(error) => { tracing::error!(?error, "TODO"); return; } } let heat_script_option = bot_data .has_heat_script() .then(|| bot_data.get_heat_script().expect("TODO")); let heat_script_option = heat_script_option.map(|heat_script| heat_script.to_str().expect("TODO")); heat_script_option.map_or("none set yet".into(), |heat_script| { format!("```\n{heat_script}\n```") }) }; state .discord_client .interaction(state.discord_application_id) .create_followup(&interaction.token) .embeds(&[EmbedBuilder::new() .field(EmbedFieldBuilder::new("Heat Script", heat_script_description).build()) .validate() .unwrap() .build()]) .flags(MessageFlags::EPHEMERAL) .await .expect("TODO"); let mut user_id_stream = state.user_data_manager.list().await.expect("TODO"); while let Some(user_id) = user_id_stream.try_next().await.expect("TODO") { let (consent, notification_script) = state .user_data_manager .with(user_id, |user_data| { let consent = user_data.get_voice_recording_consent().unwrap(); let notification_script = user_data.has_notification_script().then_some( user_data .get_notification_script() .expect("TODO") .to_string() .expect("TODO"), ); (consent, notification_script) }) .await .expect("TODO"); let user_mention = format!("<@{user_id}>"); state .discord_client .interaction(state.discord_application_id) .create_followup(&interaction.token) .embeds(&[EmbedBuilder::new() .author(EmbedAuthorBuilder::new(user_mention)) .field(EmbedFieldBuilder::new("Consent", format!("{consent:?}")).build()) .field( EmbedFieldBuilder::new( "Notification Script", format!("{notification_script:?}"), ) .build(), ) .validate() .unwrap() .build()]) .flags(MessageFlags::EPHEMERAL) .await .expect("TODO"); } }