diff options
| -rw-r--r-- | CHANGELOG | 5 | ||||
| -rw-r--r-- | Cargo.lock | 2 | ||||
| -rw-r--r-- | documentation/mumctl.1 | 5 | ||||
| -rw-r--r-- | documentation/mumctl.txt | 3 | ||||
| -rw-r--r-- | mumctl/src/main.rs | 28 | ||||
| -rw-r--r-- | mumd/Cargo.toml | 1 | ||||
| -rw-r--r-- | mumd/src/network/tcp.rs | 41 | ||||
| -rw-r--r-- | mumd/src/state.rs | 143 | ||||
| -rw-r--r-- | mumlib/Cargo.toml | 2 | ||||
| -rw-r--r-- | mumlib/src/command.rs | 81 | ||||
| -rw-r--r-- | mumlib/src/error.rs | 2 |
11 files changed, 250 insertions, 63 deletions
@@ -22,11 +22,13 @@ Added * --version now includes the current commit hash. * Server passwords. Thanks @rbran! * Added support for sending and receiving text messages. + * See a list of occured events with +mumctl events+. Changed ~~~~~~~ -* Changed how you mute yourself/others. See man pages for details on the new options. + * Changed how you mute yourself/others. See man pages for details on the new options. + * The current channel status is now printed when connecting to a server. // Removed // ~~~~~~~ @@ -39,6 +41,7 @@ Fixed be found. * Lots of other minor informative error messages instead of panics. * Status requests are sent in parallel. + * Pings are now less spammy in the log output. Other ~~~~~ @@ -139,6 +139,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi", ] @@ -814,6 +815,7 @@ version = "0.4.0" dependencies = [ "bincode", "bytes", + "chrono", "cpal", "dasp_frame", "dasp_interpolate", diff --git a/documentation/mumctl.1 b/documentation/mumctl.1 index 01840d1..8d8ba72 100644 --- a/documentation/mumctl.1 +++ b/documentation/mumctl.1 @@ -94,6 +94,11 @@ mumctl disconnect Disconnect from the currently connected server. .RE .sp +mumctl events +.RS 4 +Print all events that have occured since mumd was started. +.RE +.sp mumctl help .RS 4 Show a help message. diff --git a/documentation/mumctl.txt b/documentation/mumctl.txt index 888d847..1c8ee8f 100644 --- a/documentation/mumctl.txt +++ b/documentation/mumctl.txt @@ -58,6 +58,9 @@ mumctl deafen :: mumctl disconnect :: Disconnect from the currently connected server. +mumctl events :: + Print all events that have occured since mumd was started. + mumctl help :: Show a help message. diff --git a/mumctl/src/main.rs b/mumctl/src/main.rs index 84517d5..1fb2a45 100644 --- a/mumctl/src/main.rs +++ b/mumctl/src/main.rs @@ -87,6 +87,11 @@ enum Command { }, /// Send a message to a channel or a user Message(Target), + /// Get events that have happened since we connected + Events { + #[structopt(short = "f", long = "follow")] + follow: bool, + }, } #[derive(Debug, StructOpt)] @@ -263,10 +268,10 @@ fn match_opt() -> Result<(), Error> { password: password.map(|x| x.to_string()), accept_invalid_cert: true, //TODO })??; - if let Some(CommandResponse::ServerConnect { welcome_message }) = response { - println!("Connected to {}", host); + if let Some(CommandResponse::ServerConnect { welcome_message, server_state }) = response { + parse_state(&server_state); if let Some(message) = welcome_message { - println!("Welcome: {}", message); + println!("\nWelcome: {}", message); } } } @@ -366,7 +371,7 @@ fn match_opt() -> Result<(), Error> { for response in send_command_multi(MumCommand::PastMessages { block: follow })? { match response { Ok(Some(CommandResponse::PastMessage { message })) => { - println!("{}: {}", message.1, message.0) + println!("[{}] {}: {}", message.0.format("%d %b %H:%M"), message.2, message.1) } Ok(_) => unreachable!("Response should only be a Some(PastMessages)"), Err(e) => error!("{}", e), @@ -381,7 +386,7 @@ fn match_opt() -> Result<(), Error> { } => { let msg = MumCommand::SendMessage { message, - target: if names.is_empty() { + targets: if names.is_empty() { MessageTarget::Channel(vec![(ChannelTarget::Current, recursive)]) } else { MessageTarget::Channel( @@ -397,11 +402,22 @@ fn match_opt() -> Result<(), Error> { Target::User { message, names } => { let msg = MumCommand::SendMessage { message, - target: MessageTarget::User(names), + targets: MessageTarget::User(names), }; send_command(msg)??; } }, + Command::Events { follow } => { + for response in send_command_multi(MumCommand::Events { block: follow })? { + match response { + Ok(Some(CommandResponse::Event { event })) => { + println!("{}", event) + } + Ok(_) => unreachable!("Response should only be a Some(Event)"), + Err(e) => error!("{}", e), + } + } + } } let config_path = config::default_cfg_path(); diff --git a/mumd/Cargo.toml b/mumd/Cargo.toml index d8e2635..1e8e63f 100644 --- a/mumd/Cargo.toml +++ b/mumd/Cargo.toml @@ -42,6 +42,7 @@ tokio-stream = "0.1.0" tokio-native-tls = "0.3" tokio-util = { version = "0.6", features = ["codec", "net"] } bincode = "1.3.2" +chrono = "0.4" libnotify = { version = "1.0", optional = true } diff --git a/mumd/src/network/tcp.rs b/mumd/src/network/tcp.rs index 5cc2bf7..4a753bf 100644 --- a/mumd/src/network/tcp.rs +++ b/mumd/src/network/tcp.rs @@ -13,6 +13,7 @@ use mumble_protocol::control::{msgs, ClientControlCodec, ControlCodec, ControlPa use mumble_protocol::crypt::ClientCryptState; use mumble_protocol::voice::VoicePacket; use mumble_protocol::{Clientbound, Serverbound}; +use mumlib::command::MumbleEventKind; use std::collections::HashMap; use std::convert::{Into, TryInto}; use std::net::SocketAddr; @@ -309,6 +310,10 @@ async fn listen( let mut crypt_state = None; let mut crypt_state_sender = Some(crypt_state_sender); + let mut last_late = 0; + let mut last_lost = 0; + let mut last_resync = 0; + loop { let packet = match stream.next().await { Some(Ok(packet)) => packet, @@ -337,6 +342,8 @@ async fn listen( if let Some(user) = user { notifications::send(format!("{}: {}", user, msg.get_message())); //TODO: probably want a config flag for this + let user = user.to_string(); + state.push_event(MumbleEventKind::TextMessageReceived(user)) //TODO also include message target } state.register_message((msg.get_message().to_owned(), msg.get_actor())); drop(state); @@ -435,6 +442,40 @@ async fn listen( } } } + ControlPacket::Ping(msg) => { + trace!("Received Ping {:?}", *msg); + + let late = msg.get_late(); + let lost = msg.get_lost(); + let resync = msg.get_resync(); + + let late = late - last_late; + let lost = lost - last_lost; + let resync = resync - last_resync; + + last_late += late; + last_lost += lost; + last_resync += resync; + + macro_rules! format_if_nonzero { + ($value:expr) => { + if $value != 0 { + format!("\n {}: {}", stringify!($value), $value) + } else { + String::new() + } + } + } + + if late != 0 || lost != 0 || resync != 0 { + debug!( + "Ping:{}{}{}", + format_if_nonzero!(late), + format_if_nonzero!(lost), + format_if_nonzero!(resync), + ); + } + } packet => { debug!("Received unhandled ControlPacket {:#?}", packet); } diff --git a/mumd/src/state.rs b/mumd/src/state.rs index 942d320..d12b5b6 100644 --- a/mumd/src/state.rs +++ b/mumd/src/state.rs @@ -8,14 +8,15 @@ use crate::network::tcp::{TcpEvent, TcpEventData}; use crate::network::{ConnectionInfo, VoiceStreamType}; use crate::notifications; use crate::state::server::Server; - use crate::state::user::UserDiff; + +use chrono::NaiveDateTime; use log::*; use mumble_protocol::control::msgs; use mumble_protocol::control::ControlPacket; use mumble_protocol::ping::PongPacket; use mumble_protocol::voice::Serverbound; -use mumlib::command::{ChannelTarget, Command, CommandResponse, MessageTarget}; +use mumlib::command::{ChannelTarget, Command, CommandResponse, MessageTarget, MumbleEvent, MumbleEventKind}; use mumlib::config::Config; use mumlib::Error; use std::{ @@ -72,9 +73,11 @@ pub struct State { server: Option<Server>, audio_input: AudioInput, audio_output: AudioOutput, - message_buffer: Vec<(String, u32)>, + message_buffer: Vec<(NaiveDateTime, String, u32)>, phase_watcher: (watch::Sender<StatePhase>, watch::Receiver<StatePhase>), + + events: Vec<MumbleEvent>, } impl State { @@ -95,6 +98,7 @@ impl State { audio_output, message_buffer: Vec::new(), phase_watcher, + events: Vec::new(), }; state.reload_config(); Ok(state) @@ -129,21 +133,25 @@ impl State { // this is someone else // send notification only if we've passed the connecting phase if matches!(*self.phase_receiver().borrow(), StatePhase::Connected(_)) { - let channel_id = msg.get_channel_id(); - - if channel_id - == self.get_users_channel(self.server().unwrap().session_id().unwrap()) - { - if let Some(channel) = self.server().unwrap().channels().get(&channel_id) { - notifications::send(format!( - "{} connected and joined {}", - &msg.get_name(), - channel.name() - )); - } - - self.audio_output - .play_effect(NotificationEvents::UserConnected); + let this_channel = msg.get_channel_id(); + let other_channel = self.get_users_channel(self.server().unwrap().session_id().unwrap()); + let this_channel_name = self + .server() + .unwrap() + .channels() + .get(&this_channel) + .map(|c| c.name()) + .unwrap_or("<unnamed channel>") + .to_string(); + + if this_channel == other_channel { + notifications::send(format!( + "{} connected and joined {}", + msg.get_name(), + this_channel_name, + )); + self.push_event(MumbleEventKind::UserConnected(msg.get_name().to_string(), this_channel_name)); + self.audio_output.play_effect(NotificationEvents::UserConnected); } } } @@ -165,6 +173,7 @@ impl State { .users_mut() .get_mut(&session) .unwrap(); + let username = user.name().to_string(); let mute = if msg.has_self_mute() && user.self_mute() != msg.get_self_mute() { Some(msg.get_self_mute()) @@ -180,35 +189,43 @@ impl State { let diff = UserDiff::from(msg); user.apply_user_diff(&diff); - let user = self.server().unwrap().users().get(&session).unwrap(); if Some(session) != self.server().unwrap().session_id() { - //send notification if the user moved to or from any channel + // Send notification if the user moved either to or from our channel if let Some(to_channel) = diff.channel_id { let this_channel = self.get_users_channel(self.server().unwrap().session_id().unwrap()); - if from_channel == this_channel || to_channel == this_channel { + + if from_channel == this_channel { + // User moved from our channel to somewhere else if let Some(channel) = self.server().unwrap().channels().get(&to_channel) { + let channel = channel.name().to_string(); notifications::send(format!( "{} moved to channel {}", - user.name(), - channel.name() + &username, + &channel, )); - } else { - warn!("{} moved to invalid channel {}", user.name(), to_channel); + self.push_event(MumbleEventKind::UserLeftChannel(username.clone(), channel)); } - self.audio_output - .play_effect(if from_channel == this_channel { - NotificationEvents::UserJoinedChannel - } else { - NotificationEvents::UserLeftChannel - }); + self.audio_output.play_effect(NotificationEvents::UserLeftChannel); + } else if to_channel == this_channel { + // User moved from somewhere else to our channel + if let Some(channel) = self.server().unwrap().channels().get(&from_channel) { + let channel = channel.name().to_string(); + notifications::send(format!( + "{} moved to your channel from {}", + &username, + &channel, + )); + self.push_event(MumbleEventKind::UserJoinedChannel(username.clone(), channel)); + } + self.audio_output.play_effect(NotificationEvents::UserJoinedChannel); } } //send notification if a user muted/unmuted if mute != None || deaf != None { - let mut s = user.name().to_string(); + let mut s = username; if let Some(mute) = mute { s += if mute { " muted" } else { " unmuted" }; } @@ -219,7 +236,8 @@ impl State { s += if deaf { " deafened" } else { " undeafened" }; } s += " themselves"; - notifications::send(s); + notifications::send(s.clone()); + self.push_event(MumbleEventKind::UserMuteStateChanged(s)); } } } @@ -233,11 +251,25 @@ impl State { let this_channel = self.get_users_channel(self.server().unwrap().session_id().unwrap()); let other_channel = self.get_users_channel(msg.get_session()); if this_channel == other_channel { - self.audio_output - .play_effect(NotificationEvents::UserDisconnected); - if let Some(user) = self.server().unwrap().users().get(&msg.get_session()) { - notifications::send(format!("{} disconnected", &user.name())); - } + let channel_name = self + .server() + .unwrap() + .channels() + .get(&this_channel) + .map(|c| c.name()) + .unwrap_or("<unnamed channel>") + .to_string(); + let user_name = self + .server() + .unwrap() + .users() + .get(&msg.get_session()) + .map(|u| u.name()) + .unwrap_or("<unknown user>") + .to_string(); + notifications::send(format!("{} disconnected", &user_name)); + self.push_event(MumbleEventKind::UserDisconnected(user_name, channel_name)); + self.audio_output.play_effect(NotificationEvents::UserDisconnected); } self.server_mut() @@ -266,7 +298,7 @@ impl State { } pub fn register_message(&mut self, msg: (String, u32)) { - self.message_buffer.push(msg); + self.message_buffer.push((chrono::Local::now().naive_local(), msg.0, msg.1)); } pub fn broadcast_phase(&self, phase: StatePhase) { @@ -279,6 +311,11 @@ impl State { .play_effect(NotificationEvents::ServerConnect); } + /// Store a new event + pub fn push_event(&mut self, kind: MumbleEventKind) { + self.events.push(MumbleEvent { timestamp: chrono::Local::now().naive_local(), kind }); + } + pub fn audio_input(&self) -> &AudioInput { &self.audio_input } @@ -418,6 +455,19 @@ pub fn handle_command( new_deaf.map(|b| CommandResponse::DeafenStatus { is_deafened: b }) )) } + Command::Events { block } => { + if block { + warn!("Blocking event list is unimplemented"); + now!(Err(Error::Unimplemented)) + } else { + let events: Vec<_> = state + .events + .iter() + .map(|event| Ok(Some(CommandResponse::Event { event: event.clone() }))) + .collect(); + ExecutionContext::Now(Box::new(move || Box::new(events.into_iter()))) + } + } Command::InputVolumeSet(volume) => { state.audio_input.set_volume(volume); now!(Ok(None)) @@ -555,7 +605,8 @@ pub fn handle_command( accept_invalid_cert, ))) .unwrap(); - at!(TcpEvent::Connected, |res| { + let state = Arc::clone(&og_state); + at!(TcpEvent::Connected, move |res| { //runs the closure when the client is connected if let TcpEventData::Connected(res) = res { Box::new(iter::once(res.map(|msg| { @@ -565,6 +616,7 @@ pub fn handle_command( } else { None }, + server_state: state.read().unwrap().server.as_ref().unwrap().into(), }) }))) } else { @@ -601,12 +653,12 @@ pub fn handle_command( }), Box::new(move |pong| { Ok(pong.map(|pong| { - (CommandResponse::ServerStatus { + CommandResponse::ServerStatus { version: pong.version, users: pong.users, max_users: pong.max_users, bandwidth: pong.bandwidth, - }) + } })) }), ), @@ -650,6 +702,7 @@ pub fn handle_command( Box::new(move |data, sender| { if let TcpEventData::TextMessage(a) = data { let message = ( + chrono::Local::now().naive_local(), a.get_message().to_owned(), ref_state .read() @@ -669,14 +722,14 @@ pub fn handle_command( let messages = std::mem::take(&mut state.message_buffer); let messages: Vec<_> = messages .into_iter() - .map(|(msg, user)| (msg, state.get_user_name(user).unwrap())) + .map(|(timestamp, msg, user)| (timestamp, msg, state.get_user_name(user).unwrap())) .map(|e| Ok(Some(CommandResponse::PastMessage { message: e }))) .collect(); ExecutionContext::Now(Box::new(move || Box::new(messages.into_iter()))) } } - Command::SendMessage { message, target } => { + Command::SendMessage { message, targets } => { if !matches!(*state.phase_receiver().borrow(), StatePhase::Connected(_)) { return now!(Err(Error::Disconnected)); } @@ -685,7 +738,7 @@ pub fn handle_command( msg.set_message(message); - match target { + match targets { MessageTarget::Channel(channels) => for (channel, recursive) in channels { let channel_id = if let ChannelTarget::Named(name) = channel { let channel = state.server().unwrap().channel_name(&name); diff --git a/mumlib/Cargo.toml b/mumlib/Cargo.toml index 5c9d4e1..5ec9365 100644 --- a/mumlib/Cargo.toml +++ b/mumlib/Cargo.toml @@ -13,7 +13,7 @@ readme = "../README.md" [dependencies] colored = "2" -chrono = "0.4" +chrono = { version = "0.4", features = [ "serde" ] } dirs = "3" fern = "0.6" log = "0.4" diff --git a/mumlib/src/command.rs b/mumlib/src/command.rs index 4f4cf3a..8cb7cb9 100644 --- a/mumlib/src/command.rs +++ b/mumlib/src/command.rs @@ -1,6 +1,60 @@ use crate::state::{Channel, Server}; +use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; +use std::fmt; + +/// Something that happened in our channel at a point in time. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MumbleEvent { + pub timestamp: NaiveDateTime, + pub kind: MumbleEventKind +} + +impl fmt::Display for MumbleEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}] {}", self.timestamp.format("%d %b %H:%M"), self.kind) + } +} + +/// The different kinds of events that can happen. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MumbleEventKind { + UserConnected(String, String), + UserDisconnected(String, String), + UserMuteStateChanged(String), // This logic is kinda weird so we only store the rendered message. + TextMessageReceived(String), + UserJoinedChannel(String, String), + UserLeftChannel(String, String), +} + +//TODO These strings are (mostly) duplicated with their respective notifications. +// The only difference is that the text message event doesn't contain the text message. +impl fmt::Display for MumbleEventKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MumbleEventKind::UserConnected(user, channel) => { + write!(f, "{} connected to {}", user, channel) + } + MumbleEventKind::UserDisconnected(user, channel) => { + write!(f, "{} disconnected from {}", user, channel) + } + MumbleEventKind::UserMuteStateChanged(message) => { + write!(f, "{}", message) + } + MumbleEventKind::TextMessageReceived(user) => { + write!(f, "{} sent a text message", user) + } + MumbleEventKind::UserJoinedChannel(name, from) => { + write!(f, "{} moved to your channel from {}", name, from) + } + MumbleEventKind::UserLeftChannel(name, to) => { + write!(f, "{} moved to {}", name, to) + } + + } + } +} #[derive(Clone, Debug, Deserialize, Serialize)] pub enum Command { @@ -10,11 +64,21 @@ pub enum Command { ChannelList, ConfigReload, DeafenSelf(Option<bool>), + Events { + block: bool + }, InputVolumeSet(f32), MuteOther(String, Option<bool>), MuteSelf(Option<bool>), OutputVolumeSet(f32), + PastMessages { + block: bool, + }, Ping, + SendMessage { + message: String, + targets: MessageTarget, + }, ServerConnect { host: String, port: u16, @@ -29,13 +93,6 @@ pub enum Command { }, Status, UserVolumeSet(String, f32), - PastMessages { - block: bool, - }, - SendMessage { - message: String, - target: MessageTarget, - }, } #[derive(Debug, Deserialize, Serialize)] @@ -46,12 +103,19 @@ pub enum CommandResponse { 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, @@ -62,9 +126,6 @@ pub enum CommandResponse { Status { server_state: Server, }, - PastMessage { - message: (String, String), - }, } /// Messages sent to channels can be sent either to a named channel or the diff --git a/mumlib/src/error.rs b/mumlib/src/error.rs index 83f97b8..2cb3927 100644 --- a/mumlib/src/error.rs +++ b/mumlib/src/error.rs @@ -11,6 +11,7 @@ pub enum Error { InvalidServerAddr(String, u16), InvalidUsername(String), InvalidServerPassword, + Unimplemented, NotConnectedToChannel, } @@ -27,6 +28,7 @@ impl fmt::Display for Error { } Error::InvalidUsername(username) => write!(f, "Invalid username: {}", username), Error::InvalidServerPassword => write!(f, "Invalid server password"), + Error::Unimplemented => write!(f, "Unimplemented"), Error::NotConnectedToChannel => write!(f, "Not connected to a channel"), } } |
