mod: mute audience feature
This commit is contained in:
+28
-10
@@ -4,6 +4,7 @@ use serenity::all::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::controls::{self, ControlAction};
|
use super::controls::{self, ControlAction};
|
||||||
|
use super::voice;
|
||||||
use crate::app::buzzer::{
|
use crate::app::buzzer::{
|
||||||
BUZZER_CUSTOM_ID, BuzzerSessionKey, LOCK_CUSTOM_ID, RESET_CUSTOM_ID, UNLOCK_CUSTOM_ID,
|
BUZZER_CUSTOM_ID, BuzzerSessionKey, LOCK_CUSTOM_ID, RESET_CUSTOM_ID, UNLOCK_CUSTOM_ID,
|
||||||
buzzer_sound, session_components,
|
buzzer_sound, session_components,
|
||||||
@@ -87,18 +88,35 @@ async fn press(ctx: &Context, interaction: &ComponentInteraction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let guild_id = active_session.guild_id;
|
let guild_id = active_session.guild_id;
|
||||||
drop(session);
|
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 Some(manager) = songbird::get(ctx).await else {
|
let exempt_user_ids = [active_session.host_id, interaction.user.id];
|
||||||
tracing::error!("voice manager unavailable while playing buzzer");
|
let voice_result = voice::mute_channel_except(ctx, active_session, &exempt_user_ids).await;
|
||||||
return;
|
if voice_result.failures > 0 {
|
||||||
};
|
tracing::warn!(
|
||||||
let Some(call) = manager.get(guild_id) else {
|
failures = voice_result.failures,
|
||||||
tracing::error!(%guild_id, "bot is not connected to voice for active buzzer");
|
%guild_id,
|
||||||
return;
|
"buzzer locked with participant mute failures"
|
||||||
};
|
);
|
||||||
|
|
||||||
call.lock().await.play_only_input(buzzer_sound());
|
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) {
|
async fn run_control(ctx: &Context, interaction: &ComponentInteraction, action: ControlAction) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use serenity::all::{Context, EditMessage, GuildId, MessageId, UserId};
|
use serenity::all::{Context, EditMessage, GuildId, MessageId, UserId};
|
||||||
|
|
||||||
|
use super::voice;
|
||||||
use crate::app::buzzer::{BuzzerSessionKey, session_components};
|
use crate::app::buzzer::{BuzzerSessionKey, session_components};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
@@ -48,10 +49,6 @@ pub async fn apply(
|
|||||||
"The buzzer has been reset.",
|
"The buzzer has been reset.",
|
||||||
),
|
),
|
||||||
ControlAction::Lock => {
|
ControlAction::Lock => {
|
||||||
if !active_session.is_open {
|
|
||||||
return "The buzzer is already locked.".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = match active_session.buzzed_user {
|
let content = match active_session.buzzed_user {
|
||||||
Some(user_id) => {
|
Some(user_id) => {
|
||||||
format!("<@{user_id}> buzzed first! The buzzer is manually locked.")
|
format!("<@{user_id}> buzzed first! The buzzer is manually locked.")
|
||||||
@@ -61,10 +58,6 @@ pub async fn apply(
|
|||||||
(content, false, false, "The buzzer has been locked.")
|
(content, false, false, "The buzzer has been locked.")
|
||||||
}
|
}
|
||||||
ControlAction::Unlock => {
|
ControlAction::Unlock => {
|
||||||
if active_session.is_open {
|
|
||||||
return "The buzzer is already unlocked.".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = match active_session.buzzed_user {
|
let content = match active_session.buzzed_user {
|
||||||
Some(user_id) => {
|
Some(user_id) => {
|
||||||
format!("<@{user_id}> buzzed first! The buzzer is manually unlocked.")
|
format!("<@{user_id}> buzzed first! The buzzer is manually unlocked.")
|
||||||
@@ -93,7 +86,28 @@ pub async fn apply(
|
|||||||
active_session.buzzed_user = None;
|
active_session.buzzed_user = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
success_message.to_string()
|
let voice_result = match action {
|
||||||
|
ControlAction::Lock => {
|
||||||
|
let mut exempt_user_ids = vec![active_session.host_id];
|
||||||
|
if let Some(buzzed_user_id) = active_session.buzzed_user {
|
||||||
|
exempt_user_ids.push(buzzed_user_id);
|
||||||
|
}
|
||||||
|
voice::mute_channel_except(ctx, active_session, &exempt_user_ids).await
|
||||||
|
}
|
||||||
|
ControlAction::Reset | ControlAction::Unlock => {
|
||||||
|
voice::unmute_session_members(ctx, active_session).await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if voice_result.failures == 0 {
|
||||||
|
success_message.to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{success_message} I couldn't {} {} participant(s); check my Mute Members permission and role position.",
|
||||||
|
action.failed_voice_verb(),
|
||||||
|
voice_result.failures,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControlAction {
|
impl ControlAction {
|
||||||
@@ -104,4 +118,11 @@ impl ControlAction {
|
|||||||
Self::Unlock => "unlock",
|
Self::Unlock => "unlock",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn failed_voice_verb(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Lock => "mute",
|
||||||
|
Self::Reset | Self::Unlock => "unmute",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ pub mod reset;
|
|||||||
pub mod start;
|
pub mod start;
|
||||||
pub mod stop;
|
pub mod stop;
|
||||||
pub mod unlock;
|
pub mod unlock;
|
||||||
|
mod voice;
|
||||||
|
|||||||
+10
-1
@@ -1,5 +1,6 @@
|
|||||||
use serenity::all::{CommandInteraction, Context, CreateCommand, EditMessage};
|
use serenity::all::{CommandInteraction, Context, CreateCommand, EditMessage};
|
||||||
|
|
||||||
|
use super::voice;
|
||||||
use crate::app::buzzer::{BuzzerSessionKey, stopped_session_components};
|
use crate::app::buzzer::{BuzzerSessionKey, stopped_session_components};
|
||||||
|
|
||||||
pub async fn run(ctx: &Context, command: &CommandInteraction) -> String {
|
pub async fn run(ctx: &Context, command: &CommandInteraction) -> String {
|
||||||
@@ -17,7 +18,7 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> String {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut session = session.lock().await;
|
let mut session = session.lock().await;
|
||||||
let Some(active_session) = session.as_ref() else {
|
let Some(active_session) = session.as_mut() else {
|
||||||
return "There is no active buzzer session.".to_string();
|
return "There is no active buzzer session.".to_string();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +32,14 @@ pub async fn run(ctx: &Context, command: &CommandInteraction) -> String {
|
|||||||
let text_channel_id = active_session.text_channel_id;
|
let text_channel_id = active_session.text_channel_id;
|
||||||
let button_message_id = active_session.button_message_id;
|
let button_message_id = active_session.button_message_id;
|
||||||
|
|
||||||
|
let voice_result = voice::unmute_session_members(ctx, active_session).await;
|
||||||
|
if voice_result.failures > 0 {
|
||||||
|
return format!(
|
||||||
|
"I couldn't unmute {} participant(s), so the session was not stopped. Check my Mute Members permission and role position, then try again.",
|
||||||
|
voice_result.failures,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let Some(manager) = songbird::get(ctx).await else {
|
let Some(manager) = songbird::get(ctx).await else {
|
||||||
return "The voice manager is unavailable.".to_string();
|
return "The voice manager is unavailable.".to_string();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
use serenity::all::{Context, EditMember, UserId};
|
||||||
|
|
||||||
|
use crate::app::buzzer::BuzzerSession;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct VoiceUpdateResult {
|
||||||
|
pub failures: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Server-mutes every human participant in the session voice channel except
|
||||||
|
/// the supplied users. Pre-existing server mutes are left untouched so the bot
|
||||||
|
/// does not later undo a moderator's mute.
|
||||||
|
pub async fn mute_channel_except(
|
||||||
|
ctx: &Context,
|
||||||
|
session: &mut BuzzerSession,
|
||||||
|
exempt_user_ids: &[UserId],
|
||||||
|
) -> VoiceUpdateResult {
|
||||||
|
let bot_user_id = ctx.cache.current_user().id;
|
||||||
|
let participants = ctx.cache.guild(session.guild_id).map(|guild| {
|
||||||
|
guild
|
||||||
|
.voice_states
|
||||||
|
.values()
|
||||||
|
.filter(|state| state.channel_id == Some(session.voice_channel_id))
|
||||||
|
.map(|state| {
|
||||||
|
let is_bot = state.user_id == bot_user_id
|
||||||
|
|| state.member.as_ref().is_some_and(|member| member.user.bot);
|
||||||
|
(state.user_id, state.mute, is_bot)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some(participants) = participants else {
|
||||||
|
tracing::error!(
|
||||||
|
guild_id = %session.guild_id,
|
||||||
|
"guild unavailable while updating buzzer voice mutes"
|
||||||
|
);
|
||||||
|
return VoiceUpdateResult { failures: 1 };
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result = VoiceUpdateResult::default();
|
||||||
|
for (user_id, already_muted, is_bot) in participants {
|
||||||
|
if is_bot {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if exempt_user_ids.contains(&user_id) {
|
||||||
|
if session.muted_user_ids.contains(&user_id)
|
||||||
|
&& set_server_mute(ctx, session, user_id, false).await
|
||||||
|
{
|
||||||
|
session.muted_user_ids.remove(&user_id);
|
||||||
|
} else if session.muted_user_ids.contains(&user_id) {
|
||||||
|
result.failures += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A mute not recorded in the session belongs to a moderator or some
|
||||||
|
// other system and must not be claimed or later reversed by this bot.
|
||||||
|
if already_muted || session.muted_user_ids.contains(&user_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if set_server_mute(ctx, session, user_id, true).await {
|
||||||
|
session.muted_user_ids.insert(user_id);
|
||||||
|
} else {
|
||||||
|
result.failures += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unmutes only members that this buzzer session successfully muted.
|
||||||
|
pub async fn unmute_session_members(
|
||||||
|
ctx: &Context,
|
||||||
|
session: &mut BuzzerSession,
|
||||||
|
) -> VoiceUpdateResult {
|
||||||
|
let muted_user_ids = session.muted_user_ids.iter().copied().collect::<Vec<_>>();
|
||||||
|
let mut result = VoiceUpdateResult::default();
|
||||||
|
|
||||||
|
for user_id in muted_user_ids {
|
||||||
|
if set_server_mute(ctx, session, user_id, false).await {
|
||||||
|
session.muted_user_ids.remove(&user_id);
|
||||||
|
} else {
|
||||||
|
result.failures += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_server_mute(
|
||||||
|
ctx: &Context,
|
||||||
|
session: &BuzzerSession,
|
||||||
|
user_id: UserId,
|
||||||
|
muted: bool,
|
||||||
|
) -> bool {
|
||||||
|
let builder = EditMember::new()
|
||||||
|
.mute(muted)
|
||||||
|
.audit_log_reason("Hildebrand buzzer session");
|
||||||
|
|
||||||
|
match session
|
||||||
|
.guild_id
|
||||||
|
.edit_member(&ctx.http, user_id, builder)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => true,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::error!(
|
||||||
|
?error,
|
||||||
|
guild_id = %session.guild_id,
|
||||||
|
%user_id,
|
||||||
|
muted,
|
||||||
|
"failed to update participant server mute"
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::f32::consts::TAU;
|
use std::f32::consts::TAU;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -24,6 +25,9 @@ pub struct BuzzerSession {
|
|||||||
|
|
||||||
pub buzzed_user: Option<UserId>,
|
pub buzzed_user: Option<UserId>,
|
||||||
pub is_open: bool,
|
pub is_open: bool,
|
||||||
|
/// Members server-muted by this session. Only these members are unmuted
|
||||||
|
/// when the buzzer is reset, unlocked, or stopped.
|
||||||
|
pub muted_user_ids: HashSet<UserId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuzzerSession {
|
impl BuzzerSession {
|
||||||
@@ -42,6 +46,7 @@ impl BuzzerSession {
|
|||||||
button_message_id,
|
button_message_id,
|
||||||
buzzed_user: None,
|
buzzed_user: None,
|
||||||
is_open: true,
|
is_open: true,
|
||||||
|
muted_user_ids: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user