🏡 index : ~doyle/titanirc.git

author Jordan Doyle <jordan@doyle.la> 2023-01-07 22:33:22.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-01-07 22:33:22.0 +00:00:00
commit
65d8bf5b2543f65dc9cb11b04ba0c3f1fdea54cd [patch]
tree
38316667fe3aa120ea99900ad2b2c5918f780e9b
parent
b736bad1812959761fc49fd492f945028d8b5870
download
65d8bf5b2543f65dc9cb11b04ba0c3f1fdea54cd.tar.gz

Implement INVITE command



Diff

 src/channel.rs          | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 src/client.rs           | 19 ++++++++++++++++---
 src/messages.rs         | 16 ++++++++++++++++
 src/server.rs           | 36 ++++++++++++++++++++++++++++++++++--
 src/channel/response.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++
 src/server/response.rs  |  3 ++-
 6 files changed, 176 insertions(+), 17 deletions(-)

diff --git a/src/channel.rs b/src/channel.rs
index 83fd925..f08cb94 100644
--- a/src/channel.rs
+++ a/src/channel.rs
@@ -1,27 +1,33 @@
pub mod response;

use std::collections::HashMap;

use actix::{Actor, Addr, AsyncContext, Context, Handler, MessageResult};
use actix::{
    Actor, ActorFutureExt, Addr, AsyncContext, Context, Handler, MessageResult, ResponseActFuture,
    WrapFuture,
};
use chrono::{DateTime, Utc};
use futures::future::Either;
use irc_proto::{Command, Message};
use tracing::{debug, error, info, instrument, Span};

use crate::{
    channel::response::{ChannelNamesList, ChannelTopic},
    channel::response::{ChannelInviteResult, ChannelNamesList, ChannelTopic},
    client::Client,
    connection::InitiatedConnection,
    messages::{
        Broadcast, ChannelFetchTopic, ChannelJoin, ChannelKickUser, ChannelMemberList,
        ChannelMessage, ChannelPart, ChannelUpdateTopic, ServerDisconnect, UserKickedFromChannel,
        UserNickChange,
        Broadcast, ChannelFetchTopic, ChannelInvite, ChannelJoin, ChannelKickUser,
        ChannelMemberList, ChannelMessage, ChannelPart, ChannelUpdateTopic, FetchClientByNick,
        ServerDisconnect, UserKickedFromChannel, UserNickChange,
    },
    server::Server,
};

/// A channel is an IRC channel (ie. #abc) that multiple users can connect to in order

/// to chat together.

pub struct Channel {
    pub name: String,
    pub server: Addr<Server>,
    pub clients: HashMap<Addr<Client>, InitiatedConnection>,
    pub topic: Option<CurrentChannelTopic>,
}
@@ -257,6 +263,64 @@
        // send the part message to both the parting user and other clients
        msg.client.do_send(message.clone());
        ctx.notify(message);
    }
}

impl Handler<ChannelInvite> for Channel {
    type Result = ResponseActFuture<Self, ChannelInviteResult>;

    #[instrument(parent = &msg.span, skip_all)]
    fn handle(&mut self, msg: ChannelInvite, _ctx: &mut Self::Context) -> Self::Result {
        let Some(source) = self.clients.get(&msg.client) else {
            return Box::pin(futures::future::ready(ChannelInviteResult::NotOnChannel));
        };

        let source = source.to_nick();

        let fut = self
            .server
            .send(FetchClientByNick {
                nick: msg.nick.clone(),
            })
            .into_actor(self)
            .then(|client, this, _ctx| {
                let client = match client.unwrap() {
                    Some(v) if this.clients.contains_key(&v) => {
                        return Either::Left(futures::future::ready(
                            ChannelInviteResult::UserAlreadyOnChannel,
                        ))
                        .into_actor(this);
                    }
                    Some(v) => v,
                    None => {
                        return Either::Left(futures::future::ready(
                            ChannelInviteResult::NoSuchUser,
                        ))
                        .into_actor(this)
                    }
                };

                let channel_name = this.name.to_string();

                Either::Right(async move {
                    client
                        .send(Broadcast {
                            message: Message {
                                tags: None,
                                prefix: Some(source),
                                command: Command::INVITE(msg.nick, channel_name),
                            },
                            span: msg.span,
                        })
                        .await
                        .unwrap();

                    ChannelInviteResult::Successful
                })
                .into_actor(this)
            });

        Box::pin(fut)
    }
}

