Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MONITOR #113

Merged
merged 1 commit into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading