2026-07-31 13:02:37 -04:00
|
|
|
use serenity::all::{
|
|
|
|
|
ComponentInteraction, Context, CreateInteractionResponse, CreateInteractionResponseMessage,
|
|
|
|
|
EditInteractionResponse,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use super::controls::{self, ControlAction};
|
2026-08-02 13:45:17 -04:00
|
|
|
use super::voice;
|
2026-07-31 13:02:37 -04:00
|
|
|
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::<BuzzerSessionKey>().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;
|
2026-08-02 13:45:17 -04:00
|
|
|
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"),
|
|
|
|
|
}
|
2026-07-31 13:02:37 -04:00
|
|
|
|
2026-08-02 13:45:17 -04:00
|
|
|
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"
|
|
|
|
|
);
|
2026-07-31 13:02:37 -04:00
|
|
|
|
2026-08-02 13:45:17 -04:00
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-31 13:02:37 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
}
|