diff options
| -rw-r--r-- | Cargo.lock | 49 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | mumctl/src/main.rs | 37 | ||||
| -rw-r--r-- | mumd/Cargo.toml | 4 | ||||
| -rw-r--r-- | mumd/src/audio.rs | 39 | ||||
| -rw-r--r-- | mumd/src/audio/input.rs | 35 | ||||
| -rw-r--r-- | mumd/src/audio/output.rs | 134 | ||||
| -rw-r--r-- | mumd/src/audio/transformers.rs | 7 | ||||
| -rw-r--r-- | mumd/src/client.rs | 2 | ||||
| -rw-r--r-- | mumd/src/error.rs | 6 | ||||
| -rw-r--r-- | mumd/src/main.rs | 38 | ||||
| -rw-r--r-- | mumd/src/network.rs | 4 | ||||
| -rw-r--r-- | mumd/src/network/tcp.rs | 41 | ||||
| -rw-r--r-- | mumd/src/network/udp.rs | 19 | ||||
| -rw-r--r-- | mumd/src/state.rs | 43 | ||||
| -rw-r--r-- | mumd/src/state/channel.rs | 13 | ||||
| -rw-r--r-- | mumd/src/state/server.rs | 2 | ||||
| -rw-r--r-- | mumd/src/state/user.rs | 2 | ||||
| -rw-r--r-- | mumlib/src/command.rs | 62 | ||||
| -rw-r--r-- | mumlib/src/config.rs | 58 | ||||
| -rw-r--r-- | mumlib/src/lib.rs | 24 | ||||
| -rw-r--r-- | mumlib/src/state.rs | 41 |
22 files changed, 511 insertions, 161 deletions
@@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "alsa" version = "0.5.0" @@ -686,9 +688,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.95" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" [[package]] name = "libloading" @@ -817,10 +819,8 @@ dependencies = [ "bytes", "chrono", "cpal", - "dasp_frame", "dasp_interpolate", "dasp_ring_buffer", - "dasp_sample", "dasp_signal", "futures-channel", "futures-util", @@ -830,14 +830,12 @@ dependencies = [ "mumble-protocol", "mumlib", "native-tls", - "openssl", "opus", "serde", "strum", "strum_macros", "tokio", "tokio-native-tls", - "tokio-stream", "tokio-util", ] @@ -1035,9 +1033,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "openssl" @@ -1203,24 +1201,24 @@ dependencies = [ [[package]] name = "protobuf" -version = "2.23.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45604fc7a88158e7d514d8e22e14ac746081e7a70d7690074dd0029ee37458d6" +checksum = "db50e77ae196458ccd3dc58a31ea1a90b0698ab1b7928d89f644c25d72070267" [[package]] name = "protobuf-codegen" -version = "2.23.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb87f342b585958c1c086313dbc468dcac3edf5e90362111c26d7a58127ac095" +checksum = "09321cef9bee9ddd36884f97b7f7cc92a586cdc74205c4b3aeba65b5fc9c6f90" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.23.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca6e0e2f898f7856a6328650abc9b2df71b7c1a5f39be0800d19051ad0214b2" +checksum = "1afb68a6d768571da3db86ce55f0f62966e0fc25eaf96acd070ea548a91b0d23" dependencies = [ "protobuf", "protobuf-codegen", @@ -1249,9 +1247,9 @@ dependencies = [ [[package]] name = "rand_chacha" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -1351,9 +1349,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b239a3d5db51252f6f48f42172c65317f37202f4a21021bf5f9d40a408f4592c" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" dependencies = [ "bitflags", "core-foundation", @@ -1472,9 +1470,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" dependencies = [ "proc-macro2", "quote", @@ -1570,17 +1568,6 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] name = "tokio-util" version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -116,6 +116,18 @@ ServerRoot $ mumctl channel connect Channel2 ``` +## Known issues + +The main hub for issues is [our issue +tracker](https://github.com/mum-rs/mum/issues). Additionally, there are some +features that aren't present on the issue tracker: + +- Administration tools. See [the admin tools + project](https://github.com/mum-rs/mum/projects/1). +- Surround output. If this is something you want, [open an + issue](https://github.com/mum-rs/mum/issues/new) so we can take a look at + implementing it. + ## Why? Mostly because it's a fun way of learning a new language. Also: diff --git a/mumctl/src/main.rs b/mumctl/src/main.rs index fbeb0cd..7f428eb 100644 --- a/mumctl/src/main.rs +++ b/mumctl/src/main.rs @@ -1,5 +1,20 @@ +#![warn(elided_lifetimes_in_paths)] +#![warn(meta_variable_misuse)] +#![warn(missing_debug_implementations)] +#![warn(single_use_lifetimes)] +#![warn(unreachable_pub)] +#![warn(unused_crate_dependencies)] +#![warn(unused_import_braces)] +#![warn(unused_lifetimes)] +#![warn(unused_qualifications)] +#![deny(macro_use_extern_crate)] +#![deny(missing_abi)] +#![deny(future_incompatible)] +#![forbid(unsafe_code)] +#![forbid(non_ascii_idents)] + use colored::Colorize; -use log::*; +use log::{Level, LevelFilter, Metadata, Record, error, warn}; use mumlib::command::{ChannelTarget, Command as MumCommand, CommandResponse, MessageTarget}; use mumlib::config::{self, Config, ServerConfig}; use mumlib::state::Channel as MumChannel; @@ -17,11 +32,11 @@ const INDENTATION: &str = " "; struct SimpleLogger; impl log::Log for SimpleLogger { - fn enabled(&self, metadata: &Metadata) -> bool { + fn enabled(&self, metadata: &Metadata<'_>) -> bool { metadata.level() <= Level::Info } - fn log(&self, record: &Record) { + fn log(&self, record: &Record<'_>) { if self.enabled(record.metadata()) { println!( "{}{}", @@ -256,9 +271,9 @@ fn match_opt() -> Result<(), Error> { server .username .as_ref() - .or(username.as_ref()) + .or_else(|| username.as_ref()) .ok_or(CliError::NoUsername)?, - server.password.as_ref().or(password.as_ref()), + server.password.as_ref().or_else(|| password.as_ref()), server.port.unwrap_or(port), server.accept_invalid_cert, ), @@ -525,7 +540,7 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<( (Some("port"), None) => { println!( "{}", - server.port.ok_or(CliError::NotSet("port".to_string()))? + server.port.ok_or_else(|| CliError::NotSet("port".to_string()))? ); } (Some("username"), None) => { @@ -534,7 +549,7 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<( server .username .as_ref() - .ok_or(CliError::NotSet("username".to_string()))? + .ok_or_else(|| CliError::NotSet("username".to_string()))? ); } (Some("password"), None) => { @@ -543,7 +558,7 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<( server .password .as_ref() - .ok_or(CliError::NotSet("password".to_string()))? + .ok_or_else(|| CliError::NotSet("password".to_string()))? ); } (Some("accept_invalid_cert"), None) => { @@ -552,11 +567,11 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<( server .accept_invalid_cert .map(|b| b.to_string()) - .ok_or(CliError::NotSet("accept_invalid_cert".to_string()))? + .ok_or_else(|| CliError::NotSet("accept_invalid_cert".to_string()))? ); } (Some("name"), Some(_)) => { - return Err(CliError::UseServerRename)?; + return Err(CliError::UseServerRename.into()); } (Some("host"), Some(value)) => { server.host = value; @@ -578,7 +593,7 @@ fn match_server_command(server_command: Server, config: &mut Config) -> Result<( } } (Some(_), _) => { - return Err(CliError::ConfigKeyNotFound(key.unwrap()))?; + return Err(CliError::ConfigKeyNotFound(key.unwrap()).into()); } } } diff --git a/mumd/Cargo.toml b/mumd/Cargo.toml index 1e8e63f..28ff2ce 100644 --- a/mumd/Cargo.toml +++ b/mumd/Cargo.toml @@ -23,8 +23,6 @@ cpal = "0.13" bytes = "1.0" dasp_interpolate = { version = "0.11", features = ["linear"] } dasp_signal = "0.11" -dasp_frame = "0.11" -dasp_sample = "0.11" dasp_ring_buffer = "0.11" futures-util = { version = "0.3", features = ["sink"]} futures-channel = "0.3" @@ -32,13 +30,11 @@ hound = "3.4" log = "0.4" mumble-protocol = "0.4.1" native-tls = "0.2" -openssl = { version = "0.10" } opus = "0.2" serde = { version = "1.0", features = ["derive"] } strum = "0.20" strum_macros = "0.20" tokio = { version = "1.0", features = ["macros", "rt", "rt-multi-thread", "sync", "net", "time", "fs"] } -tokio-stream = "0.1.0" tokio-native-tls = "0.3" tokio-util = { version = "0.6", features = ["codec", "net"] } bincode = "1.3.2" diff --git a/mumd/src/audio.rs b/mumd/src/audio.rs index 2e20583..6860741 100644 --- a/mumd/src/audio.rs +++ b/mumd/src/audio.rs @@ -1,3 +1,7 @@ +//! All things audio. +//! +//! Audio is handled mostly as signals from [dasp_signal]. Input/output is handled by [cpal]. + pub mod input; pub mod output; pub mod transformers; @@ -27,8 +31,11 @@ use strum::IntoEnumIterator; use strum_macros::EnumIter; use tokio::sync::watch; +/// The sample rate used internally. const SAMPLE_RATE: u32 = 48000; +/// All types of notifications that can be shown. Each notification can be bound to its own audio +/// file. #[derive(Debug, Eq, PartialEq, Clone, Copy, Hash, EnumIter)] pub enum NotificationEvents { ServerConnect, @@ -65,9 +72,12 @@ impl TryFrom<&str> for NotificationEvents { } } +/// Input audio state. Input audio is picket up from an [AudioInputDevice] (e.g. +/// a microphone) and sent over the network. pub struct AudioInput { device: DefaultAudioInputDevice, + /// Outgoing voice packets that should be sent over the network. channel_receiver: Arc<tokio::sync::Mutex<Box<dyn Stream<Item = VoicePacket<Serverbound>> + Unpin>>>, } @@ -112,12 +122,30 @@ impl AudioInput { } } +impl Debug for AudioInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("AudioInput") + .field("device", &self.device) + .field("channel_receiver", &"receiver") + .finish() + } +} + +#[derive(Debug)] +/// Audio output state. The audio is received from each client over the network, +/// decoded, merged and finally played to an [AudioOutputDevice] (e.g. speaker, +/// headphones, ...). pub struct AudioOutput { device: DefaultAudioOutputDevice, + /// The volume and mute-status of a user ID. user_volumes: Arc<Mutex<HashMap<u32, (f32, bool)>>>, + /// The client stream per user ID. A separate stream is kept for UDP and TCP. + /// + /// Shared with [DefaultAudioOutputDevice]. client_streams: Arc<Mutex<ClientStream>>, + /// Which sound effect should be played on an event. sounds: HashMap<NotificationEvents, Vec<f32>>, } @@ -140,6 +168,7 @@ impl AudioOutput { Ok(res) } + /// Loads sound effects, getting unspecified effects from [get_default_sfx]. pub fn load_sound_effects(&mut self, sound_effects: &[SoundEffect]) { let overrides: HashMap<_, _> = sound_effects .iter() @@ -153,6 +182,7 @@ impl AudioOutput { }) .collect(); + // This makes sure that every [NotificationEvent] is present in [self.sounds]. self.sounds = NotificationEvents::iter() .map(|event| { let bytes = overrides @@ -195,6 +225,7 @@ impl AudioOutput { .collect(); } + /// Decodes a voice packet. pub fn decode_packet_payload( &self, stream_type: VoiceStreamType, @@ -207,10 +238,12 @@ impl AudioOutput { .decode_packet((stream_type, session_id), payload); } + /// Sets the volume of the output device. pub fn set_volume(&self, output_volume: f32) { self.device.set_volume(output_volume); } + /// Sets the incoming volume of a user. pub fn set_user_volume(&self, id: u32, volume: f32) { match self.user_volumes.lock().unwrap().entry(id) { Entry::Occupied(mut entry) => { @@ -222,6 +255,7 @@ impl AudioOutput { } } + /// Mutes another user. pub fn set_mute(&self, id: u32, mute: bool) { match self.user_volumes.lock().unwrap().entry(id) { Entry::Occupied(mut entry) => { @@ -233,12 +267,14 @@ impl AudioOutput { } } + /// Queues a sound effect. pub fn play_effect(&self, effect: NotificationEvents) { let samples = self.sounds.get(&effect).unwrap(); - self.client_streams.lock().unwrap().extend(None, samples); + self.client_streams.lock().unwrap().add_sound_effect(samples); } } +/// Reads a sound effect from disk. // moo fn get_sfx(file: &str) -> Cow<'static, [u8]> { let mut buf: Vec<u8> = Vec::new(); @@ -251,6 +287,7 @@ fn get_sfx(file: &str) -> Cow<'static, [u8]> { } } +/// Gets the default sound effect. fn get_default_sfx() -> Cow<'static, [u8]> { Cow::from(include_bytes!("fallback_sfx.wav").as_ref()) } diff --git a/mumd/src/audio/input.rs b/mumd/src/audio/input.rs index a1227e3..4dfc465 100644 --- a/mumd/src/audio/input.rs +++ b/mumd/src/audio/input.rs @@ -1,6 +1,8 @@ +//! Listens to the microphone and sends it to the networking. use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::{InputCallbackInfo, Sample, SampleFormat, SampleRate, StreamConfig}; use log::*; +use std::fmt::Debug; use tokio::sync::watch; use crate::audio::SAMPLE_RATE; @@ -8,6 +10,7 @@ use crate::audio::transformers::{NoiseGate, Transformer}; use crate::error::{AudioError, AudioStream}; use crate::state::StatePhase; +/// Generates a callback that receives [Sample]s and sends them as floats to a [futures_channel::mpsc::Sender]. pub fn callback<T: Sample>( mut input_sender: futures_channel::mpsc::Sender<Vec<u8>>, mut transformers: Vec<Box<dyn Transformer + Send + 'static>>, @@ -29,8 +32,8 @@ pub fn callback<T: Sample>( buffer.extend(data.by_ref().take(buffer_size - buffer.len())); let encoded = transformers .iter_mut() - .try_fold(&mut buffer[..], |acc, e| e.transform(acc)) - .map(|buf| opus_encoder.encode_vec_float(&*buf, buffer_size).unwrap()); + .try_fold((opus::Channels::Mono, &mut buffer[..]), |acc, e| e.transform(acc)) + .map(|buf| opus_encoder.encode_vec_float(&*buf.1, buffer_size).unwrap()); if let Some(encoded) = encoded { if let Err(e) = input_sender.try_send(encoded) { @@ -43,11 +46,19 @@ pub fn callback<T: Sample>( } } +/// Something that can listen to audio and send it somewhere. +/// +/// One sample is assumed to be an encoded opus frame. See [opus::Encoder]. pub trait AudioInputDevice { + /// Starts the device. fn play(&self) -> Result<(), AudioError>; + /// Stops the device. fn pause(&self) -> Result<(), AudioError>; + /// Sets the input volume of the device. fn set_volume(&self, volume: f32); + /// Returns a receiver to this device's values. fn sample_receiver(&mut self) -> Option<futures_channel::mpsc::Receiver<Vec<u8>>>; + /// The amount of channels this device has. fn num_channels(&self) -> usize; } @@ -59,6 +70,7 @@ pub struct DefaultAudioInputDevice { } impl DefaultAudioInputDevice { + /// Initializes the default audio input. pub fn new( input_volume: f32, phase_watcher: watch::Receiver<StatePhase>, @@ -160,20 +172,35 @@ impl AudioInputDevice for DefaultAudioInputDevice { fn play(&self) -> Result<(), AudioError> { self.stream .play() - .map_err(|e| AudioError::InputPlayError(e)) + .map_err(AudioError::InputPlayError) } + fn pause(&self) -> Result<(), AudioError> { self.stream .pause() - .map_err(|e| AudioError::InputPauseError(e)) + .map_err(AudioError::InputPauseError) } + fn set_volume(&self, volume: f32) { self.volume_sender.send(volume).unwrap(); } + fn sample_receiver(&mut self) -> Option<futures_channel::mpsc::Receiver<Vec<u8>>> { self.sample_receiver.take() } + fn num_channels(&self) -> usize { self.channels as usize } } + +impl Debug for DefaultAudioInputDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DefaultAudioInputDevice") + .field("sample_receiver", &self.sample_receiver) + .field("channels", &self.channels) + .field("volume_sender", &self.volume_sender) + .field("stream", &"cpal::Stream") + .finish() + } +} diff --git a/mumd/src/audio/output.rs b/mumd/src/audio/output.rs index a2f6bcc..6cec6fc 100644 --- a/mumd/src/audio/output.rs +++ b/mumd/src/audio/output.rs @@ -1,23 +1,79 @@ +//! Receives audio packets from the networking and plays them. + use crate::audio::SAMPLE_RATE; use crate::error::{AudioError, AudioStream}; use crate::network::VoiceStreamType; +use bytes::Bytes; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::{OutputCallbackInfo, Sample, SampleFormat, SampleRate, StreamConfig}; +use dasp_ring_buffer::Bounded; use log::*; use mumble_protocol::voice::VoicePacketPayload; use std::collections::{HashMap, VecDeque}; +use std::fmt::Debug; +use std::iter; use std::ops::AddAssign; use std::sync::{Arc, Mutex}; use tokio::sync::watch; type ClientStreamKey = (VoiceStreamType, u32); +/// State for decoding audio received from another user. +#[derive(Debug)] +pub struct ClientAudioData { + buf: Bounded<Vec<f32>>, + output_channels: opus::Channels, + // We need both since a client can hypothetically send both mono + // and stereo packets, and we can't switch a decoder on the fly + // to reuse it. + mono_decoder: opus::Decoder, + stereo_decoder: opus::Decoder, +} + +impl ClientAudioData { + pub fn new(sample_rate: u32, output_channels: opus::Channels) -> Self { + Self { + mono_decoder: opus::Decoder::new(sample_rate, opus::Channels::Mono).unwrap(), + stereo_decoder: opus::Decoder::new(sample_rate, opus::Channels::Stereo).unwrap(), + output_channels, + buf: Bounded::from_full(vec![0.0; sample_rate as usize * output_channels as usize]), //buffer 1 s of audio + } + } + + pub fn store_packet(&mut self, bytes: Bytes) { + let packet_channels = opus::packet::get_nb_channels(&bytes).unwrap(); + let (decoder, channels) = match packet_channels { + opus::Channels::Mono => (&mut self.mono_decoder, 1), + opus::Channels::Stereo => (&mut self.stereo_decoder, 2), + }; + let mut out: Vec<f32> = vec![0.0; 720 * channels * 4]; //720 is because that is the max size of packet we can get that we want to decode + let parsed = decoder + .decode_float(&bytes, &mut out, false) + .expect("Error decoding"); + out.truncate(parsed); + match (packet_channels, self.output_channels) { + (opus::Channels::Mono, opus::Channels::Mono) | (opus::Channels::Stereo, opus::Channels::Stereo) => for sample in out { + self.buf.push(sample); + }, + (opus::Channels::Mono, opus::Channels::Stereo) => for sample in out { + self.buf.push(sample); + self.buf.push(sample); + }, + (opus::Channels::Stereo, opus::Channels::Mono) => for sample in out.into_iter().step_by(2) { + self.buf.push(sample); + }, + } + } +} + +/// Collected state for client opus decoders and sound effects. +#[derive(Debug)] pub struct ClientStream { - buffer_clients: HashMap<ClientStreamKey, (VecDeque<f32>, opus::Decoder)>, //TODO ring buffer? + buffer_clients: HashMap<ClientStreamKey, ClientAudioData>, buffer_effects: VecDeque<f32>, sample_rate: u32, - channels: opus::Channels, + output_channels: opus::Channels, } impl ClientStream { @@ -31,29 +87,21 @@ impl ClientStream { buffer_clients: HashMap::new(), buffer_effects: VecDeque::new(), sample_rate, - channels, + output_channels: channels, } } - fn get_client(&mut self, client: ClientStreamKey) -> &mut (VecDeque<f32>, opus::Decoder) { - let sample_rate = self.sample_rate; - let channels = self.channels; - self.buffer_clients.entry(client).or_insert_with(|| { - let opus_decoder = opus::Decoder::new(sample_rate, channels).unwrap(); - (VecDeque::new(), opus_decoder) - }) + fn get_client(&mut self, client: ClientStreamKey) -> &mut ClientAudioData { + self.buffer_clients.entry(client).or_insert( + ClientAudioData::new(self.sample_rate, self.output_channels) + ) } + /// Decodes a voice packet. pub fn decode_packet(&mut self, client: ClientStreamKey, payload: VoicePacketPayload) { match payload { VoicePacketPayload::Opus(bytes, _eot) => { - let mut out: Vec<f32> = vec![0.0; 720 * (self.channels as usize) * 4]; //720 is because that is the max size of packet we can get that we want to decode - let (buffer, decoder) = self.get_client(client); - let parsed = decoder - .decode_float(&bytes, &mut out, false) - .expect("Error decoding"); - out.truncate(parsed); - buffer.extend(&out); + self.get_client(client).store_packet(bytes); } _ => { unimplemented!("Payload type not supported"); @@ -61,16 +109,19 @@ impl ClientStream { } } - pub fn extend(&mut self, client: Option<ClientStreamKey>, values: &[f32]) { - let buffer = match client { - Some(x) => &mut self.get_client(x).0, - None => &mut self.buffer_effects, - }; - buffer.extend(values.iter().copied()); + /// Extends the sound effect buffer queue with some received values. + pub fn add_sound_effect(&mut self, values: &[f32]) { + self.buffer_effects.extend(values.iter().copied()); } } +/// Adds two values in some saturating way. +/// +/// Since we support [f32], [i16] and [u16] we need some way of adding two values +/// without peaking above/below the edge values. This trait ensures that we can +/// use all three primitive types as a generic parameter. pub trait SaturatingAdd { + /// Adds two values in some saturating way. See trait documentation. fn saturating_add(self, rhs: Self) -> Self; } @@ -104,14 +155,20 @@ pub trait AudioOutputDevice { fn client_streams(&self) -> Arc<Mutex<ClientStream>>; } +/// The default audio output device, as determined by [cpal]. pub struct DefaultAudioOutputDevice { config: StreamConfig, stream: cpal::Stream, + /// The client stream per user ID. A separate stream is kept for UDP and TCP. + /// + /// Shared with [super::AudioOutput]. client_streams: Arc<Mutex<ClientStream>>, + /// Output volume configuration. volume_sender: watch::Sender<f32>, } impl DefaultAudioOutputDevice { + /// Initializes the default audio output. pub fn new( output_volume: f32, user_volumes: Arc<Mutex<HashMap<u32, (f32, bool)>>>, @@ -148,7 +205,7 @@ impl DefaultAudioOutputDevice { let output_stream = match output_supported_sample_format { SampleFormat::F32 => output_device.build_output_stream( &output_config, - curry_callback::<f32>( + callback::<f32>( Arc::clone(&client_streams), output_volume_receiver, user_volumes, @@ -157,7 +214,7 @@ impl DefaultAudioOutputDevice { ), SampleFormat::I16 => output_device.build_output_stream( &output_config, - curry_callback::<i16>( + callback::<i16>( Arc::clone(&client_streams), output_volume_receiver, user_volumes, @@ -166,7 +223,7 @@ impl DefaultAudioOutputDevice { ), SampleFormat::U16 => output_device.build_output_stream( &output_config, - curry_callback::<u16>( + callback::<u16>( Arc::clone(&client_streams), output_volume_receiver, user_volumes, @@ -189,13 +246,13 @@ impl AudioOutputDevice for DefaultAudioOutputDevice { fn play(&self) -> Result<(), AudioError> { self.stream .play() - .map_err(|e| AudioError::OutputPlayError(e)) + .map_err(AudioError::OutputPlayError) } fn pause(&self) -> Result<(), AudioError> { self.stream .pause() - .map_err(|e| AudioError::OutputPauseError(e)) + .map_err(AudioError::OutputPauseError) } fn set_volume(&self, volume: f32) { @@ -211,7 +268,9 @@ impl AudioOutputDevice for DefaultAudioOutputDevice { } } -pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display>( +/// Returns a function that fills a buffer with audio from client streams +/// modified according to some audio configuration. +pub fn callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display>( user_bufs: Arc<Mutex<ClientStream>>, output_volume_receiver: watch::Receiver<f32>, user_volumes: Arc<Mutex<HashMap<u32, (f32, bool)>>>, @@ -227,10 +286,10 @@ pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display> let user_volumes = user_volumes.lock().unwrap(); for (k, v) in user_bufs.buffer_clients.iter_mut() { let (user_volume, muted) = user_volumes.get(&k.1).cloned().unwrap_or((1.0, false)); - for sample in data.iter_mut() { - if !muted { + if !muted { + for (sample, val) in data.iter_mut().zip(v.buf.drain().chain(iter::repeat(0.0))) { *sample = sample.saturating_add(Sample::from( - &(v.0.pop_front().unwrap_or(0.0) * volume * user_volume), + &(val * volume * user_volume), )); } } @@ -242,3 +301,14 @@ pub fn curry_callback<T: Sample + AddAssign + SaturatingAdd + std::fmt::Display> } } } + +impl Debug for DefaultAudioOutputDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DefaultAudioInputDevice") + .field("client_streams", &self.client_streams) + .field("config", &self.config) + .field("volume_sender", &self.volume_sender) + .field("stream", &"cpal::Stream") + .finish() + } +} diff --git a/mumd/src/audio/transformers.rs b/mumd/src/audio/transformers.rs index 25e28b8..21a71b5 100644 --- a/mumd/src/audio/transformers.rs +++ b/mumd/src/audio/transformers.rs @@ -2,10 +2,11 @@ pub trait Transformer { /// Do the transform. Returning `None` is interpreted as "the buffer is unwanted". /// The implementor is free to modify the buffer however it wants to. - fn transform<'a>(&mut self, buf: &'a mut [f32]) -> Option<&'a mut [f32]>; + fn transform<'a>(&mut self, buf: (opus::Channels, &'a mut [f32])) -> Option<(opus::Channels, &'a mut [f32])>; } /// A struct representing a noise gate transform. +#[derive(Debug)] pub struct NoiseGate { alltime_high: f32, open: usize, @@ -25,7 +26,7 @@ impl NoiseGate { } impl Transformer for NoiseGate { - fn transform<'a>(&mut self, buf: &'a mut [f32]) -> Option<&'a mut [f32]> { + fn transform<'a>(&mut self, (channels, buf): (opus::Channels, &'a mut [f32])) -> Option<(opus::Channels, &'a mut [f32])> { const MUTE_PERCENTAGE: f32 = 0.1; let max = buf.iter().map(|e| e.abs()).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); @@ -43,7 +44,7 @@ impl Transformer for NoiseGate { if self.open == 0 { None } else { - Some(buf) + Some((channels, buf)) } } } diff --git a/mumd/src/client.rs b/mumd/src/client.rs index 9e8ca18..753583f 100644 --- a/mumd/src/client.rs +++ b/mumd/src/client.rs @@ -33,7 +33,7 @@ pub async fn handle( packet_sender.clone(), packet_receiver, event_queue.clone(), - ).fuse() => r.map_err(|e| ClientError::TcpError(e)), + ).fuse() => r.map_err(ClientError::TcpError), _ = udp::handle( Arc::clone(&state), connection_info_receiver.clone(), diff --git a/mumd/src/error.rs b/mumd/src/error.rs index da1bdd3..4277d7f 100644 --- a/mumd/src/error.rs +++ b/mumd/src/error.rs @@ -5,6 +5,7 @@ use tokio::sync::mpsc; pub type ServerSendError = mpsc::error::SendError<ControlPacket<Serverbound>>; +#[derive(Debug)] pub enum TcpError { NoConnectionInfoReceived, TlsConnectorBuilderError(native_tls::Error), @@ -40,6 +41,7 @@ impl From<ServerSendError> for TcpError { } } +#[derive(Debug)] pub enum UdpError { NoConnectionInfoReceived, DisconnectBeforeCryptSetup, @@ -53,6 +55,7 @@ impl From<std::io::Error> for UdpError { } } +#[derive(Debug)] pub enum ClientError { TcpError(TcpError), } @@ -65,6 +68,7 @@ impl fmt::Display for ClientError { } } +#[derive(Debug)] pub enum AudioStream { Input, Output, @@ -79,6 +83,7 @@ impl fmt::Display for AudioStream { } } +#[derive(Debug)] pub enum AudioError { NoDevice(AudioStream), NoConfigs(AudioStream, cpal::SupportedStreamConfigsError), @@ -105,6 +110,7 @@ impl fmt::Display for AudioError { } } +#[derive(Debug)] pub enum StateError { AudioError(AudioError), ConfigError(ConfigError), diff --git a/mumd/src/main.rs b/mumd/src/main.rs index a96944c..e7ac033 100644 --- a/mumd/src/main.rs +++ b/mumd/src/main.rs @@ -1,10 +1,25 @@ -mod audio; -mod client; -mod command; -mod error; -mod network; -mod notifications; -mod state; +#![warn(elided_lifetimes_in_paths)] +#![warn(meta_variable_misuse)] +#![warn(missing_debug_implementations)] +#![warn(single_use_lifetimes)] +#![warn(unreachable_pub)] +#![warn(unused_crate_dependencies)] +#![warn(unused_import_braces)] +#![warn(unused_lifetimes)] +#![warn(unused_qualifications)] +#![deny(macro_use_extern_crate)] +#![deny(missing_abi)] +#![deny(future_incompatible)] +#![forbid(unsafe_code)] +#![forbid(non_ascii_idents)] + +pub mod audio; +pub mod client; +pub mod command; +pub mod error; +pub mod network; +pub mod notifications; +pub mod state; use crate::state::State; @@ -76,12 +91,9 @@ async fn main() { _ = receive_commands(command_sender).fuse() => Ok(()), }; - match run { - Err(e) => { - error!("mumd: {}", e); - std::process::exit(1); - } - _ => {} + if let Err(e) = run { + error!("mumd: {}", e); + std::process::exit(1); } } diff --git a/mumd/src/network.rs b/mumd/src/network.rs index 2b803c0..1066fef 100644 --- a/mumd/src/network.rs +++ b/mumd/src/network.rs @@ -30,8 +30,8 @@ impl ConnectionInfo { #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub enum VoiceStreamType { - TCP, - UDP, + Tcp, + Udp, } async fn run_until<F, R>( diff --git a/mumd/src/network/tcp.rs b/mumd/src/network/tcp.rs index f620a32..0fdc4c5 100644 --- a/mumd/src/network/tcp.rs +++ b/mumd/src/network/tcp.rs @@ -14,6 +14,7 @@ use mumble_protocol::{Clientbound, Serverbound}; use mumlib::command::MumbleEventKind; use std::collections::HashMap; use std::convert::{Into, TryInto}; +use std::fmt::Debug; use std::net::SocketAddr; use std::sync::{Arc, RwLock}; use tokio::net::TcpStream; @@ -31,8 +32,8 @@ type TcpSender = SplitSink< type TcpReceiver = SplitStream<Framed<TlsStream<TcpStream>, ControlCodec<Serverbound, Clientbound>>>; -pub(crate) type TcpEventCallback = Box<dyn FnOnce(TcpEventData)>; -pub(crate) type TcpEventSubscriber = Box<dyn FnMut(TcpEventData) -> bool>; //the bool indicates if it should be kept or not +pub(crate) type TcpEventCallback = Box<dyn FnOnce(TcpEventData<'_>)>; +pub(crate) type TcpEventSubscriber = Box<dyn FnMut(TcpEventData<'_>) -> bool>; //the bool indicates if it should be kept or not /// Why the TCP was disconnected. #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] @@ -57,15 +58,15 @@ pub enum TcpEvent { /// Having two different types might feel a bit confusing. Essentially, a /// callback _registers_ to a [TcpEvent] but _takes_ a [TcpEventData] as /// parameter. -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum TcpEventData<'a> { Connected(Result<&'a msgs::ServerSync, mumlib::Error>), Disconnected(DisconnectedReason), TextMessage(&'a msgs::TextMessage), } -impl<'a> From<&TcpEventData<'a>> for TcpEvent { - fn from(t: &TcpEventData) -> Self { +impl From<&TcpEventData<'_>> for TcpEvent { + fn from(t: &TcpEventData<'_>) -> Self { match t { TcpEventData::Connected(_) => TcpEvent::Connected, TcpEventData::Disconnected(reason) => TcpEvent::Disconnected(*reason), @@ -74,7 +75,7 @@ impl<'a> From<&TcpEventData<'a>> for TcpEvent { } } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct TcpEventQueue { callbacks: Arc<RwLock<HashMap<TcpEvent, Vec<TcpEventCallback>>>>, subscribers: Arc<RwLock<HashMap<TcpEvent, Vec<TcpEventSubscriber>>>>, @@ -111,7 +112,7 @@ impl TcpEventQueue { /// Fires all callbacks related to a specific TCP event and removes them from the event queue. /// Also calls all event subscribers, but keeps them in the queue - pub fn resolve<'a>(&self, data: TcpEventData<'a>) { + pub fn resolve(&self, data: TcpEventData<'_>) { if let Some(vec) = self .callbacks .write() @@ -139,6 +140,13 @@ impl TcpEventQueue { } } +impl Debug for TcpEventQueue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TcpEventQueue") + .finish() + } +} + pub async fn handle( state: Arc<RwLock<State>>, mut connection_info_receiver: watch::Receiver<Option<ConnectionInfo>>, @@ -148,13 +156,14 @@ pub async fn handle( event_queue: TcpEventQueue, ) -> Result<(), TcpError> { loop { - let connection_info = 'data: loop { - while connection_info_receiver.changed().await.is_ok() { + let connection_info = loop { + if connection_info_receiver.changed().await.is_ok() { if let Some(data) = connection_info_receiver.borrow().clone() { - break 'data data; + break data; } + } else { + return Err(TcpError::NoConnectionInfoReceived); } - return Err(TcpError::NoConnectionInfoReceived); }; let connect_result = connect( connection_info.socket_addr, @@ -242,12 +251,12 @@ async fn connect( builder.danger_accept_invalid_certs(accept_invalid_cert); let connector: TlsConnector = builder .build() - .map_err(|e| TcpError::TlsConnectorBuilderError(e))? + .map_err(TcpError::TlsConnectorBuilderError)? .into(); let tls_stream = connector .connect(&server_host, stream) .await - .map_err(|e| TcpError::TlsConnectError(e))?; + .map_err(TcpError::TlsConnectError)?; debug!("TLS connected"); // Wrap the TLS stream with Mumble's client-side control-channel codec @@ -304,13 +313,13 @@ async fn send_voice( inner_phase_watcher.changed().await.unwrap(); if matches!( *inner_phase_watcher.borrow(), - StatePhase::Connected(VoiceStreamType::TCP) + StatePhase::Connected(VoiceStreamType::Tcp) ) { break; } } run_until( - |phase| !matches!(phase, StatePhase::Connected(VoiceStreamType::TCP)), + |phase| !matches!(phase, StatePhase::Connected(VoiceStreamType::Tcp)), async { loop { packet_sender.send( @@ -465,7 +474,7 @@ async fn listen( .. } => { state.read().unwrap().audio_output().decode_packet_payload( - VoiceStreamType::TCP, + VoiceStreamType::Tcp, session_id, payload, ); diff --git a/mumd/src/network/udp.rs b/mumd/src/network/udp.rs index 95dcf33..0f78638 100644 --- a/mumd/src/network/udp.rs +++ b/mumd/src/network/udp.rs @@ -37,13 +37,14 @@ pub async fn handle( let receiver = state.read().unwrap().audio_input().receiver(); loop { - let connection_info = 'data: loop { - while connection_info_receiver.changed().await.is_ok() { + let connection_info = loop { + if connection_info_receiver.changed().await.is_ok() { if let Some(data) = connection_info_receiver.borrow().clone() { - break 'data data; + break data; } + } else { + return Err(UdpError::NoConnectionInfoReceived); } - return Err(UdpError::NoConnectionInfoReceived); }; let (sink, source) = connect(&mut crypt_state_receiver).await?; @@ -136,7 +137,7 @@ async fn listen( state .read() .unwrap() - .broadcast_phase(StatePhase::Connected(VoiceStreamType::UDP)); + .broadcast_phase(StatePhase::Connected(VoiceStreamType::Udp)); last_ping_recv.store(timestamp, Ordering::Relaxed); } VoicePacket::Audio { @@ -147,7 +148,7 @@ async fn listen( .. } => { state.read().unwrap().audio_output().decode_packet_payload( - VoiceStreamType::UDP, + VoiceStreamType::Udp, session_id, payload, ); @@ -173,7 +174,7 @@ async fn send_pings( state .read() .unwrap() - .broadcast_phase(StatePhase::Connected(VoiceStreamType::TCP)); + .broadcast_phase(StatePhase::Connected(VoiceStreamType::Tcp)); } match sink .lock() @@ -208,13 +209,13 @@ async fn send_voice( inner_phase_watcher.changed().await.unwrap(); if matches!( *inner_phase_watcher.borrow(), - StatePhase::Connected(VoiceStreamType::UDP) + StatePhase::Connected(VoiceStreamType::Udp) ) { break; } } run_until( - |phase| !matches!(phase, StatePhase::Connected(VoiceStreamType::UDP)), + |phase| !matches!(phase, StatePhase::Connected(VoiceStreamType::Udp)), async { let mut receiver = receiver.lock().await; loop { diff --git a/mumd/src/state.rs b/mumd/src/state.rs index d2d77b1..1992884 100644 --- a/mumd/src/state.rs +++ b/mumd/src/state.rs @@ -20,6 +20,7 @@ use mumlib::command::{ChannelTarget, Command, CommandResponse, MessageTarget, Mu use mumlib::config::Config; use mumlib::Error; use std::{ + fmt::Debug, iter, net::{SocketAddr, ToSocketAddrs}, sync::{Arc, RwLock}, @@ -29,7 +30,7 @@ use tokio::sync::{mpsc, watch}; macro_rules! at { ( $( $event:expr => $generator:expr ),+ $(,)? ) => { ExecutionContext::TcpEventCallback(vec![ - $( ($event, Box::new($generator)), )* + $( ($event, Box::new($generator)), )+ ]) }; } @@ -42,18 +43,18 @@ macro_rules! now { type Responses = Box<dyn Iterator<Item = mumlib::error::Result<Option<CommandResponse>>>>; +type TcpEventCallback = Box<dyn FnOnce(TcpEventData<'_>) -> Responses>; +type TcpEventSubscriberCallback = Box< + dyn FnMut( + TcpEventData<'_>, + &mut mpsc::UnboundedSender<mumlib::error::Result<Option<CommandResponse>>>, + ) -> bool +>; + //TODO give me a better name pub enum ExecutionContext { - TcpEventCallback(Vec<(TcpEvent, Box<dyn FnOnce(TcpEventData) -> Responses>)>), - TcpEventSubscriber( - TcpEvent, - Box< - dyn FnMut( - TcpEventData, - &mut mpsc::UnboundedSender<mumlib::error::Result<Option<CommandResponse>>>, - ) -> bool, - >, - ), + TcpEventCallback(Vec<(TcpEvent, TcpEventCallback)>), + TcpEventSubscriber(TcpEvent, TcpEventSubscriberCallback), Now(Box<dyn FnOnce() -> Responses>), Ping( Box<dyn FnOnce() -> mumlib::error::Result<SocketAddr>>, @@ -63,6 +64,17 @@ pub enum ExecutionContext { ), } +impl Debug for ExecutionContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple(match self { + ExecutionContext::TcpEventCallback(_) => "TcpEventCallback", + ExecutionContext::TcpEventSubscriber(_, _) => "TcpEventSubscriber", + ExecutionContext::Now(_) => "Now", + ExecutionContext::Ping(_, _) => "Ping", + }).finish() + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum StatePhase { Disconnected, @@ -70,6 +82,7 @@ pub enum StatePhase { Connected(VoiceStreamType), } +#[derive(Debug)] pub struct State { config: Config, server: Option<Server>, @@ -90,9 +103,9 @@ impl State { config.audio.input_volume.unwrap_or(1.0), phase_watcher.1.clone(), ) - .map_err(|e| StateError::AudioError(e))?; + .map_err(StateError::AudioError)?; let audio_output = AudioOutput::new(config.audio.output_volume.unwrap_or(1.0)) - .map_err(|e| StateError::AudioError(e))?; + .map_err(StateError::AudioError)?; let mut state = Self { config, server: None, @@ -308,7 +321,7 @@ impl State { } pub fn initialized(&self) { - self.broadcast_phase(StatePhase::Connected(VoiceStreamType::TCP)); + self.broadcast_phase(StatePhase::Connected(VoiceStreamType::Tcp)); self.audio_output .play_effect(NotificationEvents::ServerConnect); } @@ -773,7 +786,7 @@ pub fn handle_command( .unwrap() .users() .iter() - .find(|(_, user)| user.name() == &name) + .find(|(_, user)| user.name() == name) .map(|(e, _)| *e); let id = match id { diff --git a/mumd/src/state/channel.rs b/mumd/src/state/channel.rs index 6995e1e..2ed05c5 100644 --- a/mumd/src/state/channel.rs +++ b/mumd/src/state/channel.rs @@ -169,13 +169,10 @@ pub fn into_channel( impl From<&Channel> for mumlib::state::Channel { fn from(channel: &Channel) -> Self { - mumlib::state::Channel { - description: channel.description.clone(), - links: Vec::new(), - max_users: channel.max_users, - name: channel.name.clone(), - children: Vec::new(), - users: Vec::new(), - } + mumlib::state::Channel::new( + channel.name.clone(), + channel.description.clone(), + channel.max_users, + ) } } diff --git a/mumd/src/state/server.rs b/mumd/src/state/server.rs index 4abde49..5d49457 100644 --- a/mumd/src/state/server.rs +++ b/mumd/src/state/server.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use std::collections::hash_map::Entry; use std::collections::HashMap; -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct Server { channels: HashMap<u32, Channel>, users: HashMap<u32, User>, diff --git a/mumd/src/state/user.rs b/mumd/src/state/user.rs index 5770bca..0ffde90 100644 --- a/mumd/src/state/user.rs +++ b/mumd/src/state/user.rs @@ -78,7 +78,7 @@ impl User { } } - pub fn apply_user_diff(&mut self, diff: &crate::state::user::UserDiff) { + pub fn apply_user_diff(&mut self, diff: &UserDiff) { if let Some(comment) = diff.comment.clone() { self.comment = Some(comment); } diff --git a/mumlib/src/command.rs b/mumlib/src/command.rs index 8cb7cb9..9d98a38 100644 --- a/mumlib/src/command.rs +++ b/mumlib/src/command.rs @@ -1,3 +1,6 @@ +//! [Command]s can be sent from a controller to mumd which might respond with a +//! [CommandResponse]. + use crate::state::{Channel, Server}; use chrono::NaiveDateTime; @@ -7,7 +10,9 @@ use std::fmt; /// Something that happened in our channel at a point in time. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MumbleEvent { + /// When the event occured. pub timestamp: NaiveDateTime, + /// What occured. pub kind: MumbleEventKind } @@ -20,11 +25,17 @@ impl fmt::Display for MumbleEvent { /// The different kinds of events that can happen. #[derive(Debug, Clone, Serialize, Deserialize)] pub enum MumbleEventKind { + /// A user connected to the server and joined our channel. Contains `(user, channel)`. UserConnected(String, String), + /// A user disconnected from the server while in our channel. Contains `(user, channel)`. UserDisconnected(String, String), + /// A user {un,}{muted,deafened}. Contains a rendered message with what changed and the user. UserMuteStateChanged(String), // This logic is kinda weird so we only store the rendered message. + /// A text message was received. Contains who sent the message. TextMessageReceived(String), + /// A user switched to our channel from some other channel. Contains `(user, previous-channel)`. UserJoinedChannel(String, String), + /// A user switched from our channel to some other channel. Contains `(user, new-channel)`. UserLeftChannel(String, String), } @@ -56,73 +67,124 @@ impl fmt::Display for MumbleEventKind { } } +/// Sent by a controller to mumd who might respond with a [CommandResponse]. Not +/// all commands receive a response. #[derive(Clone, Debug, Deserialize, Serialize)] pub enum Command { + /// No response. ChannelJoin { channel_identifier: String, }, + + /// Response: [CommandResponse::ChannelList]. ChannelList, + + /// Force reloading of config file from disk. No response. ConfigReload, + /// Response: [CommandResponse::DeafenStatus]. Toggles if None. DeafenSelf(Option<bool>), Events { block: bool }, + /// Set the outgoing audio volume (i.e. from you to the server). No response. InputVolumeSet(f32), + + /// Response: [CommandResponse::MuteStatus]. Toggles mute state if None. MuteOther(String, Option<bool>), + + /// Response: [CommandResponse::MuteStatus]. Toggles mute state if None. MuteSelf(Option<bool>), + + /// Set the master incoming audio volume (i.e. from the server to you). + /// No response. OutputVolumeSet(f32), + + /// Request a list of past messages. Blocks while waiting for more messages + /// if block is true. Response: multiple [CommandResponse::PastMessage]. PastMessages { block: bool, }, + + /// Response: [CommandResponse::Pong]. Used to test existance of a + /// mumd-instance. Ping, + + /// Send a message to some [MessageTarget]. SendMessage { + /// The message to send. message: String, + /// The target(s) to send the message to. targets: MessageTarget, }, + + /// Connect to the specified server. Response: [CommandResponse::ServerConnect]. ServerConnect { + /// The URL or IP-adress to connect to. host: String, + /// The port to connect to. port: u16, + /// The username to connect with. username: String, + /// The server password, if applicable. Not sent if None. password: Option<String>, + /// Whether to accept an invalid server certificate or not. accept_invalid_cert: bool, }, + + /// Disconnect from the currently connected server. No response. ServerDisconnect, + + /// Send a server status request via UDP (e.g. not requiring a TCP connection). + /// Response: [CommandResponse::ServerStatus]. ServerStatus { host: String, port: u16, }, + + /// Request the status of the current server. Response: [CommandResponse::Status]. Status, + + /// The the volume of the specified user. No response. UserVolumeSet(String, f32), } +/// A response to a sent [Command]. #[derive(Debug, Deserialize, Serialize)] pub enum CommandResponse { ChannelList { channels: Channel, }, + DeafenStatus { is_deafened: bool, }, + Event { event: MumbleEvent, }, + MuteStatus { is_muted: bool, }, + PastMessage { message: (NaiveDateTime, String, String), }, + Pong, + ServerConnect { welcome_message: Option<String>, server_state: Server, }, + ServerStatus { version: u32, users: u32, max_users: u32, bandwidth: u32, }, + Status { server_state: Server, }, diff --git a/mumlib/src/config.rs b/mumlib/src/config.rs index 3edef37..c2544e8 100644 --- a/mumlib/src/config.rs +++ b/mumlib/src/config.rs @@ -1,3 +1,5 @@ +//! Representations of the mumdrc configuration file. + use crate::error::ConfigError; use crate::DEFAULT_PORT; @@ -23,9 +25,13 @@ struct TOMLConfig { servers: Option<Array>, } +/// Our representation of the mumdrc config file. +// Deserialized via [TOMLConfig]. #[derive(Clone, Debug, Default)] pub struct Config { + /// General audio configuration. pub audio: AudioConfig, + /// Saved servers. pub servers: Vec<ServerConfig>, /// Whether we allow connecting to servers with invalid server certificates. /// @@ -34,6 +40,15 @@ pub struct Config { } impl Config { + /// Writes this config to the specified path. + /// + /// Pass create = true if you want the file to be created if it doesn't already exist. + /// + /// # Errors + /// + /// - [ConfigError::WontCreateFile] if the file doesn't exist and create = false was passed. + /// - Any [ConfigError::TOMLErrorSer] encountered when serializing the config. + /// - Any [ConfigError::IOError] encountered when writing the file. pub fn write(&self, path: &Path, create: bool) -> Result<(), ConfigError> { // Possible race here. It's fine since it shows when: // 1) the file doesn't exist when checked and is then created @@ -55,41 +70,60 @@ impl Config { } } +/// Overwrite a specific sound effect with a file that should be played instead. #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct SoundEffect { + /// During which event the effect should be played. pub event: String, + /// The file that should be played. pub file: String, } +/// General audio configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct AudioConfig { + /// The microphone input sensitivity. pub input_volume: Option<f32>, + /// The output main gain. pub output_volume: Option<f32>, + /// Overriden sound effects. pub sound_effects: Option<Vec<SoundEffect>>, } +/// A saved server. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ServerConfig { + /// The alias of the server. pub name: String, + /// The host (URL or IP-adress) of the server. pub host: String, + /// The port, if non-default. pub port: Option<u16>, + /// The username to connect with. Prompted on connection if omitted. pub username: Option<String>, + /// The password to connect with. Nothing is sent to the server if omitted. pub password: Option<String>, + /// Whether to accept invalid server certifications for this server. pub accept_invalid_cert: Option<bool>, } impl ServerConfig { + /// Creates a [SocketAddr] for this server. + /// + /// Returns `None` if no resolution could be made. See + /// [std::net::ToSocketAddrs] for more information. pub fn to_socket_addr(&self) -> Option<SocketAddr> { - match (self.host.as_str(), self.port.unwrap_or(DEFAULT_PORT)) - .to_socket_addrs() - .map(|mut e| e.next()) - { - Ok(Some(addr)) => Some(addr), - _ => None, - } + Some((self.host.as_str(), self.port.unwrap_or(DEFAULT_PORT)) + .to_socket_addrs() + .ok()? + .next()?) } } +/// Finds the default path of the configuration file. +/// +/// The user config dir is looked for first (cross-platform friendly) and +/// `/etc/mumdrc` is used as a fallback. pub fn default_cfg_path() -> PathBuf { match dirs::config_dir() { Some(mut p) => { @@ -143,6 +177,14 @@ impl From<Config> for TOMLConfig { } } +/// Reads the config at the specified path. +/// +/// If the file isn't found, returns a default config. +/// +/// # Errors +/// +/// - Any [ConfigError::TOMLErrorDe] encountered when deserializing the config. +/// - Any [ConfigError::IOError] encountered when reading the file. pub fn read_cfg(path: &Path) -> Result<Config, ConfigError> { match fs::read_to_string(path) { Ok(s) => { @@ -155,7 +197,7 @@ pub fn read_cfg(path: &Path) -> Result<Config, ConfigError> { } else { error!("Error reading config file: {}", e); } - return Ok(Config::default()); + Ok(Config::default()) } } } diff --git a/mumlib/src/lib.rs b/mumlib/src/lib.rs index 9b7d686..2282df5 100644 --- a/mumlib/src/lib.rs +++ b/mumlib/src/lib.rs @@ -1,3 +1,21 @@ +#![warn(elided_lifetimes_in_paths)] +#![warn(meta_variable_misuse)] +#![warn(missing_debug_implementations)] +#![warn(single_use_lifetimes)] +#![warn(unreachable_pub)] +#![warn(unused_crate_dependencies)] +#![warn(unused_import_braces)] +#![warn(unused_lifetimes)] +#![warn(unused_qualifications)] +// #![warn(missing_docs)] may be enabled later when more is documented +#![deny(macro_use_extern_crate)] +#![deny(missing_abi)] +#![deny(future_incompatible)] +#![forbid(unsafe_code)] +#![forbid(non_ascii_idents)] +//! Shared items for crates that want to communicate with mumd and/or mumctl. + + pub mod command; pub mod config; pub mod error; @@ -8,9 +26,15 @@ pub use error::Error; use colored::*; use log::*; +/// The default file path to use for the socket. pub const SOCKET_PATH: &str = "/tmp/mumd"; + +/// The default mumble port. pub const DEFAULT_PORT: u16 = 64738; +/// Setup a minimal fern logger. +/// +/// Format: `LEVEL [yyyy-mm-dd][HH:MM:SS] FILE:LINE MESSAGE` pub fn setup_logger<T: Into<fern::Output>>(target: T, color: bool) { fern::Dispatch::new() .format(move |out, message, record| { diff --git a/mumlib/src/state.rs b/mumlib/src/state.rs index 6fad332..5008465 100644 --- a/mumlib/src/state.rs +++ b/mumlib/src/state.rs @@ -1,25 +1,58 @@ use serde::{Deserialize, Serialize}; use std::fmt; +/// The state of the currently connected Mumble server. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Server { + /// State of the currently connected channel. pub channels: Channel, + /// The welcome text we received when we connected. pub welcome_text: Option<String>, + /// Our username. pub username: String, + /// The host (ip:port) of the server. pub host: String, } +/// A representation of a channel in a Mumble server. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Channel { + /// The description of the channel, if set. pub description: Option<String>, - pub links: Vec<Vec<usize>>, //to represent several walks through the tree to find channels its linked to + /// The maximum number of allowed users in this channel. pub max_users: u32, + /// The name of this channel. pub name: String, + /// Any children this channel has. pub children: Vec<Channel>, + /// This channel's connected users. pub users: Vec<User>, + + links: Vec<Vec<usize>>, //to represent several walks through the tree to find channels its linked to } impl Channel { + /// Create a new Channel representation. + pub fn new( + name: String, + description: Option<String>, + max_users: u32, + ) -> Self { + Self { + description, + max_users, + name, + children: Vec::new(), + users: Vec::new(), + + links: Vec::new(), + } + } + + /// Create an iterator over this channel and its children in a [pre-order + /// traversal](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR) + /// which ensures that parent channels are returned before any of its + /// children. pub fn iter(&self) -> Iter<'_> { Iter { me: Some(&self), @@ -32,6 +65,8 @@ impl Channel { } } + /// Create an iterator over this channel and its childrens connected users + /// in a pre-order traversal. pub fn users_iter(&self) -> UsersIter<'_> { UsersIter { channels: self.children.iter().map(|e| e.users_iter()).collect(), @@ -46,6 +81,8 @@ impl Channel { } } +/// An iterator over channels. Created by [Channel::iter]. +#[derive(Debug)] pub struct Iter<'a> { me: Option<&'a Channel>, channel: Option<usize>, @@ -76,6 +113,8 @@ impl<'a> Iterator for Iter<'a> { } } +/// An iterator over users. Created by [Channel::users_iter]. +#[derive(Debug)] pub struct UsersIter<'a> { channel: Option<usize>, channels: Vec<UsersIter<'a>>, |
