diff --git a/src/requests/mod.rs b/src/requests/mod.rs index c86b58f..c7844f5 100644 --- a/src/requests/mod.rs +++ b/src/requests/mod.rs @@ -43,6 +43,9 @@ impl Server { ClientMessage::Typing { channel_id } => { indicator::start_typing(self, client, channel_id)? } + + ClientMessage::JoinVoice { channel_id } => voice::join(self, client, channel_id)?, + ClientMessage::LeaveVoice { channel_id } => voice::leave(self, client, channel_id)?, }, WsMessage::Binary(data) => { @@ -70,4 +73,27 @@ impl Server { }); } } + + pub fn broadcast_bin_to( + self: &Arc, + targets: &[&String], + bytes: Vec, + ) -> crate::Result<()> { + for c in self.clients.lock().unwrap().iter() { + if !targets.contains(&&c.get_uuid()?) { + continue; + } + + let c = c.clone(); + let server = self.clone(); + let bytes = bytes.clone(); + std::thread::spawn(move || { + server + .wrap_err(&c, c.send_bin(&bytes)) + .expect("Failed to broadcast"); + }); + } + + Ok(()) + } } diff --git a/src/requests/voice.rs b/src/requests/voice.rs index 25adbee..54f16e8 100644 --- a/src/requests/voice.rs +++ b/src/requests/voice.rs @@ -3,13 +3,56 @@ use std::sync::Arc; crate::logger!(LOGGER "Voice chat"); -pub fn voice(server: &Arc, client: &Client, data: &[u8]) -> crate::Result<()> { - let prefix = (10u16).to_le_bytes(); +pub fn join(server: &Arc, client: &Client, channel_id: &str) -> crate::Result<()> { + let user_id = client.get_uuid()?; + let voice_id = server + .voice + .lock() + .unwrap() + .set(user_id.clone(), channel_id.to_string()); + + server.broadcast(crate::types::message::ServerMessage::VoiceJoin { + user_id, + channel_id: channel_id.to_string(), + voice_id: voice_id, + }); + + Ok(()) +} + +pub fn leave(server: &Arc, client: &Client, channel_id: &str) -> crate::Result<()> { + let user_id = client.get_uuid()?; + + let Some(voice_id) = server.voice.lock().unwrap().remove(channel_id, &user_id) else { + return Ok(()); + }; + + server.broadcast(crate::types::message::ServerMessage::VoiceLeave { + user_id, + channel_id: channel_id.to_string(), + voice_id, + }); + + Ok(()) +} + +pub fn voice(server: &Arc, client: &Client, data: &[u8]) -> crate::Result<()> { + let v = server.voice.lock().unwrap(); + let Some((channel_id, voice_id)) = v.find_user(&client.get_uuid()?) else { + return Ok(()); + }; + + let targets = v.get(&channel_id); + + let prefix = voice_id.to_le_bytes(); let mut payload = Vec::with_capacity(prefix.len() + data.len()); payload.extend_from_slice(&prefix); payload.extend_from_slice(data); client.send_bin(&payload)?; + + server.broadcast_bin_to(&targets, payload)?; + Ok(()) } diff --git a/src/server.rs b/src/server.rs index aac6fe6..525bb03 100644 --- a/src/server.rs +++ b/src/server.rs @@ -25,7 +25,7 @@ use crate::{ cli, logger, plugin::{Plugin, loader::PluginLoader, types::LoaderMessage}, types::{self, message::WsMessage}, - utils::{self, auth, client::Client}, + utils::{self, auth, client::Client, voice::Voice}, }; #[derive(serde::Serialize, serde::Deserialize)] @@ -45,6 +45,7 @@ pub struct Server { pub db: utils::database::Database, pub shutting_down: AtomicBool, pub indicators: Mutex>, + pub voice: Mutex, } impl Default for ServerConfig { @@ -81,6 +82,7 @@ impl Server { plugins: Mutex::new(Vec::new()), shutting_down: AtomicBool::new(false), indicators: Mutex::new(Vec::new()), + voice: Mutex::new(Voice::new()), }) } diff --git a/src/types.rs b/src/types.rs index 0c86311..169bd82 100644 --- a/src/types.rs +++ b/src/types.rs @@ -92,6 +92,14 @@ pub mod message { Typing { channel_id: String, }, + + JoinVoice { + channel_id: String, + }, + + LeaveVoice { + channel_id: String, + }, } /// Messages sent *from the server* to the client @@ -136,6 +144,18 @@ pub mod message { }, Chunk(Vec), + + VoiceJoin { + user_id: String, + channel_id: String, + voice_id: u16, + }, + + VoiceLeave { + user_id: String, + channel_id: String, + voice_id: u16, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 270e652..69a2226 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -3,3 +3,4 @@ pub mod client; pub mod database; pub mod logger; pub mod vfs; +pub mod voice; diff --git a/src/utils/voice.rs b/src/utils/voice.rs new file mode 100644 index 0000000..245540f --- /dev/null +++ b/src/utils/voice.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; + +pub struct Voice { + // channel_id -> (user_id -> voice_id) + connections: HashMap>, +} + +impl Voice { + pub fn new() -> Self { + Self { + connections: HashMap::new(), + } + } + + /// Insert or update a user's voice_id in a channel + pub fn set(&mut self, user_id: String, channel_id: String) -> u16 { + let voice_id = rand::random::(); + + self.connections + .entry(channel_id) + .or_insert_with(HashMap::new) + .insert(user_id, voice_id); + + voice_id + } + + /// Remove a user from a channel + /// Returns their voice_id if they existed + pub fn remove(&mut self, channel_id: &str, user_id: &str) -> Option { + let channel = self.connections.get_mut(channel_id)?; + let voice_id = channel.remove(user_id)?; + + // Clean up empty channels + if channel.is_empty() { + self.connections.remove(channel_id); + } + + Some(voice_id) + } + + /// Get all users + voice_ids in a channel + pub fn get(&self, channel_id: &str) -> Vec<&String> { + self.connections + .get(channel_id) + .map(|users| users.keys().collect()) + .unwrap_or_default() + } + + /// Check if a user exists in a channel + pub fn check(&self, channel_id: &str, user_id: &str) -> Option { + self.connections + .get(channel_id) + .and_then(|users| users.get(user_id).copied()) + } + + pub fn find_user(&self, user_id: &str) -> Option<(&String, u16)> { + self.connections + .iter() + .find_map(|(channel_id, users)| users.get(user_id).map(|v| (channel_id, *v))) + } +}