diff --git a/src/client.rs b/src/client.rs
index 6768949..5c4088e 100644
--- a/src/client.rs
+++ a/src/client.rs
@@ -13,9 +13,9 @@
    channel::Channel,
    connection::{InitiatedConnection, MessageSink},
    messages::{
        Broadcast, ChannelFetchTopic, ChannelJoin, ChannelKickUser, ChannelList, ChannelMemberList,
        ChannelMessage, ChannelPart, ChannelUpdateTopic, FetchClientDetails, ServerDisconnect,
        UserKickedFromChannel, UserNickChange,
        Broadcast, ChannelFetchTopic, ChannelInvite, ChannelJoin, ChannelKickUser, ChannelList,
        ChannelMemberList, ChannelMessage, ChannelPart, ChannelUpdateTopic, FetchClientDetails,
        ServerDisconnect, UserKickedFromChannel, UserNickChange,
    },
    server::Server,
    SERVER_NAME,
@@ -394,7 +394,18 @@

                ctx.spawn(fut);
            }
            Command::INVITE(_, _) => {}
            Command::INVITE(nick, channel) => {
                let Some(channel) = self.channels.get(&channel) else {
                    error!(%channel, "User not connected to channel");
                    return;
                };

                channel.do_send(ChannelInvite {
                    nick,
                    client: ctx.address(),
                    span: Span::current(),
                });
            }
            Command::KICK(channel, users, reason) => {
                let Some(channel) = self.channels.get(&channel) else {
                    error!(%channel, "User not connected to channel");
diff --git a/src/messages.rs b/src/messages.rs
index 1dd8bec..9c4a8aa 100644
--- a/src/messages.rs
+++ a/src/messages.rs
@@ -124,3 +124,19 @@
    pub message: String,
    pub span: Span,
}

/// Invites a user to the channel.

#[derive(Message)]
#[rtype(result = "super::channel::response::ChannelInviteResult")]
pub struct ChannelInvite {
    pub nick: String,
    pub client: Addr<Client>,
    pub span: Span,
}

/// Fetches a client handle by nick from the server.

#[derive(Message)]
#[rtype(result = "Option<Addr<Client>>")]
pub struct FetchClientByNick {
    pub nick: String,
}
diff --git a/src/server.rs b/src/server.rs
index 834d01a..50f0b24 100644
--- a/src/server.rs
+++ a/src/server.rs
@@ -1,8 +1,8 @@
pub mod response;

use std::collections::{HashMap, HashSet};
use std::collections::HashMap;

use actix::{Actor, Addr, Context, Handler, ResponseFuture};
use actix::{Actor, Addr, AsyncContext, Context, Handler, MessageResult, ResponseFuture};
use futures::{stream::FuturesOrdered, TryFutureExt};
use irc_proto::{Command, Message, Prefix, Response};
use tokio_stream::StreamExt;
@@ -11,9 +11,10 @@
use crate::{
    channel::Channel,
    client::Client,
    connection::InitiatedConnection,
    messages::{
        Broadcast, ChannelFetchTopic, ChannelJoin, ChannelList, ChannelMemberList,
        ServerDisconnect, UserConnected, UserNickChange,
        FetchClientByNick, ServerDisconnect, UserConnected, UserNickChange,
    },
    SERVER_NAME,
};
@@ -22,7 +23,7 @@
#[derive(Default)]
pub struct Server {
    channels: HashMap<String, Addr<Channel>>,
    clients: HashSet<Addr<Client>>,
    clients: HashMap<Addr<Client>, InitiatedConnection>,
}

/// Received when a user connects to the server, and sends them the server preamble

@@ -74,7 +75,7 @@
            });
        }

        self.clients.insert(msg.handle);
        self.clients.insert(msg.handle, msg.connection);
    }
}

