Skip to content

Commit

Permalink
Implement MONITOR
Browse files Browse the repository at this point in the history
  • Loading branch information
progval authored and spb committed Apr 14, 2024
1 parent c238780 commit 251e1f9
Show file tree
Hide file tree
Showing 13 changed files with 523 additions and 10 deletions.
129 changes: 129 additions & 0 deletions sable_ircd/src/command/handlers/monitor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Implementation of the UI of [IRCv3 MONITOR](https://ircv3.net/specs/extensions/monitor)
use super::*;
use crate::monitor::MonitorInsertError;
use crate::utils::LineWrapper;

const MAX_CONTENT_LENGTH: usize = 300; // Conservative limit to avoid hitting 512 bytes limit

#[command_handler("MONITOR")]
fn handle_monitor(
server: &ClientServer,
cmd: &dyn Command,
subcommand: &str,
targets: Option<&str>,
) -> CommandResult {
match subcommand.to_ascii_uppercase().as_str() {
"+" => handle_monitor_add(server, cmd, targets),
"-" => handle_monitor_del(server, cmd, targets),
"C" => handle_monitor_clear(server, cmd),
"L" => handle_monitor_list(server, cmd),
"S" => handle_monitor_show(server, cmd),
_ => Ok(()), // The spec does not say what to do; existing implementations ignore it
}
}

fn handle_monitor_add(
server: &ClientServer,
cmd: &dyn Command,
targets: Option<&str>,
) -> CommandResult {
let targets = targets
.ok_or(CommandError::NotEnoughParameters)? // technically we could just ignore
.split(',')
.map(|target| Nickname::parse_str(cmd, target))
.collect::<Result<Vec<_>, _>>()?; // ditto
let mut monitors = server.monitors.write();
let res = targets
.iter()
.map(|&target| monitors.insert(target, cmd.connection_id()))
.collect::<Result<(), _>>()
.map_err(
|MonitorInsertError::TooManyMonitorsPerConnection { max, current }| {
CommandError::Numeric(make_numeric!(MonListFull, max, current))
},
);
drop(monitors); // Release lock
send_statuses(cmd, targets);
res
}

fn handle_monitor_del(
server: &ClientServer,
cmd: &dyn Command,
targets: Option<&str>,
) -> CommandResult {
let targets = targets
.ok_or(CommandError::NotEnoughParameters)? // technically we could just ignore
.split(',')
.map(|target| Nickname::parse_str(cmd, target))
.collect::<Result<Vec<_>, _>>()?; // ditto

let mut monitors = server.monitors.write();
for target in targets {
monitors.remove(target, cmd.connection_id());
}
Ok(())
}

fn handle_monitor_clear(server: &ClientServer, cmd: &dyn Command) -> CommandResult {
server
.monitors
.write()
.remove_connection(cmd.connection_id());
Ok(())
}

fn handle_monitor_list(server: &ClientServer, cmd: &dyn Command) -> CommandResult {
// Copying the set of monitors to release lock on `server.monitors` ASAP
let monitors: Option<Vec<_>> = server
.monitors
.read()
.monitored_nicks(cmd.connection_id())
.map(|monitors| monitors.iter().copied().collect());

if let Some(monitors) = monitors {
LineWrapper::<',', _, _>::new(MAX_CONTENT_LENGTH, monitors.into_iter())
.for_each(|line| cmd.numeric(make_numeric!(MonList, &line)));
}
cmd.numeric(make_numeric!(EndOfMonList));

Ok(())
}

fn handle_monitor_show(server: &ClientServer, cmd: &dyn Command) -> CommandResult {
// Copying the set of monitors to release lock on `server.monitors` ASAP
let monitors: Option<Vec<_>> = server
.monitors
.read()
.monitored_nicks(cmd.connection_id())
.map(|monitors| monitors.iter().copied().collect());

if let Some(monitors) = monitors {
send_statuses(cmd, monitors);
}
Ok(())
}

fn send_statuses(cmd: &dyn Command, targets: Vec<Nickname>) {
let mut online = Vec::new();
let mut offline = Vec::new();
for target in targets {
match cmd.network().user_by_nick(&target) {
Ok(user) => online.push(user.nuh()),
Err(LookupError::NoSuchNick(_)) => offline.push(target),
Err(e) => {
tracing::error!(
"Unexpected error while computing online status of {}: {}",
target,
e
);
}
}
}

LineWrapper::<',', _, _>::new(MAX_CONTENT_LENGTH, online.into_iter())
.for_each(|line| cmd.numeric(make_numeric!(MonOnline, &line)));
LineWrapper::<',', _, _>::new(MAX_CONTENT_LENGTH, offline.into_iter())
.for_each(|line| cmd.numeric(make_numeric!(MonOffline, &line)));
}
1 change: 1 addition & 0 deletions sable_ircd/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ mod handlers {
mod kill;
mod kline;
mod mode;
mod monitor;
mod motd;
mod names;
mod nick;
Expand Down
1 change: 1 addition & 0 deletions sable_ircd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ use connection_collection::ConnectionCollectionLockHelper;
mod isupport;
use isupport::*;

mod monitor;
mod movable;

pub mod server;
Expand Down
7 changes: 7 additions & 0 deletions sable_ircd/src/messages/numeric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ define_messages! {

440(ServicesNotAvailable) => { () => ":Services are not available"},

// https://ircv3.net/specs/extensions/monitor
730(MonOnline) => { (content: &str ) => ":{content}" },
731(MonOffline) => { (content: &str ) => ":{content}" },
732(MonList) => { (targets: &str) => ":{targets}" },
733(EndOfMonList) => { () => ":End of MONITOR list" },
734(MonListFull) => { (limit: usize, targets: usize) => "{limit} {targets} :Monitor list is full." },

900(LoggedIn) => { (account: &Nickname) => "* {account} :You are now logged in as {account}" }, // TODO: <nick>!<ident>@<host> instead of *
903(SaslSuccess) => { () => ":SASL authentication successful" },
904(SaslFail) => { () => ":SASL authentication failed" },
Expand Down
5 changes: 1 addition & 4 deletions sable_ircd/src/messages/source_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,7 @@ impl MessageSource for update::HistoricMessageSource {

impl MessageSource for update::HistoricUser {
fn format(&self) -> String {
format!(
"{}!{}@{}",
self.nickname, self.user.user, self.user.visible_host
)
self.nuh()
}
}

Expand Down
206 changes: 206 additions & 0 deletions sable_ircd/src/monitor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//! Implementation of [IRCv3 MONITOR](https://ircv3.net/specs/extensions/monitor)
//!
//! Monitors are connection-specific (not user-wide), and not propagated across the network.
//! Therefore, they are identified only by a `ConnectionId`.
use std::collections::{HashMap, HashSet};

use anyhow::{anyhow, Context, Result};
use thiserror::Error;

use crate::make_numeric;
use crate::messages::MessageSink;
use crate::prelude::*;
use crate::ClientServer;
use client_listener::ConnectionId;
use sable_network::prelude::*;
use sable_network::validated::Nickname;

#[derive(Error, Clone, Debug)]
pub enum MonitorInsertError {
#[error("this connection has too many monitors ({current}), maximum is {max}")]
/// `current` may be greater than `max` if server configuration was edited.
TooManyMonitorsPerConnection { max: usize, current: usize },
}

#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
pub struct MonitorSet {
pub max_per_connection: usize,
monitors_by_connection: HashMap<ConnectionId, HashSet<Nickname>>,
monitors_by_nickname: HashMap<Nickname, HashSet<ConnectionId>>,
}

impl MonitorSet {
pub fn new(max_per_connection: usize) -> MonitorSet {
MonitorSet {
max_per_connection,
monitors_by_connection: HashMap::new(),
monitors_by_nickname: HashMap::new(),
}
}

/// Marks the `nick` as being monitored by the given connection
pub fn insert(
&mut self,
nick: Nickname,
monitor: ConnectionId,
) -> Result<(), MonitorInsertError> {
let entry = self
.monitors_by_connection
.entry(monitor)
.or_insert_with(HashSet::new);
if entry.len() >= self.max_per_connection {
return Err(MonitorInsertError::TooManyMonitorsPerConnection {
max: self.max_per_connection,
current: entry.len(),
});
}
entry.insert(nick);
self.monitors_by_nickname
.entry(nick)
.or_insert_with(HashSet::new)
.insert(monitor);
Ok(())
}

/// Marks the `nick` as no longer monitored by the given connection
///
/// Returns whether the nick was indeed monitored by the connection.
pub fn remove(&mut self, nick: Nickname, monitor: ConnectionId) -> bool {
self.monitors_by_connection
.get_mut(&monitor)
.map(|set| set.remove(&nick));
self.monitors_by_nickname
.get_mut(&nick)
.map(|set| set.remove(&monitor))
.unwrap_or(false)
}

/// Remove all monitors of a connection
///
/// Returns the set of nicks the connection monitored, if any.
pub fn remove_connection(&mut self, monitor: ConnectionId) -> Option<HashSet<Nickname>> {
let nicks = self.monitors_by_connection.remove(&monitor);
if let Some(nicks) = &nicks {
for nick in nicks {
self.monitors_by_nickname
.get_mut(nick)
.expect("monitors_by_nickname missing nick present in monitors_by_connection")
.remove(&monitor);
}
}
nicks
}

/// Returns all connections monitoring the given nick
pub fn nick_monitors(&self, nick: &Nickname) -> Option<&HashSet<ConnectionId>> {
self.monitors_by_nickname.get(nick)
}

/// Returns all nicks monitored by the given connection
pub fn monitored_nicks(&self, monitor: ConnectionId) -> Option<&HashSet<Nickname>> {
self.monitors_by_connection.get(&monitor)
}
}

/// Trait of [`NetworkStateChange`] details that are relevant to connections using
/// [IRCv3 MONITOR](https://ircv3.net/specs/extensions/monitor) to monitor users.
pub(crate) trait MonitoredItem: std::fmt::Debug {
/// Same as [`try_notify_monitors`] but logs errors instead of returning `Result`.
fn notify_monitors(&self, server: &ClientServer) {
if let Err(e) = self.try_notify_monitors(server) {
tracing::error!("Error while notifying monitors of {:?}: {}", self, e);
}
}

/// Send `RPL_MONONLINE`/`RPL_MONOFFLINE` to all connections monitoring nicks involved in this
/// event
fn try_notify_monitors(&self, server: &ClientServer) -> Result<()>;
}

impl MonitoredItem for update::NewUser {
fn try_notify_monitors(&self, server: &ClientServer) -> Result<()> {
notify_monitors(server, &self.user.nickname, || {
make_numeric!(MonOnline, &self.user.nuh())
})
}
}

impl MonitoredItem for update::UserNickChange {
fn try_notify_monitors(&self, server: &ClientServer) -> Result<()> {
if self.user.nickname != self.new_nick {
// Don't notify on case change
notify_monitors(server, &self.user.nickname, || {
make_numeric!(MonOffline, &self.user.nickname.to_string())
})?;
notify_monitors(server, &self.new_nick, || {
make_numeric!(
MonOnline,
&update::HistoricUser {
nickname: self.new_nick,
..self.user.clone()
}
.nuh()
)
})?;
}
Ok(())
}
}

impl MonitoredItem for update::UserQuit {
fn try_notify_monitors(&self, server: &ClientServer) -> Result<()> {
notify_monitors(server, &self.user.nickname, || {
make_numeric!(MonOffline, &self.user.nickname.to_string())
})
}
}

impl MonitoredItem for update::BulkUserQuit {
fn try_notify_monitors(&self, server: &ClientServer) -> Result<()> {
self.items
.iter()
.map(|item| item.try_notify_monitors(server))
.collect::<Vec<_>>() // Notify all monitors even if one of them fails halfway
.into_iter()
.collect()
}
}

fn notify_monitors(
server: &ClientServer,
nick: &Nickname,
mut make_numeric: impl FnMut() -> UntargetedNumeric,
) -> Result<()> {
// Copying the set of monitors to release lock on `server.monitors` ASAP
let monitors: Option<Vec<_>> = server
.monitors
.read()
.monitors_by_nickname
.get(nick)
.map(|monitors| monitors.iter().copied().collect());
if let Some(monitors) = monitors {
let network = server.network();
monitors
.into_iter()
.map(|monitor| -> Result<()> {
let Some(conn) = server.find_connection(monitor) else {
// TODO: Remove from monitors?
return Ok(());
};
let user_id = conn
.user_id()
.ok_or(anyhow!("Monitor by user with no user_id {:?}", conn.id()))?;
let monitor_user = network
.user(user_id)
.context("Could not find monitoring user")?;
conn.send(make_numeric().format_for(server, &monitor_user));
Ok(())
})
.collect::<Vec<_>>() // Notify all monitors even if one of them fails halfway
.into_iter()
.collect()
} else {
Ok(())
}
}
Loading

0 comments on commit 251e1f9

Please sign in to comment.