Compare commits

..

2 Commits

Author SHA1 Message Date
62399c2046 feat: allow setting a corresponding text channel for a voice channel 2026-04-17 01:19:09 -04:00
612a696829 feat: mute 2026-04-16 23:13:45 -04:00
5 changed files with 85 additions and 11 deletions

View File

@@ -288,6 +288,8 @@ pub async fn handle(state: State, interaction: Interaction) {
call.add_global_event(CoreEvent::SpeakingStateUpdate.into(), handler.clone()); call.add_global_event(CoreEvent::SpeakingStateUpdate.into(), handler.clone());
call.add_global_event(CoreEvent::VoiceTick.into(), handler); call.add_global_event(CoreEvent::VoiceTick.into(), handler);
call.mute(true).await.expect("TODO");
} }
let channel_mention = format!("<#{voice_channel_id}>"); let channel_mention = format!("<#{voice_channel_id}>");

View File

@@ -13,7 +13,7 @@ use twilight_model::{
}, },
}; };
use crate::VCs; use crate::{VCs, track_vcs::GuildVoiceChannelToTextChannel};
mod debug; mod debug;
mod join; mod join;
@@ -31,6 +31,7 @@ pub struct State {
pub discord_bot_owner_user_id: Id<UserMarker>, pub discord_bot_owner_user_id: Id<UserMarker>,
pub discord_client: Arc<twilight_http::Client>, pub discord_client: Arc<twilight_http::Client>,
pub discord_user_id: Id<UserMarker>, pub discord_user_id: Id<UserMarker>,
pub discord_voice_channel_corresponding_text_channel: Arc<GuildVoiceChannelToTextChannel>,
pub recording_data: Operator, pub recording_data: Operator,
pub songbird: Arc<Songbird>, pub songbird: Arc<Songbird>,
pub user_data: Operator, pub user_data: Operator,

View File

@@ -11,7 +11,7 @@ pub use one_to_many::OneToManyUniqueBTreeMap;
pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData; pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData;
pub use one_to_one::OneToOneBTreeMap; pub use one_to_one::OneToOneBTreeMap;
pub use storage::Storage; pub use storage::Storage;
pub use track_vcs::{VCs, initialize_vcs, update_vcs}; pub use track_vcs::{GuildVoiceChannelToTextChannel, VCs, initialize_vcs, update_vcs};
pub use vc_user::{UserInVCData, VoiceStatus}; pub use vc_user::{UserInVCData, VoiceStatus};
capnp::generated_code!(pub mod user_capnp); capnp::generated_code!(pub mod user_capnp);

View File

@@ -1,13 +1,16 @@
use clap::Parser; use clap::Parser;
use fomo_reducer::{CommandRouter, State, Storage, all_commands, initialize_vcs, update_vcs}; use fomo_reducer::{
CommandRouter, GuildVoiceChannelToTextChannel, State, Storage, all_commands, initialize_vcs,
update_vcs,
};
use secrecy::{ExposeSecret, SecretString}; use secrecy::{ExposeSecret, SecretString};
use snafu::Snafu; use snafu::{OptionExt, ResultExt, Snafu};
use songbird::{ use songbird::{
Config, Songbird, Config, Songbird,
driver::{Channels, DecodeConfig, SampleRate}, driver::{Channels, DecodeConfig, SampleRate},
shards::TwilightMap, shards::TwilightMap,
}; };
use std::{fmt::Debug, sync::Arc}; use std::{fmt::Debug, str::FromStr, sync::Arc};
use strum::EnumString; use strum::EnumString;
use tokio::{select, signal::ctrl_c, task::JoinSet}; use tokio::{select, signal::ctrl_c, task::JoinSet};
use tokio_util::{sync::CancellationToken, time::FutureExt as _}; use tokio_util::{sync::CancellationToken, time::FutureExt as _};
@@ -19,7 +22,10 @@ use twilight_model::{
payload::{incoming::InteractionCreate, outgoing::UpdatePresence}, payload::{incoming::InteractionCreate, outgoing::UpdatePresence},
presence::{ActivityType, MinimalActivity, Status}, presence::{ActivityType, MinimalActivity, Status},
}, },
id::{Id, marker::UserMarker}, id::{
Id,
marker::{ChannelMarker, GuildMarker, UserMarker},
},
}; };
#[derive(Clone, Copy, Debug, strum::Display, EnumString)] #[derive(Clone, Copy, Debug, strum::Display, EnumString)]
@@ -63,6 +69,41 @@ impl From<AudioSampleRate> for SampleRate {
} }
} }
#[derive(Debug, Snafu)]
enum ParseGuildVCToTextChannelError {
NoScope,
NoRelation,
ParseGuildError {
source: <Id<GuildMarker> as FromStr>::Err,
},
ParseVoiceChannelError {
source: <Id<ChannelMarker> as FromStr>::Err,
},
ParseTextChannelError {
source: <Id<ChannelMarker> as FromStr>::Err,
},
}
fn parse_guild_vc_to_text_channel(
source: &str,
) -> Result<(Id<GuildMarker>, Id<ChannelMarker>, Id<ChannelMarker>), ParseGuildVCToTextChannelError>
{
let (guild, voice_channel_and_text_channel) = source.split_once(':').context(NoScopeSnafu)?;
let (voice_channel, text_channel) = voice_channel_and_text_channel
.split_once("->")
.context(NoRelationSnafu)?;
let guild = guild.parse().context(ParseGuildSnafu)?;
let voice_channel = voice_channel.parse().context(ParseVoiceChannelSnafu)?;
let text_channel = text_channel.parse().context(ParseTextChannelSnafu)?;
Ok((guild, voice_channel, text_channel))
}
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct AppArgs { struct AppArgs {
#[arg(long, env)] #[arg(long, env)]
@@ -77,6 +118,10 @@ struct AppArgs {
#[arg(long, env)] #[arg(long, env)]
discord_status: Option<Arc<str>>, discord_status: Option<Arc<str>>,
#[arg(long, env, value_parser = parse_guild_vc_to_text_channel)]
discord_voice_channel_corresponding_text_channel:
Vec<(Id<GuildMarker>, Id<ChannelMarker>, Id<ChannelMarker>)>,
#[arg(long, env, default_value_t = AudioChannels::Mono)] #[arg(long, env, default_value_t = AudioChannels::Mono)]
audio_channels: AudioChannels, audio_channels: AudioChannels,
@@ -141,6 +186,7 @@ async fn main() -> Result<(), MainError> {
discord_bot_owner_user_id, discord_bot_owner_user_id,
discord_nickname, discord_nickname,
discord_status, discord_status,
discord_voice_channel_corresponding_text_channel,
audio_channels, audio_channels,
audio_sample_rate, audio_sample_rate,
bot_data, bot_data,
@@ -244,11 +290,11 @@ async fn main() -> Result<(), MainError> {
.await .await
.expect("failed to deserialize set commands"); // TODO .expect("failed to deserialize set commands"); // TODO
let vcs = initialize_vcs(&discord_client).await;
let command_router = CommandRouter::from_iter(commands); let command_router = CommandRouter::from_iter(commands);
let command_router = Arc::new(command_router); let command_router = Arc::new(command_router);
let vcs = initialize_vcs(&discord_client).await;
let discord_client = Arc::new(discord_client); let discord_client = Arc::new(discord_client);
let songbird = Arc::new(songbird); let songbird = Arc::new(songbird);
let vcs = Arc::new(vcs); let vcs = Arc::new(vcs);
@@ -257,6 +303,22 @@ async fn main() -> Result<(), MainError> {
let recording_data = recording_data.into_inner(); let recording_data = recording_data.into_inner();
let user_data = user_data.into_inner(); let user_data = user_data.into_inner();
let discord_voice_channel_corresponding_text_channel = {
let mut map = GuildVoiceChannelToTextChannel::default();
for (guild_id, voice_channel_id, text_channel_id) in
discord_voice_channel_corresponding_text_channel
{
map.entry(guild_id)
.or_default()
.insert(voice_channel_id, text_channel_id);
}
map
};
let discord_voice_channel_corresponding_text_channel =
Arc::new(discord_voice_channel_corresponding_text_channel);
let state = State { let state = State {
audio_channels, audio_channels,
audio_sample_rate, audio_sample_rate,
@@ -266,6 +328,7 @@ async fn main() -> Result<(), MainError> {
discord_bot_owner_user_id, discord_bot_owner_user_id,
discord_client, discord_client,
discord_user_id, discord_user_id,
discord_voice_channel_corresponding_text_channel,
recording_data, recording_data,
songbird, songbird,
user_data, user_data,
@@ -344,7 +407,12 @@ async fn handle_events(command_router: Arc<CommandRouter>, state: State, mut sha
Ok(event) => { Ok(event) => {
handle_event(command_router.clone(), state.clone(), event).await; handle_event(command_router.clone(), state.clone(), event).await;
} }
Err(reconnect_error) if matches!(reconnect_error.kind(), &twilight_gateway::error::ReceiveMessageErrorType::Reconnect) => { Err(reconnect_error)
if matches!(
reconnect_error.kind(),
&twilight_gateway::error::ReceiveMessageErrorType::Reconnect
) =>
{
tracing::error!(?reconnect_error); tracing::error!(?reconnect_error);
return; return;
} }

View File

@@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use dashmap::DashMap; use dashmap::DashMap;
use futures::{StreamExt, stream::FuturesUnordered}; use futures::{StreamExt, stream::FuturesUnordered};
use twilight_model::{ use twilight_model::{
@@ -8,10 +10,11 @@ use twilight_model::{
}, },
}; };
use crate::{OneToManyUniqueBTreeMapWithData, UserInVCData, VoiceStatus}; use crate::{OneToManyUniqueBTreeMapWithData, OneToOneBTreeMap, UserInVCData, VoiceStatus};
pub type GuildVoiceChannelToTextChannel = BTreeMap<Id<GuildMarker>, OneToOneBTreeMap<Id<ChannelMarker>, Id<ChannelMarker>>>;
type VCsInGuild = OneToManyUniqueBTreeMapWithData<Id<ChannelMarker>, Id<UserMarker>, UserInVCData>; type VCsInGuild = OneToManyUniqueBTreeMapWithData<Id<ChannelMarker>, Id<UserMarker>, UserInVCData>;
pub type VCs = DashMap<Id<GuildMarker>, VCsInGuild>; pub type VCs = DashMap<Id<GuildMarker>, VCsInGuild>;
#[tracing::instrument(skip(discord_client), ret)] #[tracing::instrument(skip(discord_client), ret)]