From e8938527e56346fe15e920c4472cfe6a85c28545 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Mon, 29 Jan 2024 01:58:22 +0000 Subject: [PATCH] Implement WHO command --- src/channel.rs | 18 ++++++++++++++---- src/channel/response.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ src/client.rs | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ src/connection.rs | 2 ++ src/messages.rs | 15 +++++++++++++++ src/server.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++++----- src/server/response.rs | 32 ++++++++++++++++++++++++++++++++ text/info.txt | 8 ++++++++ 8 files changed, 252 insertions(+), 33 deletions(-) create mode 100644 text/info.txt diff --git a/src/channel.rs b/src/channel.rs index 61b8f9f..9ad568d 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -17,15 +17,16 @@ use crate::{ permissions::Permission, response::{ ChannelInviteResult, ChannelJoinRejectionReason, ChannelNamesList, ChannelTopic, - MissingPrivileges, + ChannelWhoList, MissingPrivileges, }, }, client::Client, connection::{Capability, InitiatedConnection, UserId}, messages::{ - Broadcast, ChannelFetchTopic, ChannelInvite, ChannelJoin, ChannelKickUser, - ChannelMemberList, ChannelMessage, ChannelPart, ChannelSetMode, ChannelUpdateTopic, - FetchClientByNick, MessageKind, ServerDisconnect, UserKickedFromChannel, UserNickChange, + Broadcast, ChannelFetchTopic, ChannelFetchWhoList, ChannelInvite, ChannelJoin, + ChannelKickUser, ChannelMemberList, ChannelMessage, ChannelPart, ChannelSetMode, + ChannelUpdateTopic, FetchClientByNick, MessageKind, ServerDisconnect, + UserKickedFromChannel, UserNickChange, }, persistence::{ events::{FetchAllUserChannelPermissions, SetUserChannelPermissions}, @@ -196,6 +197,15 @@ impl Handler for Channel { } } +impl Handler for Channel { + type Result = MessageResult; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ChannelFetchWhoList, _ctx: &mut Self::Context) -> Self::Result { + MessageResult(ChannelWhoList::new(self)) + } +} + impl Handler for Channel { type Result = (); diff --git a/src/channel/response.rs b/src/channel/response.rs index 68bc75c..166dbd9 100644 --- a/src/channel/response.rs +++ b/src/channel/response.rs @@ -66,6 +66,55 @@ impl ChannelTopic { } } +pub struct ChannelWhoList { + pub channel_name: String, + pub nick_list: Vec<(Permission, InitiatedConnection)>, +} + +impl ChannelWhoList { + #[must_use] + pub fn new(channel: &Channel) -> Self { + Self { + channel_name: channel.name.to_string(), + nick_list: channel + .clients + .values() + .map(|v| (channel.get_user_permissions(v.user_id), v.clone())) + .collect(), + } + } + + #[must_use] + pub fn into_messages(self, for_user: &str) -> Vec { + 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" }; + + out.push(Message { + tags: None, + prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), + command: Command::Response( + Response::RPL_WHOREPLY, + vec![ + for_user.to_string(), + self.channel_name.to_string(), + conn.user, + conn.host.to_string(), + SERVER_NAME.to_string(), + conn.nick, + format!("{presence}{}", perm.into_prefix()), // TODO: user modes & server operator + "0".to_string(), + conn.real_name, + ], + ), + }); + } + + out + } +} + pub struct ChannelNamesList { pub channel_name: String, pub nick_list: Vec<(Permission, InitiatedConnection)>, diff --git a/src/client.rs b/src/client.rs index 2518045..62b28bc 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2,12 +2,12 @@ use std::{collections::HashMap, time::Duration}; use actix::{ fut::wrap_future, io::WriteHandler, Actor, ActorContext, ActorFuture, ActorFutureExt, Addr, - AsyncContext, Context, Handler, MessageResult, ResponseActFuture, Running, StreamHandler, - WrapFuture, + AsyncContext, Context, Handler, MessageResult, ResponseActFuture, ResponseFuture, Running, + StreamHandler, WrapFuture, }; use chrono::{DateTime, SecondsFormat, Utc}; use clap::{crate_name, crate_version}; -use futures::FutureExt; +use futures::{future, stream::FuturesUnordered, FutureExt, StreamExt}; use irc_proto::{ error::ProtocolError, message::Tag, ChannelExt, Command, Message, Prefix, Response, }; @@ -21,11 +21,11 @@ use crate::{ NickNotOwnedByUser, }, messages::{ - Broadcast, ChannelFetchTopic, ChannelInvite, ChannelJoin, ChannelKickUser, ChannelList, - ChannelMemberList, ChannelMessage, ChannelPart, ChannelSetMode, ChannelUpdateTopic, - FetchClientDetails, MessageKind, PrivateMessage, ServerAdminInfo, ServerDisconnect, - ServerFetchMotd, ServerListUsers, UserKickedFromChannel, UserNickChange, - UserNickChangeInternal, + Broadcast, ChannelFetchTopic, ChannelFetchWhoList, ChannelInvite, ChannelJoin, + ChannelKickUser, ChannelList, ChannelMemberList, ChannelMessage, ChannelPart, + ChannelSetMode, ChannelUpdateTopic, FetchClientDetails, FetchWhoList, MessageKind, + PrivateMessage, ServerAdminInfo, ServerDisconnect, ServerFetchMotd, ServerListUsers, + UserKickedFromChannel, UserNickChange, UserNickChangeInternal, }, persistence::{ events::{ @@ -34,7 +34,7 @@ use crate::{ }, Persistence, }, - server::Server, + server::{response::WhoList, Server}, SERVER_NAME, }; @@ -215,6 +215,31 @@ impl Handler for Client { } } +/// Retrieves the entire WHO list for the user. +impl Handler for Client { + type Result = ResponseFuture<::Result>; + + fn handle(&mut self, msg: FetchWhoList, _ctx: &mut Self::Context) -> Self::Result { + let user_id = self.connection.user_id; + + let futures = self + .channels + .values() + .map(|v| { + v.send(ChannelFetchWhoList { + span: msg.span.clone(), + }) + }) + .collect::>(); + Box::pin(futures.fold(WhoList::default(), move |mut acc, item| { + let mut item = item.unwrap(); + item.nick_list.retain(|(_, conn)| conn.user_id == user_id); + acc.list.push(item); + future::ready(acc) + })) + } +} + /// Returns the client's current nick/connection info. impl Handler for Client { type Result = MessageResult; @@ -258,19 +283,17 @@ impl Handler for Client { span: Span::current(), }); - futures.push( - futures::future::join(channel_handle_fut, channel_messages_fut).map( - move |(handle, messages)| { - (channel_name, handle.unwrap().unwrap(), messages.unwrap()) - }, - ), - ); + futures.push(future::join(channel_handle_fut, channel_messages_fut).map( + move |(handle, messages)| { + (channel_name, handle.unwrap().unwrap(), messages.unwrap()) + }, + )); } // await on all the `ChannelJoin` events to the server, and once we get the channel // handles back write them to the server let fut = wrap_future::<_, Self>( - futures::future::join_all(futures.into_iter()).instrument(Span::current()), + future::join_all(futures.into_iter()).instrument(Span::current()), ) .map(|result, this, _ctx| { for (channel_name, handle, messages) in result { @@ -327,7 +350,7 @@ impl Handler for Client { // await on all the `ChannelMemberList` events to the channels, and once we get the lists back // write them to the client let fut = wrap_future::<_, Self>( - futures::future::join_all(futures.into_iter()).instrument(Span::current()), + future::join_all(futures.into_iter()).instrument(Span::current()), ) .map(|result, this, _ctx| { for list in result { @@ -488,7 +511,9 @@ impl StreamHandler> for Client { span: Span::current(), }); } - Command::UserMODE(_, _) => {} + Command::UserMODE(_, _) => { + // TODO + } Command::QUIT(message) => { // set the user's leave reason and request a shutdown of the actor to close the // connection @@ -717,10 +742,44 @@ impl StreamHandler> for Client { }); ctx.spawn(fut); } - Command::INFO(_) => {} - Command::SERVLIST(_, _) => {} - Command::SQUERY(_, _) => {} - Command::WHO(_, _) => {} + Command::INFO(_) => { + static INFO_STR: &str = include_str!("../text/info.txt"); + for line in INFO_STR.trim().split('\n') { + self.writer.write(Message { + tags: None, + prefix: None, + command: Command::Response( + Response::RPL_INFO, + vec![self.connection.nick.to_string(), line.to_string()], + ), + }); + } + + self.writer.write(Message { + tags: None, + prefix: None, + command: Command::Response( + Response::RPL_ENDOFINFO, + vec![ + self.connection.nick.to_string(), + "End of INFO list".to_string(), + ], + ), + }); + } + Command::WHO(Some(mask), _) => { + let span = Span::current(); + let fut = self + .server + .send(FetchWhoList { span, query: mask }) + .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::WHOIS(_, _) => {} Command::WHOWAS(_, _, _) => {} Command::KILL(_, _) => {} @@ -770,7 +829,7 @@ impl StreamHandler> for Client { Command::BATCH(_, _, _) => {} Command::CHGHOST(_, _) => {} Command::Response(_, _) => {} - v @ _ => self.writer.write(Message { + v => self.writer.write(Message { tags: None, prefix: Some(Prefix::new_from_str(&self.connection.nick)), command: Command::Response( diff --git a/src/connection.rs b/src/connection.rs index 7d5d343..badd41c 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -58,6 +58,7 @@ pub struct InitiatedConnection { pub real_name: String, pub user_id: UserId, pub capabilities: Capability, + pub presence: bool, } impl InitiatedConnection { @@ -96,6 +97,7 @@ impl TryFrom for InitiatedConnection { real_name, user_id, capabilities, + presence: true, }) } } diff --git a/src/messages.rs b/src/messages.rs index cd85bdc..3aef3f6 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -54,6 +54,14 @@ pub struct ChannelList { pub span: Span, } +/// Fetches the WHO list for the given query. +#[derive(Message, Clone)] +#[rtype(result = "super::server::response::WhoList")] +pub struct FetchWhoList { + pub span: Span, + pub query: String, +} + /// Sent when the user attempts to join a channel. #[derive(Message)] #[rtype( @@ -89,6 +97,13 @@ pub struct ChannelFetchTopic { pub span: Span, } +/// Retrieves the WHO list for the channel. +#[derive(Message)] +#[rtype(result = "super::channel::response::ChannelWhoList")] +pub struct ChannelFetchWhoList { + pub span: Span, +} + /// Sets the given modes on a channel. #[derive(Message)] #[rtype(result = "()")] diff --git a/src/server.rs b/src/server.rs index 7926a48..acc3e9f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -8,7 +8,10 @@ use actix::{ }; use actix_rt::Arbiter; use clap::crate_version; -use futures::{stream::FuturesOrdered, TryFutureExt}; +use futures::{ + stream::{FuturesOrdered, FuturesUnordered}, + TryFutureExt, +}; use irc_proto::{Command, Message, Prefix, Response}; use rand::seq::SliceRandom; use tokio_stream::StreamExt; @@ -20,12 +23,13 @@ use crate::{ config::Config, connection::InitiatedConnection, messages::{ - Broadcast, ChannelFetchTopic, ChannelJoin, ChannelList, ChannelMemberList, - FetchClientByNick, MessageKind, PrivateMessage, ServerAdminInfo, ServerDisconnect, - ServerFetchMotd, ServerListUsers, UserConnected, UserNickChange, UserNickChangeInternal, + Broadcast, ChannelFetchTopic, ChannelFetchWhoList, ChannelJoin, ChannelList, + ChannelMemberList, FetchClientByNick, FetchWhoList, MessageKind, PrivateMessage, + ServerAdminInfo, ServerDisconnect, ServerFetchMotd, ServerListUsers, UserConnected, + UserNickChange, UserNickChangeInternal, }, persistence::Persistence, - server::response::{AdminInfo, ListUsers, Motd}, + server::response::{AdminInfo, ListUsers, Motd, WhoList}, SERVER_NAME, }; @@ -225,6 +229,46 @@ impl Handler for Server { } } +impl Handler for Server { + type Result = ResponseFuture<::Result>; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: FetchWhoList, _ctx: &mut Self::Context) -> Self::Result { + if let Some(channel) = self.channels.get(&msg.query).cloned() { + Box::pin(async move { + WhoList { + list: vec![channel + .send(ChannelFetchWhoList { span: msg.span }) + .await + .unwrap()], + query: msg.query, + } + }) + } else { + let futures = self + .clients + .iter() + .filter(|(_, conn)| conn.nick == msg.query) + .map(|(client, _)| { + client.send(FetchWhoList { + span: msg.span.clone(), + query: String::new(), + }) + }) + .collect::>(); + + let init = WhoList { + query: msg.query, + list: Vec::new(), + }; + Box::pin(futures.fold(init, |mut acc, item| { + acc.list.extend(item.unwrap().list); + acc + })) + } + } +} + impl Handler for Server { type Result = ResponseFuture<::Result>; diff --git a/src/server/response.rs b/src/server/response.rs index 3960d46..3def582 100644 --- a/src/server/response.rs +++ b/src/server/response.rs @@ -2,6 +2,38 @@ use irc_proto::{Command, Message, Prefix, Response}; use crate::{server::Server, SERVER_NAME}; +#[derive(Default)] +pub struct WhoList { + pub list: Vec, + pub query: String, +} + +impl WhoList { + #[must_use] + pub 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, diff --git a/text/info.txt b/text/info.txt new file mode 100644 index 0000000..9b00a25 --- /dev/null +++ b/text/info.txt @@ -0,0 +1,8 @@ + _____ _____ _____ ___ _ _ ___________ ___________ +|_ _|_ _|_ _/ _ \ | \ | |_ _| ___ \/ __ \ _ \ + | | | | | |/ /_\ \| \| | | | | |_/ /| / \/ | | | + | | | | | || _ || . ` | | | | / | | | | | | + | | _| |_ | || | | || |\ |_| |_| |\ \ | \__/\ |/ / + \_/ \___/ \_/\_| |_/\_| \_/\___/\_| \_| \____/___/ + + https://github.com/mitborg/titanirc -- libgit2 1.7.2