🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2023-01-07 14:35:02.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-01-07 14:35:02.0 +00:00:00
commit
f44f6610ba5df7040e3b6e88e6724af3e804621e [patch]
tree
7f9ef507aa4dab13d2fad63cea2406a6e225ea92
parent
11689f89def5291ccb9f25cdd0415ace1760f331
download
f44f6610ba5df7040e3b6e88e6724af3e804621e.tar.gz

Implement NAMES command



Diff

 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<ChannelList> 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<String>,
    pub nick_list: Vec<String>,
}

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<Message> {
        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<FetchClientDetails> 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<JoinChannelRequest> 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<ListChannelMemberRequest> for Client {
    type Result = ResponseActFuture<Self, ()>;

    #[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<UserNickChange> for Client {
@@ -263,11 +302,7 @@ impl StreamHandler<Result<irc_proto::Message, ProtocolError>> 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<Result<irc_proto::Message, ProtocolError>> 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<Result<irc_proto::Message, ProtocolError>> for Client {
    }
}

pub fn parse_channel_name_list(s: &str) -> Vec<String> {
    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<ProtocolError> for Client {
    #[instrument(parent = &self.span, skip_all)]
@@ -372,10 +428,18 @@ impl WriteHandler<ProtocolError> 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<String>,
    span: Span,
}

/// A [`Client`] internal self-notification to schedule channel joining
#[derive(actix::Message, Debug)]
#[rtype(result = "()")]
pub struct JoinChannelRequest {
struct JoinChannelRequest {
    channels: Vec<String>,
    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<crate::connection::InitiatedConnection>")]
#[rtype(result = "super::channel::response::ChannelNamesList")]
pub struct ChannelList {
    pub span: Span,
}