From f44f6610ba5df7040e3b6e88e6724af3e804621e Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 7 Jan 2023 14:35:02 +0000 Subject: [PATCH] Implement NAMES command --- src/channel.rs | 6 +++--- src/channel/response.rs | 18 +++++++++++++++--- src/client.rs | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ src/connection.rs | 1 + src/messages.rs | 2 +- 5 files changed, 96 insertions(+), 19 deletions(-) diff --git a/src/channel.rs b/src/channel.rs index 1adc303..375b9e7 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -1,4 +1,4 @@ -mod response; +pub mod response; use std::collections::HashMap; @@ -12,9 +12,9 @@ use crate::{ connection::InitiatedConnection, messages::{ Broadcast, ChannelJoin, ChannelList, ChannelMessage, ChannelPart, ServerDisconnect, + UserNickChange, }, }; -use crate::messages::UserNickChange; /// A channel is an IRC channel (ie. #abc) that multiple users can connect to in order /// to chat together. @@ -45,7 +45,7 @@ impl Handler for Channel { #[instrument(parent = &msg.span, skip_all)] fn handle(&mut self, msg: ChannelList, _ctx: &mut Self::Context) -> Self::Result { - MessageResult(self.clients.values().cloned().collect()) + MessageResult(ChannelNamesList::new(self)) } } diff --git a/src/channel/response.rs b/src/channel/response.rs index 6163f47..087e854 100644 --- a/src/channel/response.rs +++ b/src/channel/response.rs @@ -8,6 +8,7 @@ pub struct ChannelTopic { } impl ChannelTopic { + #[must_use] pub fn new(channel: &Channel) -> Self { Self { channel_name: channel.name.to_string(), @@ -15,8 +16,9 @@ impl ChannelTopic { } } + #[must_use] pub fn into_message(self, for_user: String) -> Message { - irc_proto::Message { + Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( @@ -29,10 +31,11 @@ impl ChannelTopic { pub struct ChannelNamesList { channel_name: String, - nick_list: Vec, + pub nick_list: Vec, } impl ChannelNamesList { + #[must_use] pub fn new(channel: &Channel) -> Self { Self { channel_name: channel.name.to_string(), @@ -44,9 +47,18 @@ impl ChannelNamesList { } } + #[must_use] + pub const fn empty(channel_name: String) -> Self { + Self { + channel_name, + nick_list: vec![], + } + } + + #[must_use] pub fn into_messages(self, for_user: String) -> Vec { vec![ - irc_proto::Message { + Message { tags: None, prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())), command: Command::Response( diff --git a/src/client.rs b/src/client.rs index 9485a6b..14c6d1d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,14 +7,14 @@ use actix::{ use futures::FutureExt; use irc_proto::{error::ProtocolError, ChannelExt, Command, Message}; use tokio::time::Instant; -use tracing::{error, info_span, instrument, warn, Instrument, Span, debug}; +use tracing::{debug, error, info_span, instrument, warn, Instrument, Span}; use crate::{ channel::Channel, connection::{InitiatedConnection, MessageSink}, messages::{ - Broadcast, ChannelJoin, ChannelMessage, ChannelPart, FetchClientDetails, ServerDisconnect, - UserNickChange, + Broadcast, ChannelJoin, ChannelList, ChannelMessage, ChannelPart, FetchClientDetails, + ServerDisconnect, UserNickChange, }, server::Server, SERVER_NAME, @@ -133,7 +133,7 @@ impl Handler for Client { } /// A self-message from the Client's [`StreamHandler`] implementation when the user -/// sends a request command out. +/// sends a join command out. /// /// This will block the user from performing any actions until they're connected to the /// channel due to us awaiting on the join handles. @@ -180,6 +180,45 @@ impl Handler for Client { } } +/// A self-message from the Client's [`StreamHandler`] implementation when the user +/// sends a request for each channel's member list. +impl Handler for Client { + type Result = ResponseActFuture; + + #[instrument(parent = &msg.span, skip_all)] + fn handle(&mut self, msg: ListChannelMemberRequest, _ctx: &mut Self::Context) -> Self::Result { + let mut futures = Vec::with_capacity(msg.channels.len()); + + // loop over all channels the user is connected to and fetch their members + for (channel_name, handle) in &self.channels { + if !msg.channels.contains(channel_name) { + continue; + } + + futures.push(handle.send(ChannelList { + span: Span::current(), + })); + } + + // await on all the `ChannelList` 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()), + ) + .map(|result, this, _ctx| { + for list in result { + let list = list.unwrap(); + + for message in list.into_messages(this.connection.nick.clone()) { + this.writer.write(message); + } + } + }); + + Box::pin(fut) + } +} + /// A message received from the root server to indicate that another known user has changed their /// nick impl Handler for Client { @@ -263,11 +302,7 @@ impl StreamHandler> for Client { Command::SQUIT(_, _) => {} Command::JOIN(channel_names, _passwords, _real_name) => { // split the list of channel names... - let channels = channel_names - .split(',') - .filter(|v| !v.is_empty()) - .map(ToString::to_string) - .collect(); + let channels = parse_channel_name_list(&channel_names); // ...and send a self-notification to schedule those joins ctx.notify(JoinChannelRequest { @@ -290,7 +325,21 @@ impl StreamHandler> for Client { } Command::ChannelMODE(_, _) => {} Command::TOPIC(_, _) => {} - Command::NAMES(_, _) => {} + Command::NAMES(channel_names, _) => { + // split the list of channel names... + let channels = parse_channel_name_list(channel_names.as_deref().unwrap_or("")); + + if channels.is_empty() { + warn!("Client didn't request names for a particular channel"); + return; + } + + // ...and send a self-notification to request each channel for their list + ctx.notify(ListChannelMemberRequest { + channels, + span: Span::current(), + }); + } Command::LIST(_, _) => {} Command::INVITE(_, _) => {} Command::KICK(_, _, _) => {} @@ -363,6 +412,13 @@ impl StreamHandler> for Client { } } +pub fn parse_channel_name_list(s: &str) -> Vec { + s.split(',') + .filter(|v| !v.is_empty()) + .map(ToString::to_string) + .collect() +} + /// Sent to us by actix whenever we fail to write a message to the client's outgoing tcp stream impl WriteHandler for Client { #[instrument(parent = &self.span, skip_all)] @@ -372,10 +428,18 @@ impl WriteHandler for Client { } } -/// An [`Client`] internal self-notification to schedule channel joining +/// A [`Client`] internal self-notification to grab a list of users in each channel +#[derive(actix::Message, Debug)] +#[rtype(result = "()")] +struct ListChannelMemberRequest { + channels: Vec, + span: Span, +} + +/// A [`Client`] internal self-notification to schedule channel joining #[derive(actix::Message, Debug)] #[rtype(result = "()")] -pub struct JoinChannelRequest { +struct JoinChannelRequest { channels: Vec, span: Span, } diff --git a/src/connection.rs b/src/connection.rs index 6a2205d..df7d9d9 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -78,6 +78,7 @@ pub async fn negotiate_client_connection( request.mode = Some(mode); request.real_name = Some(real_name); } + Command::AUTHENTICATE(_) => {} Command::CAP(_, _, _, _) => {} _ => { warn!(?msg, "Client sent unknown command during negotiation"); diff --git a/src/messages.rs b/src/messages.rs index 0f1e965..7fb809e 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -54,7 +54,7 @@ pub struct ChannelPart { /// Retrieves the list of users currently in a channel. #[derive(Message)] -#[rtype(result = "Vec")] +#[rtype(result = "super::channel::response::ChannelNamesList")] pub struct ChannelList { pub span: Span, } -- libgit2 1.7.2