use chrono::{DateTime, TimeZone, Utc}; use irc_proto::{Command, Message, Prefix, Response}; use itertools::Itertools; use crate::{ channel::permissions::Permission, connection::InitiatedConnection, host_mask::HostMask, persistence::events::ServerListBanEntry, server::Server, SERVER_NAME, }; pub struct Whois { pub query: String, pub conn: Option, pub channels: Vec<(Permission, String)>, } impl IntoProtocol for Whois { fn into_messages(self, for_user: &str) -> Vec { macro_rules! msg { ($response:ident, $($payload:expr),*) => { Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::$response, vec![for_user.to_string(), $($payload),*], ), } }; ($response:literal, $($payload:expr),*) => { Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Raw( format!("{:03}", $response), vec![for_user.to_string(), $($payload),*], ), } }; } let Some(conn) = self.conn else { return vec![msg!(ERR_NOSUCHNICK, self.query, "No such nick".to_string())]; }; let channels = self .channels .into_iter() .map(|(perm, channel)| format!("{}{channel}", perm.into_prefix())) .join(" "); // TODO: RPL_WHOISOPERATOR // TODO: RPL_WHOISACTUALLY // TODO: RPL_WHOISSECURE // TODO: fix missing rpl variants let mut out = vec![ msg!( 307, conn.nick.to_string(), "has identified for this nick".to_string() ), // RPL_WHOISREGNICK msg!( RPL_WHOISUSER, conn.nick.to_string(), conn.user.to_string(), conn.cloak, "*".to_string(), conn.real_name ), msg!( RPL_WHOISSERVER, conn.nick.to_string(), SERVER_NAME.to_string(), SERVER_NAME.to_string() ), msg!( RPL_WHOISIDLE, conn.nick.to_string(), "0".to_string(), conn.at.timestamp().to_string(), "seconds idle, signon time".to_string() ), // TODO msg!(RPL_WHOISCHANNELS, conn.nick.to_string(), channels), msg!( 330, conn.nick.to_string(), conn.user.to_string(), "is logged in as".to_string() ), // RPL_WHOISACCOUNT msg!( 378, conn.nick.to_string(), format!( "is connecting from {}@{} {}", conn.user, conn.resolved_host .unwrap_or_else(|| conn.host.ip().to_canonical().to_string()), conn.host.ip().to_canonical() ) ), // RPL_WHOISHOST ]; if !conn.mode.is_empty() { out.push(msg!( 379, conn.nick.to_string(), format!("is using modes {}", conn.mode) )); // RPL_WHOISMODES } if let Some(msg) = conn.away { out.push(msg!(RPL_AWAY, conn.nick.to_string(), msg)); } out.push(msg!( RPL_ENDOFWHOIS, conn.nick.to_string(), "End of /WHOIS list".to_string() )); out } } pub struct NoSuchNick { pub nick: String, } impl IntoProtocol for NoSuchNick { fn into_messages(self, for_user: &str) -> Vec { vec![Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::ERR_NOSUCHNICK, vec![for_user.to_string(), self.nick, "No such nick".to_string()], ), }] } } #[derive(Default)] pub struct WhoList { pub list: Vec, pub query: String, } impl IntoProtocol for WhoList { fn into_messages(self, for_user: &str) -> Vec { let mut out: Vec<_> = self .list .into_iter() .flat_map(|v| v.into_messages(for_user)) .collect(); out.push(Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::RPL_ENDOFWHO, vec![ for_user.to_string(), self.query, "End of WHO list".to_string(), ], ), }); out } } pub struct AdminInfo { pub line1: String, pub line2: String, pub email: String, } impl IntoProtocol for AdminInfo { fn into_messages(self, for_user: &str) -> Vec { macro_rules! msg { ($response:ident, $($payload:expr),*) => { Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::$response, vec![for_user.to_string(), $($payload),*], ), } }; } vec![ msg!( RPL_ADMINME, SERVER_NAME.to_string(), "Administrative info".to_string() ), msg!(RPL_ADMINLOC1, self.line1), msg!(RPL_ADMINLOC2, self.line2), msg!(RPL_ADMINEMAIL, self.email), ] } } pub struct ListUsers { pub current_clients: usize, pub max_clients: usize, pub operators_online: usize, pub channels_formed: usize, } impl IntoProtocol for ListUsers { #[must_use] fn into_messages(self, for_user: &str) -> Vec { macro_rules! msg { ($response:ident, $($payload:expr),*) => { Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::$response, vec![for_user.to_string(), $($payload),*], ), } }; } vec![ msg!( RPL_LUSERCLIENT, format!( "There are {} users and 0 invisible on 1 servers", self.current_clients ) ), msg!( RPL_LUSEROP, "0".to_string(), "operator(s) online".to_string() ), msg!( RPL_LUSERCHANNELS, self.channels_formed.to_string(), "channels formed".to_string() ), msg!( RPL_LUSERME, format!( "I have {} clients and 1 servers", self.current_clients.to_string() ) ), msg!( RPL_LOCALUSERS, self.current_clients.to_string(), self.max_clients.to_string(), format!( "Current local users {}, max {}", self.current_clients, self.max_clients ) ), msg!( RPL_GLOBALUSERS, self.current_clients.to_string(), self.max_clients.to_string(), format!( "Current global users {}, max {}", self.current_clients, self.max_clients ) ), ] } } #[derive(Default)] pub struct Motd { pub motd: Option, } impl Motd { #[must_use] pub fn new(server: &Server) -> Self { Self { motd: server.config.motd.clone(), } } } impl IntoProtocol for Motd { #[must_use] fn into_messages(self, for_user: &str) -> Vec { let mut out = Vec::new(); if let Some(motd) = self.motd { out.push(Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::RPL_MOTDSTART, vec![ for_user.to_string(), format!("- {SERVER_NAME} Message of the day -"), ], ), }); out.extend(motd.trim().split('\n').map(|v| Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::RPL_MOTD, vec![for_user.to_string(), v.to_string()], ), })); out.push(Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::RPL_ENDOFMOTD, vec![for_user.to_string(), "End of /MOTD command.".to_string()], ), }); } else { out.push(Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::ERR_NOMOTD, vec![for_user.to_string(), "MOTD File is missing".to_string()], ), }); } out } } #[derive(Default)] pub struct ChannelList { pub members: Vec, } impl IntoProtocol for ChannelList { #[must_use] fn into_messages(self, for_user: &str) -> Vec { let mut messages = Vec::with_capacity(self.members.len() + 2); messages.push(Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::RPL_LISTSTART, vec![ for_user.to_string(), "Channel".to_string(), "Users Name".to_string(), ], ), }); for item in self.members { messages.push(Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::RPL_LIST, vec![ for_user.to_string(), item.channel_name, item.client_count.to_string(), item.topic.unwrap_or_default(), ], ), }); } messages.push(Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( Response::RPL_LISTEND, vec![for_user.to_string(), "End of /LIST".to_string()], ), }); messages } } pub struct ChannelListItem { pub channel_name: String, pub client_count: usize, pub topic: Option, } #[derive(Clone, Debug)] pub struct ServerBan { pub mask: HostMask<'static>, pub requester: String, pub reason: Option, pub created: DateTime, pub expires: Option>, } impl From for ServerBan { fn from(value: ServerListBanEntry) -> Self { Self { mask: value.mask, requester: value.requester, reason: Some(value.reason).filter(|v| !v.is_empty()), created: Utc.timestamp_nanos(value.created_timestamp), expires: value.expires_timestamp.map(|v| Utc.timestamp_nanos(v)), } } } impl IntoProtocol for ServerBan { fn into_messages(self, for_user: &str) -> Vec { vec![Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Raw( "216".to_string(), vec![ for_user.to_string(), format!( "{} by {} ({}), created {}, expires {}", self.mask, self.requester, self.reason.as_deref().unwrap_or("no reason given"), self.created, self.expires .map(|v| v.to_string()) .as_deref() .unwrap_or("never") ), ], ), }] } } pub enum ConnectionValidated { Allowed, Reject(String), } pub trait IntoProtocol { #[must_use] fn into_messages(self, for_user: &str) -> Vec; } impl IntoProtocol for () { fn into_messages(self, _for_user: &str) -> Vec { vec![] } } impl IntoProtocol for Option { fn into_messages(self, for_user: &str) -> Vec { self.map_or_else(Vec::new, |v| v.into_messages(for_user)) } } impl IntoProtocol for Result where T: IntoProtocol, E: IntoProtocol, { fn into_messages(self, for_user: &str) -> Vec { match self { Ok(v) => v.into_messages(for_user), Err(e) => e.into_messages(for_user), } } } impl IntoProtocol for Vec { fn into_messages(self, for_user: &str) -> Vec { self.into_iter() .flat_map(|v| v.into_messages(for_user)) .collect() } }