@@ -94,7 +95,7 @@
    type Result = ResponseFuture<<ChannelJoin as actix::Message>::Result>;

    #[instrument(parent = &msg.span, skip_all)]
    fn handle(&mut self, msg: ChannelJoin, _ctx: &mut Self::Context) -> Self::Result {
    fn handle(&mut self, msg: ChannelJoin, ctx: &mut Self::Context) -> Self::Result {
        let channel = self
            .channels
            .entry(msg.channel_name.clone())
@@ -103,6 +104,7 @@
                    name: msg.channel_name.clone(),
                    clients: HashMap::new(),
                    topic: None,
                    server: ctx.address(),
                }
                .start()
            })
@@ -125,9 +127,29 @@
    #[instrument(parent = &msg.span, skip_all)]
    fn handle(&mut self, msg: UserNickChange, _ctx: &mut Self::Context) -> Self::Result {
        // inform all clients of the nick change
        for client in &self.clients {
        for client in self.clients.keys() {
            client.do_send(msg.clone());
        }

        if let Some(client) = self.clients.get_mut(&msg.client) {
            *client = msg.connection;
            client.nick = msg.new_nick;
        }
    }
}

/// Fetches a client's handle by their nick

impl Handler<FetchClientByNick> for Server {
    type Result = MessageResult<FetchClientByNick>;

    fn handle(&mut self, msg: FetchClientByNick, _ctx: &mut Self::Context) -> Self::Result {
        MessageResult(
            // TODO: need O(1) lookup here
            self.clients
                .iter()
                .find(|(_handle, connection)| connection.nick == msg.nick)
                .map(|v| v.0.clone()),
        )
    }
}

diff --git a/src/channel/response.rs b/src/channel/response.rs
index 4549aa1..98c718e 100644
--- a/src/channel/response.rs
+++ a/src/channel/response.rs
@@ -117,3 +117,48 @@
        ]
    }
}

#[derive(Copy, Clone)]
pub enum ChannelInviteResult {
    Successful,
    NoSuchUser,
    UserAlreadyOnChannel,
    NotOnChannel,
}

impl ChannelInviteResult {
    #[must_use]
    pub fn into_message(
        self,
        invited_user: String,
        channel: String,
        for_user: String,
    ) -> Option<Message> {
        let command = match self {
            Self::Successful => Command::Response(
                Response::RPL_INVITING,
                vec![for_user, invited_user, channel],
            ),
            Self::NoSuchUser => return None,
            Self::UserAlreadyOnChannel => Command::Response(
                Response::ERR_USERONCHANNEL,
                vec![
                    for_user,
                    invited_user,
                    channel,
                    "is already on channel".to_string(),
                ],
            ),
            Self::NotOnChannel => Command::Response(
                Response::ERR_NOTONCHANNEL,
                vec![for_user, channel, "You're not on that channel".to_string()],
            ),
        };

        Some(Message {
            tags: None,
            prefix: Some(Prefix::ServerName(SERVER_NAME.to_string())),
            command,
        })
    }
}
diff --git a/src/server/response.rs b/src/server/response.rs
index 9a68ed8..1e8180a 100644
--- a/src/server/response.rs
+++ a/src/server/response.rs
@@ -1,5 +1,6 @@
use crate::SERVER_NAME;
use irc_proto::{Command, Message, Prefix, Response};

use crate::SERVER_NAME;

#[derive(Default)]
pub struct ChannelList {