meta: initial commit

This commit is contained in:
2026-03-05 01:24:39 -05:00
commit e759b78d20
10 changed files with 7461 additions and 0 deletions

10
src/lib.rs Normal file
View File

@@ -0,0 +1,10 @@
mod one_to_many;
mod one_to_many_with_data;
mod one_to_one;
mod vc_user;
pub use one_to_many::OneToManyUniqueBTreeMap;
pub use one_to_many_with_data::OneToManyUniqueBTreeMapWithData;
pub use one_to_one::OneToOneBTreeMap;
pub use vc_user::{UserInVCData, VoiceStatus};

212
src/main.rs Normal file
View File

@@ -0,0 +1,212 @@
use std::{
collections::BTreeMap,
fmt::{Debug, Display},
str::FromStr,
};
use clap::Parser;
use opendal::{IntoOperatorUri, Operator, OperatorUri};
use secrecy::{ExposeSecret, SecretString};
use tracing_subscriber::fmt::format::FmtSpan;
use twilight_gateway::{
Event, EventTypeFlags, Intents, Shard, ShardId, StreamExt, error::ReceiveMessageErrorType,
};
use twilight_model::{
channel::ChannelType,
id::{
Id,
marker::{ChannelMarker, GuildMarker, UserMarker},
},
};
use typed_builder::TypedBuilder;
use vc_notifier::{OneToManyUniqueBTreeMapWithData, UserInVCData, VoiceStatus};
type VCsInServer = OneToManyUniqueBTreeMapWithData<Id<ChannelMarker>, Id<UserMarker>, UserInVCData>;
type VCs = BTreeMap<Id<GuildMarker>, VCsInServer>;
#[derive(Clone)]
struct OpendalOperator {
uri: OperatorUri,
operator: Operator,
}
impl FromStr for OpendalOperator {
type Err = opendal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let uri = s.into_operator_uri()?;
let operator = Operator::from_uri(&uri)?;
Ok(Self { uri, operator })
}
}
impl OpendalOperator {
fn into_inner(self) -> Operator {
self.operator
}
}
impl From<OpendalOperator> for Operator {
fn from(wrapper: OpendalOperator) -> Self {
wrapper.into_inner()
}
}
impl Debug for OpendalOperator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.uri, f)
}
}
#[derive(Debug, Parser)]
struct Args {
#[arg(long, env)]
discord_token: SecretString,
#[arg(long, env)]
storage: OpendalOperator,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.pretty()
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
.init();
let args = Parser::parse();
tracing::debug!(?args, "using");
let Args {
discord_token,
storage,
} = args;
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.unwrap();
let shard_id = ShardId::new(0, 1);
let intents = Intents::GUILD_VOICE_STATES;
let mut shard = Shard::new(shard_id, discord_token.expose_secret().to_owned(), intents);
let vc_event_types = EventTypeFlags::GUILD_VOICE_STATES;
let mut next_event = shard.next_event(vc_event_types);
let discord_client = twilight_http::Client::new(discord_token.expose_secret().to_owned());
let mut voice_status = initialize_vcs(&discord_client).await;
while let Some(event_res) = next_event.await {
match event_res {
Ok(event) => {
tracing::debug!(?voice_status, "before handling");
handle_event(event, &mut voice_status).await;
tracing::debug!(?voice_status, "after handling");
}
Err(error) => {
tracing::error!(?error);
}
}
next_event = shard.next_event(vc_event_types);
}
}
#[tracing::instrument(skip(discord_client), ret)]
async fn initialize_vcs(discord_client: &twilight_http::Client) -> VCs {
let mut vcs = VCs::default();
if let Ok(guilds_res) = discord_client.current_user_guilds().limit(200).await
&& let Ok(guilds) = guilds_res.model().await
{
for guild in guilds {
if let Ok(guild_members_res) = discord_client.guild_members(guild.id).limit(999).await
&& let Ok(guild_members) = guild_members_res.model().await
{
for member in guild_members {
if let Ok(voice_state_res) = discord_client
.user_voice_state(guild.id, member.user.id)
.await
&& let Ok(voice_state) = voice_state_res.model().await
{
tracing::info!(?member.user.id, ?voice_state);
let voice_status = VoiceStatus::builder()
.self_deafened(voice_state.self_deaf)
.self_muted(voice_state.self_mute)
.server_deafened(voice_state.deaf)
.server_muted(voice_state.mute)
.camming(voice_state.self_video)
.streaming(voice_state.self_stream)
.build();
let user_in_vc_data = voice_status.into();
if let Some(channel_id) = voice_state.channel_id {
vcs.entry(guild.id).or_default().insert(
channel_id,
member.user.id,
user_in_vc_data,
);
}
}
}
}
}
}
vcs
}
#[tracing::instrument(skip(vcs))]
async fn handle_event(event: Event, vcs: &mut VCs) {
match event {
Event::VoiceStateUpdate(voice_state_update) => {
let user_id = voice_state_update.user_id;
match voice_state_update.guild_id {
Some(guild_id) => match voice_state_update.channel_id {
Some(channel_id) => {
let voice_status = VoiceStatus::builder()
.self_deafened(voice_state_update.self_deaf)
.self_muted(voice_state_update.self_mute)
.server_deafened(voice_state_update.deaf)
.server_muted(voice_state_update.mute)
.camming(voice_state_update.self_video)
.streaming(voice_state_update.self_stream)
.build();
let user_in_vc_data = voice_status.into();
vcs.entry(guild_id).or_default().insert(
channel_id,
user_id,
user_in_vc_data,
);
tracing::info!(
?guild_id,
?channel_id,
?user_id,
"connected or otherwise changed state while connected"
);
}
None => {
if let Some(channel_vcers) = vcs.get_mut(&guild_id) {
channel_vcers.remove_right(&user_id);
}
tracing::info!(?guild_id, ?user_id, "disconnected");
}
},
None => {
tracing::error!("why doesn't this have a guild id attached?!");
}
}
}
other => {
tracing::warn!(?other, "wasn't expected");
}
}
}

