use serenity::all::{ ComponentInteraction, Context, CreateInteractionResponse, CreateInteractionResponseMessage, EditInteractionResponse, }; use super::controls::{self, ControlAction}; use super::voice; use crate::app::buzzer::{ BUZZER_CUSTOM_ID, BuzzerSessionKey, LOCK_CUSTOM_ID, RESET_CUSTOM_ID, UNLOCK_CUSTOM_ID, buzzer_sound, session_components, }; pub async fn run(ctx: &Context, interaction: &ComponentInteraction) { match interaction.data.custom_id.as_str() { BUZZER_CUSTOM_ID => press(ctx, interaction).await, RESET_CUSTOM_ID => run_control(ctx, interaction, ControlAction::Reset).await, LOCK_CUSTOM_ID => run_control(ctx, interaction, ControlAction::Lock).await, UNLOCK_CUSTOM_ID => run_control(ctx, interaction, ControlAction::Unlock).await, _ => {} } } async fn press(ctx: &Context, interaction: &ComponentInteraction) { let session = { let data = ctx.data.read().await; data.get::().cloned() }; let Some(session) = session else { respond_ephemeral(ctx, interaction, "The buzzer session is unavailable.").await; return; }; let mut session = session.lock().await; let Some(active_session) = session.as_mut() else { respond_ephemeral(ctx, interaction, "There is no active buzzer session.").await; return; }; let belongs_to_session = interaction.guild_id == Some(active_session.guild_id) && interaction.message.id == active_session.button_message_id; if !belongs_to_session { respond_ephemeral(ctx, interaction, "That buzzer is no longer active.").await; return; } let user_voice_channel = ctx.cache.guild(active_session.guild_id).and_then(|guild| { guild .voice_states .get(&interaction.user.id) .and_then(|state| state.channel_id) }); if user_voice_channel != Some(active_session.voice_channel_id) { respond_ephemeral( ctx, interaction, "Join the buzzer session's voice channel before buzzing.", ) .await; return; } if !active_session.is_open { let content = match active_session.buzzed_user { Some(user_id) => format!("The buzzer is locked; <@{user_id}> buzzed first."), None => "The buzzer is locked.".to_string(), }; respond_ephemeral(ctx, interaction, &content).await; return; } active_session.is_open = false; active_session.buzzed_user = Some(interaction.user.id); let message = CreateInteractionResponseMessage::new() .content(format!("<@{}> buzzed first!", interaction.user.id)) .components(session_components(false)); if let Err(error) = interaction .create_response(&ctx.http, CreateInteractionResponse::UpdateMessage(message)) .await { // Keep the state open if Discord could not disable the actual button. active_session.is_open = true; active_session.buzzed_user = None; tracing::error!(?error, "failed to lock buzzer message"); return; } let guild_id = active_session.guild_id; match songbird::get(ctx).await { Some(manager) => match manager.get(guild_id) { Some(call) => { call.lock().await.play_only_input(buzzer_sound()); } None => { tracing::error!(%guild_id, "bot is not connected to voice for active buzzer"); } }, None => tracing::error!("voice manager unavailable while playing buzzer"), } let exempt_user_ids = [active_session.host_id, interaction.user.id]; let voice_result = voice::mute_channel_except(ctx, active_session, &exempt_user_ids).await; if voice_result.failures > 0 { tracing::warn!( failures = voice_result.failures, %guild_id, "buzzer locked with participant mute failures" ); let warning = EditInteractionResponse::new().content(format!( "<@{}> buzzed first! I couldn't mute {} participant(s); check my Mute Members permission and role position.", interaction.user.id, voice_result.failures, )); if let Err(error) = interaction.edit_response(&ctx.http, warning).await { tracing::error!(?error, "failed to report participant mute failures"); } } } async fn run_control(ctx: &Context, interaction: &ComponentInteraction, action: ControlAction) { let deferred = CreateInteractionResponse::Defer(CreateInteractionResponseMessage::new().ephemeral(true)); if let Err(error) = interaction.create_response(&ctx.http, deferred).await { tracing::error!( ?error, ?action, "failed to defer buzzer control interaction" ); return; } let content = match interaction.guild_id { Some(guild_id) => { controls::apply( ctx, guild_id, interaction.user.id, Some(interaction.message.id), action, ) .await } None => "Buzzer controls can only be used in a server.".to_string(), }; if let Err(error) = interaction .edit_response(&ctx.http, EditInteractionResponse::new().content(content)) .await { tracing::error!( ?error, ?action, "failed to respond to buzzer control interaction" ); } } async fn respond_ephemeral(ctx: &Context, interaction: &ComponentInteraction, content: &str) { let message = CreateInteractionResponseMessage::new() .content(content) .ephemeral(true); if let Err(error) = interaction .create_response(&ctx.http, CreateInteractionResponse::Message(message)) .await { tracing::error!(?error, "failed to respond to buzzer interaction"); } }