From 58da12f97a7bea6cc31ecf9715dc84f52fb5990f Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 29 Jan 2024 02:42:44 +0000 Subject: [PATCH] Implement WHOIS --- src/channel.rs | 11 ++++++++++- src/channel/response.rs | 2 +- src/client.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++---- src/connection.rs | 7 +++++-- src/messages.rs | 23 +++++++++++++++++++++++ src/server.rs | 38 ++++++++++++++++++++++++++++++++++---- src/server/response.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 205 insertions(+), 13 deletions(-) diff --git a/src/channel.rs b/src/channel.rs index 9ad568d..b7da4cb 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -25,7 +25,7 @@ use crate::{ messages::{ Broadcast, ChannelFetchTopic, ChannelFetchWhoList, ChannelInvite, ChannelJoin, ChannelKickUser, ChannelMemberList, ChannelMessage, ChannelPart, ChannelSetMode, - ChannelUpdateTopic, FetchClientByNick, MessageKind, ServerDisconnect, + ChannelUpdateTopic, FetchClientByNick, FetchUserPermission, MessageKind, ServerDisconnect, UserKickedFromChannel, UserNickChange, }, persistence::{ @@ -115,6 +115,15 @@ impl Handler for Channel { } } +/// Fetches the user's permission for the current channel. +impl Handler for Channel { + type Result = MessageResult; + + fn handle(&mut self, msg: FetchUserPermission, _ctx: &mut Self::Context) -> Self::Result { + MessageResult(self.get_user_permissions(msg.user)) + } +} + /// Sends back a list of users currently connected to the client impl Handler for Channel { type Result = MessageResult; diff --git a/src/channel/response.rs b/src/channel/response.rs index 166dbd9..c8a4bc1 100644 --- a/src/channel/response.rs +++ b/src/channel/response.rs @@ -89,7 +89,7 @@ impl ChannelWhoList { let mut out = Vec::with_capacity(self.nick_list.len()); for (perm, conn) in self.nick_list { - let presence = if conn.presence { "H" } else { "G" }; + let presence = if conn.away.is_some() { "G" } else { "H" }; out.push(Message { tags: None, diff --git a/src/client.rs b/src/client.rs index 62b28bc..8eaecc0 100644 --- a/src/client.rs +++ b/src/client.rs @@ -23,9 +23,10 @@ use crate::{ messages::{ Broadcast, ChannelFetchTopic, ChannelFetchWhoList, ChannelInvite, ChannelJoin, ChannelKickUser, ChannelList, ChannelMemberList, ChannelMessage, ChannelPart, - ChannelSetMode, ChannelUpdateTopic, FetchClientDetails, FetchWhoList, MessageKind, - PrivateMessage, ServerAdminInfo, ServerDisconnect, ServerFetchMotd, ServerListUsers, - UserKickedFromChannel, UserNickChange, UserNickChangeInternal, + ChannelSetMode, ChannelUpdateTopic, ConnectedChannels, FetchClientDetails, + FetchUserPermission, FetchWhoList, FetchWhois, MessageKind, PrivateMessage, + ServerAdminInfo, ServerDisconnect, ServerFetchMotd, ServerListUsers, UserKickedFromChannel, + UserNickChange, UserNickChangeInternal, }, persistence::{ events::{ @@ -215,10 +216,42 @@ impl Handler for Client { } } +/// Retrieves all the channels the user is connected to. +impl Handler for Client { + type Result = ResponseFuture<::Result>; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ConnectedChannels, _ctx: &mut Self::Context) -> Self::Result { + let span = Span::current(); + let user_id = self.connection.user_id; + + let fut = self.channels.iter().map(move |(channel_name, handle)| { + let span = span.clone(); + let channel_name = channel_name.to_string(); + let handle = handle.clone(); + + async move { + let permission = handle + .send(FetchUserPermission { + span, + user: user_id, + }) + .await + .unwrap(); + + (permission, channel_name) + } + }); + + Box::pin(future::join_all(fut)) + } +} + /// Retrieves the entire WHO list for the user. impl Handler for Client { type Result = ResponseFuture<::Result>; + #[instrument(parent = &msg.span, skip_all)] fn handle(&mut self, msg: FetchWhoList, _ctx: &mut Self::Context) -> Self::Result { let user_id = self.connection.user_id; @@ -780,7 +813,19 @@ impl StreamHandler> for Client { }); ctx.spawn(fut); } - Command::WHOIS(_, _) => {} + Command::WHOIS(Some(query), _) => { + let span = Span::current(); + let fut = self + .server + .send(FetchWhois { span, query }) + .into_actor(self) + .map(|result, this, _ctx| { + for message in result.unwrap().into_messages(&this.connection.nick) { + this.writer.write(message); + } + }); + ctx.spawn(fut); + } Command::WHOWAS(_, _, _) => {} Command::KILL(_, _) => {} Command::PING(v, _) => { diff --git a/src/connection.rs b/src/connection.rs index badd41c..2181b42 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -11,6 +11,7 @@ use std::{ use actix::{io::FramedWrite, Actor, Addr}; use bitflags::bitflags; +use chrono::Utc; use const_format::concatcp; use futures::{SinkExt, TryStreamExt}; use irc_proto::{ @@ -58,7 +59,8 @@ pub struct InitiatedConnection { pub real_name: String, pub user_id: UserId, pub capabilities: Capability, - pub presence: bool, + pub away: Option, + pub at: chrono::DateTime, } impl InitiatedConnection { @@ -97,7 +99,8 @@ impl TryFrom for InitiatedConnection { real_name, user_id, capabilities, - presence: true, + away: None, + at: Utc::now(), }) } } diff --git a/src/messages.rs b/src/messages.rs index 3aef3f6..89b573f 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -47,6 +47,13 @@ pub struct UserNickChange { pub span: Span, } +/// List all the channels a user is connected to +#[derive(Message, Clone)] +#[rtype(result = "Vec<(crate::channel::permissions::Permission, String)>")] +pub struct ConnectedChannels { + pub span: Span, +} + /// Fetches all the channels visible to the user. #[derive(Message, Clone)] #[rtype(result = "super::server::response::ChannelList")] @@ -62,6 +69,14 @@ pub struct FetchWhoList { pub query: String, } +/// Fetches the WHOIS for the given query. +#[derive(Message, Clone)] +#[rtype(result = "super::server::response::Whois")] +pub struct FetchWhois { + pub span: Span, + pub query: String, +} + /// Sent when the user attempts to join a channel. #[derive(Message)] #[rtype( @@ -90,6 +105,14 @@ pub struct ChannelMemberList { pub span: Span, } +/// Retrieves the list of users currently in a channel. +#[derive(Message)] +#[rtype(result = "crate::channel::permissions::Permission")] +pub struct FetchUserPermission { + pub span: Span, + pub user: UserId, +} + /// Retrieves the current channel topic. #[derive(Message)] #[rtype(result = "super::channel::response::ChannelTopic")] diff --git a/src/server.rs b/src/server.rs index acc3e9f..3aa2cbb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,6 +9,7 @@ use actix::{ use actix_rt::Arbiter; use clap::crate_version; use futures::{ + future, stream::{FuturesOrdered, FuturesUnordered}, TryFutureExt, }; @@ -24,12 +25,12 @@ use crate::{ connection::InitiatedConnection, messages::{ Broadcast, ChannelFetchTopic, ChannelFetchWhoList, ChannelJoin, ChannelList, - ChannelMemberList, FetchClientByNick, FetchWhoList, MessageKind, PrivateMessage, - ServerAdminInfo, ServerDisconnect, ServerFetchMotd, ServerListUsers, UserConnected, - UserNickChange, UserNickChangeInternal, + ChannelMemberList, ConnectedChannels, FetchClientByNick, FetchWhoList, FetchWhois, + MessageKind, PrivateMessage, ServerAdminInfo, ServerDisconnect, ServerFetchMotd, + ServerListUsers, UserConnected, UserNickChange, UserNickChangeInternal, }, persistence::Persistence, - server::response::{AdminInfo, ListUsers, Motd, WhoList}, + server::response::{AdminInfo, ListUsers, Motd, WhoList, Whois}, SERVER_NAME, }; @@ -229,6 +230,35 @@ impl Handler for Server { } } +impl Handler for Server { + type Result = ResponseFuture<::Result>; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: FetchWhois, _ctx: &mut Self::Context) -> Self::Result { + let Some((handle, conn)) = self.clients.iter().find(|(_, conn)| conn.nick == msg.query) + else { + return Box::pin(future::ready(Whois { + query: msg.query, + conn: None, + channels: vec![], + })); + }; + + let conn = conn.clone(); + let channels = handle.send(ConnectedChannels { + span: Span::current(), + }); + + Box::pin(async move { + Whois { + query: msg.query, + conn: Some(conn), + channels: channels.await.unwrap(), + } + }) + } +} + impl Handler for Server { type Result = ResponseFuture<::Result>; diff --git a/src/server/response.rs b/src/server/response.rs index 3def582..94c5fec 100644 --- a/src/server/response.rs +++ b/src/server/response.rs @@ -1,6 +1,88 @@ use irc_proto::{Command, Message, Prefix, Response}; +use itertools::Itertools; -use crate::{server::Server, SERVER_NAME}; +use crate::{ + channel::permissions::Permission, connection::InitiatedConnection, server::Server, SERVER_NAME, +}; + +pub struct Whois { + pub query: String, + pub conn: Option, + pub channels: Vec<(Permission, String)>, +} + +impl Whois { + #[must_use] + pub 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),*], + ), + } + }; + } + + 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!(RPL_WHOISREGNICK, self.conn.nick.to_string(), "has identified for this nick".to_string()), + msg!( + RPL_WHOISUSER, + conn.nick.to_string(), + conn.user, + "*".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!(RPL_WHOISACCOUNT, self.conn.nick.to_string(), self.conn.user.to_string(), "is logged in as".to_string()), + // msg!(RPL_WHOISHOST, self.conn.nick.to_string(), format!("is connecting from {}@{} {}", self.conn.user, self.conn.host, self.conn.host)), + // msg!(RPL_WHOISMODES, self.conn.nick.to_string(), format!("is using modes {}", self.conn.mode)), + ]; + + 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 + } +} #[derive(Default)] pub struct WhoList { -- libgit2 1.7.2