Skip to content

Commit

Permalink
Implement WHOIS
Browse files Browse the repository at this point in the history
  • Loading branch information
w4 committed Jan 29, 2024
1 parent e893852 commit 58da12f
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 13 deletions.
11 changes: 10 additions & 1 deletion src/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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>;
Expand Down
2 changes: 1 addition & 1 deletion src/channel/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
53 changes: 49 additions & 4 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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, _) => {
Expand Down
7 changes: 5 additions & 2 deletions src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -97,7 +99,8 @@ impl TryFrom<ConnectionRequest> for InitiatedConnection {
real_name,
user_id,
capabilities,
presence: true,
away: None,
at: Utc::now(),
})
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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(
Expand Down Expand Up @@ -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")]
Expand Down
38 changes: 34 additions & 4 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use actix::{
use actix_rt::Arbiter;
use clap::crate_version;
use futures::{
future,
stream::{FuturesOrdered, FuturesUnordered},
TryFutureExt,
};
Expand All @@ -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,
};

Expand Down Expand Up @@ -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>;

Expand Down
84 changes: 83 additions & 1 deletion src/server/response.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down

0 comments on commit 58da12f

Please sign in to comment.