diff --git a/sable_ircd/src/command/handlers/whois.rs b/sable_ircd/src/command/handlers/whois.rs index e55da044..c6ff5f0c 100644 --- a/sable_ircd/src/command/handlers/whois.rs +++ b/sable_ircd/src/command/handlers/whois.rs @@ -20,7 +20,7 @@ fn whois_handler( response.numeric(make_numeric!(WhoisUser, &target)); if let Ok(Some(account)) = target.account() { - response.numeric(make_numeric!(WhoisAccount, &target, &account.name())); + response.numeric(make_numeric!(WhoisAccount, &target.nick(), &account.name())); } if let Some(away_reason) = target.away_reason() { diff --git a/sable_ircd/src/command/handlers/whowas.rs b/sable_ircd/src/command/handlers/whowas.rs new file mode 100644 index 00000000..207ee851 --- /dev/null +++ b/sable_ircd/src/command/handlers/whowas.rs @@ -0,0 +1,52 @@ +use super::*; + +const DEFAULT_COUNT: usize = 8; // Arbitrary value, that happens to match the capacity of + // historic_nick_users + +#[command_handler("WHOWAS")] +/// Syntax: WHOIS [] +fn whowas_handler( + network: &Network, + response: &dyn CommandResponse, + source: UserSource, + server: &ClientServer, + target: Nickname, + count: Option, +) -> CommandResult { + // "If given, SHOULD be a positive number. Otherwise, a full search is done." + let count = match count { + None | Some(0) => DEFAULT_COUNT, + Some(count) => count.try_into().unwrap_or(usize::MAX), + }; + let historic_users: Vec<_> = network + .historic_users_by_nick(&target) + .cloned() + .unwrap_or_default() + .into_iter() + .take(count) + .collect(); + + if historic_users.is_empty() { + response.numeric(make_numeric!(WasNoSuchNick, &target)); + } else { + for historic_user in historic_users { + let user: sable_network::network::wrapper::User<'_> = + wrapper::ObjectWrapper::wrap(network, &historic_user.user); + response.numeric(make_numeric!(WhowasUser, &historic_user)); + + if let Ok(Some(account)) = user.account() { + response.numeric(make_numeric!(WhoisAccount, &target, &account.name())); + } + + if server.policy().can_see_connection_info(&source, &user) { + for conn in user.connections() { + response.numeric(make_numeric!(WhoisServer, &user, &conn.server()?)); + response.numeric(make_numeric!(WhoisHost, &user, conn.hostname(), conn.ip())); + } + } + } + } + + response.numeric(make_numeric!(EndOfWhowas, &target)); + Ok(()) +} diff --git a/sable_ircd/src/command/mod.rs b/sable_ircd/src/command/mod.rs index bb49b016..bb76fb38 100644 --- a/sable_ircd/src/command/mod.rs +++ b/sable_ircd/src/command/mod.rs @@ -66,6 +66,7 @@ mod handlers { mod userhost; mod who; mod whois; + mod whowas; // Interim solutions that need refinement mod session; diff --git a/sable_ircd/src/messages/numeric.rs b/sable_ircd/src/messages/numeric.rs index fe7a367b..afba2976 100644 --- a/sable_ircd/src/messages/numeric.rs +++ b/sable_ircd/src/messages/numeric.rs @@ -1,5 +1,6 @@ use super::*; use sable_macros::define_messages; +use sable_network::network::update::HistoricUser; use sable_network::network::wrapper::{Channel, ChannelMode, ListModeEntry, Server, User}; define_messages! { @@ -19,6 +20,8 @@ define_messages! { => "{nick} {user} {host} * :{realname}" }, 312(WhoisServer) => { (nick: &User.nick(), server: &Server.name(), info=server.id()) => "{nick} {server} :{info:?}"}, + 314(WhowasUser) => { (nick: &HistoricUser.nickname, user=nick.user.user, host=nick.user.visible_host, realname=nick.user.realname) + => "{nick} {user} {host} * :{realname}" }, 315(EndOfWho) => { (arg: &str) => "{arg} :End of /WHO list" }, 318(EndOfWhois) => { (user: &User.nick()) => "{user} :End of /WHOIS" }, 319(WhoisChannels) => { (user: &User.nick(), chanlist: &str) @@ -29,7 +32,7 @@ define_messages! { 324(ChannelModeIs) => { (chan: &Channel.name(), modes: &ChannelMode.format()) => "{chan} {modes}" }, - 330(WhoisAccount) => { (nick: &User.nick(), account: &Nickname) + 330(WhoisAccount) => { (nick: &Nickname, account: &Nickname) => "{nick} {account} :is logged in as" }, 331(NoTopic) => { (chan: &Channel.name()) => "{chan} :No topic is set"}, @@ -48,6 +51,8 @@ define_messages! { => "{is_pub} {chan} :{content}" }, 366(EndOfNames) => { (chname: &str) => "{chname} :End of names list" }, + 369(EndOfWhowas) => { (nick: &Nickname) => "{nick} :End of /WHOWAS" }, + 256(AdminMe) => { (server_name: &ServerName) => "{server_name} :Administrative Info"}, 257(AdminLocation1) => { (server_location: &str) => ":{server_location}" }, 258(AdminLocation2) => { (admin_info: &str) => ":{admin_info}" }, @@ -65,6 +70,7 @@ define_messages! { 402(NoSuchServer) => { (server_name: &ServerName) => "{server_name} :No such server" }, 403(NoSuchChannel) => { (chname: &ChannelName) => "{chname} :No such channel" }, 404(CannotSendToChannel) => { (chan: &ChannelName) => "{chan} :Cannot send to channel" }, + 406(WasNoSuchNick) => { (nick: &Nickname) => "{nick} :There was no such nickname" }, 410(InvalidCapCmd) => { (subcommand: &str) => "{subcommand} :Invalid CAP command" }, 412(NoTextToSend) => { () => ":No text to send" }, 421(UnknownCommand) => { (command: &str) => "{command} :Unknown command" }, diff --git a/sable_network/src/network/network/accessors.rs b/sable_network/src/network/network/accessors.rs index ecef5843..070c693a 100644 --- a/sable_network/src/network/network/accessors.rs +++ b/sable_network/src/network/network/accessors.rs @@ -1,5 +1,8 @@ +use std::collections::VecDeque; + use super::{LookupError, LookupResult, Network}; use crate::network::event::*; +use crate::network::network::HistoricUser; use crate::network::state_utils; use crate::prelude::*; @@ -103,6 +106,13 @@ impl Network { }) } + /// Remove a user from nick bindings and add it to historical users for that nick + + /// Return a nickname binding for the given nick. + pub fn historic_users_by_nick(&self, nick: &Nickname) -> Option<&VecDeque> { + self.historic_nick_users.get(nick) + } + /// Look up a channel by ID pub fn channel(&self, id: ChannelId) -> LookupResult { self.channels.get(&id).ok_or(NoSuchChannel(id)).wrap(self) diff --git a/sable_network/src/network/network/mod.rs b/sable_network/src/network/network/mod.rs index acfddde3..c9e7d19d 100644 --- a/sable_network/src/network/network/mod.rs +++ b/sable_network/src/network/network/mod.rs @@ -1,6 +1,7 @@ //! Defines the [Network] object. use crate::network::event::*; +use crate::network::network::HistoricUser; use crate::network::update::*; use crate::prelude::*; @@ -9,7 +10,7 @@ use sable_macros::dispatch_event; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::sync::OnceLock; @@ -43,6 +44,8 @@ pub struct Network { #[serde_as(as = "Vec<(_,_)>")] nick_bindings: HashMap, #[serde_as(as = "Vec<(_,_)>")] + historic_nick_users: HashMap>, + #[serde_as(as = "Vec<(_,_)>")] users: HashMap, #[serde_as(as = "Vec<(_,_)>")] user_connections: HashMap, @@ -103,6 +106,7 @@ impl Network { pub fn new(config: config::NetworkConfig) -> Network { let net = Network { nick_bindings: HashMap::new(), + historic_nick_users: HashMap::new(), users: HashMap::new(), user_connections: HashMap::new(), diff --git a/sable_network/src/network/network/user_state.rs b/sable_network/src/network/network/user_state.rs index 69e5f0d9..0dffafc3 100644 --- a/sable_network/src/network/network/user_state.rs +++ b/sable_network/src/network/network/user_state.rs @@ -13,6 +13,8 @@ impl Network { updates: &dyn NetworkUpdateReceiver, ) { if let Some(user) = self.users.remove(&id) { + let mut historic_user = self.translate_historic_user(user.clone()); + // First remove the user's memberships and connections let removed_memberships = self .memberships @@ -28,6 +30,14 @@ impl Network { let removed_nickname = if let Ok(binding) = self.nick_binding_for_user(user.id) { let nick = binding.nick(); self.nick_bindings.remove(&nick); + let historic_nick_users = + self.historic_nick_users.entry(nick.clone()).or_insert_with( + || VecDeque::with_capacity(8), // arbitrary power of two + ); + if historic_nick_users.len() == historic_nick_users.capacity() { + historic_nick_users.pop_back(); + } + historic_nick_users.push_front(historic_user.clone()); nick } else { state_utils::hashed_nick_for(user.id) @@ -43,16 +53,11 @@ impl Network { ); } + historic_user.nickname = removed_nickname; updates.notify( update::UserQuit { // We can't use `translate_historic_user` because we've already removed the nick binding - user: HistoricUser { - account: user - .account - .and_then(|id| self.account(id).ok().map(|acc| acc.name())), - user, - nickname: removed_nickname, - }, + user: historic_user, nickname: removed_nickname, message, memberships: removed_memberships, diff --git a/sable_network/src/network/tests/event_application.rs b/sable_network/src/network/tests/event_application.rs index 3d4e45ce..a4962615 100644 --- a/sable_network/src/network/tests/event_application.rs +++ b/sable_network/src/network/tests/event_application.rs @@ -1,5 +1,6 @@ use super::fixtures::*; use crate::prelude::*; +use serde_json::Value; use std::str::FromStr; #[test] @@ -10,7 +11,12 @@ fn add_and_remove_user() { builder.add_user(nick); let user_id = builder.net.user_by_nick(&nick).unwrap().id(); builder.remove_user(user_id); - let modified_net = builder.json_for_compare(); + let mut modified_net = builder.json_for_compare(); + + if let Value::Object(map) = &mut modified_net { + // Empty this array, because adding and removing a user changes it. + map["historic_nick_users"] = Value::Array(Vec::new()); + } assert_eq!(empty_net, modified_net); }