66
src/one_to_many.rs Normal file
View File

@@ -0,0 +1,66 @@
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)]
pub struct OneToManyUniqueBTreeMap<Left, Right> {
left_to_rights: BTreeMap<Left, BTreeSet<Right>>,
right_to_left: BTreeMap<Right, Left>,
}
impl<Left, Right> Default for OneToManyUniqueBTreeMap<Left, Right> {
fn default() -> Self {
Self {
left_to_rights: Default::default(),
right_to_left: Default::default(),
}
}
}
impl<Left, Right> OneToManyUniqueBTreeMap<Left, Right>
where
Left: Ord + Clone,
Right: Ord + Clone,
{
/// Clones `left` and `right` so make sure it's cheap to do so
/// Returns whatever `Left` that `right` was already pointing to
pub fn insert(&mut self, left: Left, right: Right) -> Option<(Left, Right)> {
let old = self.remove_right(&right);
self.left_to_rights
.entry(left.clone())
.or_default()
.insert(right.clone());
self.right_to_left.insert(right, left);
old
}
pub fn get_rights_for(&self, left: &Left) -> Option<&BTreeSet<Right>> {
self.left_to_rights.get(left)
}
pub fn get_left_for(&self, right: &Right) -> Option<&Left> {
self.right_to_left.get(right)
}
pub fn remove_left(&mut self, left: &Left) -> Option<(Left, BTreeSet<Right>)> {
let (left, rights) = self.left_to_rights.remove_entry(left)?;
for right in &rights {
self.right_to_left.remove(right);
}
Some((left, rights))
}
pub fn remove_right(&mut self, right: &Right) -> Option<(Left, Right)> {
let left = self.right_to_left.remove(right)?;
let rights = self.left_to_rights.get_mut(&left)?;
let right = rights.take(right)?;
if rights.is_empty() {
self.left_to_rights.remove(&left);
}
Some((left, right))
}
}

View File

@@ -0,0 +1,71 @@
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone)]
pub struct OneToManyUniqueBTreeMapWithData<Left, Right, RightData> {
left_to_rights: BTreeMap<Left, BTreeMap<Right, RightData>>,
right_to_left: BTreeMap<Right, Left>,
}
impl<Left, Right, RightData> Default for OneToManyUniqueBTreeMapWithData<Left, Right, RightData> {
fn default() -> Self {
Self {
left_to_rights: Default::default(),
right_to_left: Default::default(),
}
}
}
impl<Left, Right, RightData> OneToManyUniqueBTreeMapWithData<Left, Right, RightData>
where
Left: Ord + Clone,
Right: Ord + Clone,
{
/// Clones `left` and `right` so make sure it's cheap to do so
/// Returns whatever `Left` that `right` was already pointing to
pub fn insert(
&mut self,
left: Left,
right: Right,
right_data: RightData,
) -> Option<(Left, Right, RightData)> {
let old = self.remove_right(&right);
self.left_to_rights
.entry(left.clone())
.or_default()
.insert(right.clone(), right_data);
self.right_to_left.insert(right, left);
old
}
pub fn get_rights_for(&self, left: &Left) -> Option<&BTreeMap<Right, RightData>> {
self.left_to_rights.get(left)
}
pub fn get_left_for(&self, right: &Right) -> Option<&Left> {
self.right_to_left.get(right)
}
pub fn remove_left(&mut self, left: &Left) -> Option<(Left, BTreeMap<Right, RightData>)> {
let (left, rights) = self.left_to_rights.remove_entry(left)?;
for (right, _right_data) in &rights {
self.right_to_left.remove(right);
}
Some((left, rights))
}
pub fn remove_right(&mut self, right: &Right) -> Option<(Left, Right, RightData)> {
let left = self.right_to_left.remove(right)?;
let rights = self.left_to_rights.get_mut(&left)?;
let (right, right_data) = rights.remove_entry(right)?;
if rights.is_empty() {
self.left_to_rights.remove(&left);
}
Some((left, right, right_data))
}
}

89
src/one_to_one.rs Normal file
View File

@@ -0,0 +1,89 @@
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct OneToOneBTreeMap<Left, Right> {
left_to_right: BTreeMap<Left, Right>,
right_to_left: BTreeMap<Right, Left>,
}
impl<Left, Right> Default for OneToOneBTreeMap<Left, Right> {
fn default() -> Self {
Self {
left_to_right: Default::default(),
right_to_left: Default::default(),
}
}
}
impl<Left, Right> OneToOneBTreeMap<Left, Right>
where
Left: Ord + Clone,
Right: Ord + Clone,
{
/// Clones `left` and `right` so make sure it's cheap to do so
/// Returns whatever `(Left, Right)` pair was already in the map if one was
pub fn insert(&mut self, left: Left, right: Right) -> Option<(Left, Right)> {
let old = self.remove_by_left(&left); // alternatively, remove_by_right
self.left_to_right.insert(left.clone(), right.clone());
self.right_to_left.insert(right, left);
old
}
pub fn get_right_for(&self, left: &Left) -> Option<&Right> {
self.left_to_right.get(left)
}
pub fn get_left_for(&self, right: &Right) -> Option<&Left> {
self.right_to_left.get(right)
}
pub fn remove_by_left(&mut self, left: &Left) -> Option<(Left, Right)> {
let right = self.left_to_right.remove(left)?;
let left = self.right_to_left.remove(&right)?;
Some((left, right))
}
pub fn remove_by_right(&mut self, right: &Right) -> Option<(Left, Right)> {
let left = self.right_to_left.remove(right)?;
let right = self.left_to_right.remove(&left)?;
Some((left, right))
}
}
impl<Left, Right> IntoIterator for OneToOneBTreeMap<Left, Right> {
type Item = (Left, Right);
type IntoIter = <BTreeMap<Left, Right> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.left_to_right.into_iter()
}
}
impl<'a, Left, Right> IntoIterator for &'a OneToOneBTreeMap<Left, Right> {
type Item = (&'a Left, &'a Right);
type IntoIter = <&'a BTreeMap<Left, Right> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.left_to_right.iter()
}
}
impl<Left, Right> FromIterator<(Left, Right)> for OneToOneBTreeMap<Left, Right>
where
Left: Ord + Clone,
Right: Ord + Clone,
{
fn from_iter<T: IntoIterator<Item = (Left, Right)>>(iter: T) -> Self {
let mut this = Self::default();
for (left, right) in iter {
this.insert(left, right);
}
this
}
}

104
src/vc_user.rs Normal file
View File

@@ -0,0 +1,104 @@
use typed_builder::TypedBuilder;
#[derive(Debug)]
pub enum Microphone {
Unmuted,
ServerMuted,
Muted,
}
#[derive(Debug)]
pub enum Headphone {
Undeafened,
ServerDeafened,
Deafened,
}
#[derive(Debug)]
pub enum Camera {
Showing,
Off,
}
impl From<bool> for Camera {
fn from(camming: bool) -> Self {
if camming {
Camera::Showing
} else {
Camera::Off
}
}
}
#[derive(Debug)]
pub enum Stream {
Sharing,
None,
}
impl From<bool> for Stream {
fn from(streaming: bool) -> Self {
if streaming {
Stream::Sharing
} else {
Stream::None
}
}
}
#[derive(Debug)]
pub struct UserInVCData {
pub microphone: Microphone,
pub headphone: Headphone,
pub camera: Camera,
pub stream: Stream,
}
#[derive(Debug, TypedBuilder)]
pub struct VoiceStatus {
server_deafened: bool,
self_deafened: bool,
server_muted: bool,
self_muted: bool,
camming: bool,
streaming: bool,
}
impl From<VoiceStatus> for UserInVCData {
fn from(
VoiceStatus {
server_deafened,
self_deafened,
server_muted,
self_muted,
camming,
streaming,
}: VoiceStatus,
) -> Self {
let microphone = match (server_muted, self_muted) {
(true, _) => Microphone::ServerMuted,
(false, true) => Microphone::Muted,
(false, false) => Microphone::Unmuted,
};
let headphone = match (server_deafened, self_deafened) {
(true, _) => Headphone::ServerDeafened,
(false, true) => Headphone::Deafened,
(false, false) => Headphone::Undeafened,
};
let camera = camming.into();
let stream = streaming.into();
UserInVCData {
microphone,
headphone,
camera,
stream,
}
}
}