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(-)
@@ -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,
};
pub struct Channel {
pub name: String,
pub server: Addr<Server>,
pub clients: HashMap<Addr<Client>, InitiatedConnection>,
pub topic: Option<CurrentChannelTopic>,
}
@@ -257,6 +263,64 @@
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)
}
}
@@ -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");
@@ -124,3 +124,19 @@
pub message: String,
pub span: Span,
}
#[derive(Message)]
#[rtype(result = "super::channel::response::ChannelInviteResult")]
pub struct ChannelInvite {
pub nick: String,
pub client: Addr<Client>,
pub span: Span,
}
#[derive(Message)]
#[rtype(result = "Option<Addr<Client>>")]
pub struct FetchClientByNick {
pub nick: String,
}
@@ -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>,
}
@@ -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 {
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;
}
}
}
impl Handler<FetchClientByNick> for Server {
type Result = MessageResult<FetchClientByNick>;
fn handle(&mut self, msg: FetchClientByNick, _ctx: &mut Self::Context) -> Self::Result {
MessageResult(
self.clients
.iter()
.find(|(_handle, connection)| connection.nick == msg.nick)
.map(|v| v.0.clone()),
)
}
}
@@ -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,
})
}
}
@@ -1,5 +1,6 @@
use crate::SERVER_NAME;
use irc_proto::{Command, Message, Prefix, Response};
use crate::SERVER_NAME;
#[derive(Default)]
pub struct ChannelList {