From 65d8bf5b2543f65dc9cb11b04ba0c3f1fdea54cd Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sat, 7 Jan 2023 22:33:22 +0000 Subject: [PATCH] Implement INVITE command --- src/channel.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- src/channel/response.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ src/client.rs | 19 +++++++++++++++---- src/messages.rs | 16 ++++++++++++++++ src/server.rs | 36 +++++++++++++++++++++++++++++------- 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 +++ b/src/channel.rs @@ -2,26 +2,32 @@ 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, pub clients: HashMap, InitiatedConnection>, pub topic: Option, } @@ -260,6 +266,64 @@ impl Handler for Channel { } } +impl Handler for Channel { + type Result = ResponseActFuture; + + #[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) + } +} + /// Received when a client is disconnecting from the server and broadcasts it to all connected /// users. impl Handler for Channel { diff --git a/src/channel/response.rs b/src/channel/response.rs index 4549aa1..98c718e 100644 --- a/src/channel/response.rs +++ b/src/channel/response.rs @@ -117,3 +117,48 @@ impl ChannelNamesList { ] } } + +#[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 { + 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/client.rs b/src/client.rs index 6768949..5c4088e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -13,9 +13,9 @@ use crate::{ 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 @@ impl StreamHandler> for Client { 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 +++ b/src/messages.rs @@ -124,3 +124,19 @@ pub struct ChannelMessage { 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, + pub span: Span, +} + +/// Fetches a client handle by nick from the server. +#[derive(Message)] +#[rtype(result = "Option>")] +pub struct FetchClientByNick { + pub nick: String, +} diff --git a/src/server.rs b/src/server.rs index 834d01a..50f0b24 100644 --- a/src/server.rs +++ b/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 tracing::{instrument, Span}; 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 @@ use crate::{ #[derive(Default)] pub struct Server { channels: HashMap>, - clients: HashSet>, + clients: HashMap, InitiatedConnection>, } /// Received when a user connects to the server, and sends them the server preamble @@ -74,7 +75,7 @@ impl Handler for Server { }); } - self.clients.insert(msg.handle); + self.clients.insert(msg.handle, msg.connection); } } @@ -94,7 +95,7 @@ impl Handler for Server { type Result = ResponseFuture<::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 @@ impl Handler for Server { name: msg.channel_name.clone(), clients: HashMap::new(), topic: None, + server: ctx.address(), } .start() }) @@ -125,9 +127,29 @@ impl Handler for Server { #[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 for Server { + type Result = MessageResult; + + 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/server/response.rs b/src/server/response.rs index 9a68ed8..1e8180a 100644 --- a/src/server/response.rs +++ b/src/server/response.rs @@ -1,6 +1,7 @@ -use crate::SERVER_NAME; use irc_proto::{Command, Message, Prefix, Response}; +use crate::SERVER_NAME; + #[derive(Default)] pub struct ChannelList { pub members: Vec, -- libgit2 1.7.2