🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2024-01-29 1:58:22.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2024-01-29 1:58:22.0 +00:00:00
commit
e8938527e56346fe15e920c4472cfe6a85c28545 [patch]
tree
8119d192a48713d4ac7774a8d9f5907045b11e37
parent
3f29387fd57b56444a82272e53a656f7bdd164b6
download
e8938527e56346fe15e920c4472cfe6a85c28545.tar.gz

Implement WHO command



Diff

 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(-)

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<ChannelMessage> for Channel {
    }
}

impl Handler<ChannelFetchWhoList> for Channel {
    type Result = MessageResult<ChannelFetchWhoList>;

    #[instrument(parent = &msg.span, skip_all)]
    fn handle(&mut self, msg: ChannelFetchWhoList, _ctx: &mut Self::Context) -> Self::Result {
        MessageResult(ChannelWhoList::new(self))
    }
}

impl Handler<ChannelSetMode> 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<Message> {
        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<Broadcast> for Client {
    }
}

/// Retrieves the entire WHO list for the user.
impl Handler<FetchWhoList> for Client {
    type Result = ResponseFuture<<FetchWhoList as actix::Message>::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::<FuturesUnordered<_>>();
        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<FetchClientDetails> for Client {
    type Result = MessageResult<FetchClientDetails>;
@@ -258,19 +283,17 @@ impl Handler<JoinChannelRequest> 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<ListChannelMemberRequest> 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<Result<irc_proto::Message, ProtocolError>> 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<Result<irc_proto::Message, ProtocolError>> 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<Result<irc_proto::Message, ProtocolError>> 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<ConnectionRequest> 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<FetchClientByNick> for Server {
    }
}

impl Handler<FetchWhoList> for Server {
    type Result = ResponseFuture<<FetchWhoList as actix::Message>::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::<FuturesUnordered<_>>();

            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<ChannelList> for Server {
    type Result = ResponseFuture<<ChannelList as actix::Message>::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<crate::channel::response::ChannelWhoList>,
    pub query: String,
}

impl WhoList {
    #[must_use]
    pub fn into_messages(self, for_user: &str) -> Vec<Message> {
        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