🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2024-01-29 2:42:44.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2024-01-29 2:42:44.0 +00:00:00
commit
58da12f97a7bea6cc31ecf9715dc84f52fb5990f [patch]
tree
d8af2051f69b0226780a59f382e6e9c2dcd42c34
parent
e8938527e56346fe15e920c4472cfe6a85c28545
download
58da12f97a7bea6cc31ecf9715dc84f52fb5990f.tar.gz

Implement WHOIS



Diff

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

/// Fetches the user's permission for the current channel.
impl Handler<FetchUserPermission> for Channel {
    type Result = MessageResult<FetchUserPermission>;

    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<ChannelMemberList> for Channel {
    type Result = MessageResult<ChannelMemberList>;
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<Broadcast> for Client {
    }
}

/// Retrieves all the channels the user is connected to.
impl Handler<ConnectedChannels> for Client {
    type Result = ResponseFuture<<ConnectedChannels as actix::Message>::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<FetchWhoList> for Client {
    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 {
        let user_id = self.connection.user_id;

@@ -780,7 +813,19 @@ impl StreamHandler<Result<irc_proto::Message, ProtocolError>> 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<String>,
    pub at: chrono::DateTime<Utc>,
}

impl InitiatedConnection {
@@ -97,7 +99,8 @@ impl TryFrom<ConnectionRequest> 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<FetchClientByNick> for Server {
    }
}

impl Handler<FetchWhois> for Server {
    type Result = ResponseFuture<<FetchWhois as actix::Message>::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<FetchWhoList> for Server {
    type Result = ResponseFuture<<FetchWhoList as actix::Message>::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<InitiatedConnection>,
    pub channels: Vec<(Permission, String)>,
}

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