forked from lavina/lavina
irc: implement WHOIS command (#43)
Reviewed-on: lavina/lavina#43 Co-authored-by: homycdev <abdulkhamid98@gmail.com> Co-committed-by: homycdev <abdulkhamid98@gmail.com>
This commit is contained in:
parent
adf1d8c14c
commit
abe9a26925
|
@ -8,6 +8,7 @@
|
||||||
//! A player actor is a serial handler of commands from a single player. It is preferable to run all per-player validations in the player actor,
|
//! A player actor is a serial handler of commands from a single player. It is preferable to run all per-player validations in the player actor,
|
||||||
//! so that they don't overload the room actor.
|
//! so that they don't overload the room actor.
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
@ -124,6 +125,15 @@ impl PlayerConnection {
|
||||||
self.player_handle.send(ActorCommand::ClientCommand(cmd, self.connection_id.clone())).await;
|
self.player_handle.send(ActorCommand::ClientCommand(cmd, self.connection_id.clone())).await;
|
||||||
Ok(deferred.await?)
|
Ok(deferred.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handler in [Player::check_user_existence].
|
||||||
|
#[tracing::instrument(skip(self), name = "PlayerConnection::check_user_existence")]
|
||||||
|
pub async fn check_user_existence(&self, recipient: PlayerId) -> Result<GetInfoResult> {
|
||||||
|
let (promise, deferred) = oneshot();
|
||||||
|
let cmd = ClientCommand::GetInfo { recipient, promise };
|
||||||
|
self.player_handle.send(ActorCommand::ClientCommand(cmd, self.connection_id.clone())).await;
|
||||||
|
Ok(deferred.await?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle to a player actor.
|
/// Handle to a player actor.
|
||||||
|
@ -200,6 +210,15 @@ pub enum ClientCommand {
|
||||||
body: Str,
|
body: Str,
|
||||||
promise: Promise<()>,
|
promise: Promise<()>,
|
||||||
},
|
},
|
||||||
|
GetInfo {
|
||||||
|
recipient: PlayerId,
|
||||||
|
promise: Promise<GetInfoResult>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GetInfoResult {
|
||||||
|
UserExists,
|
||||||
|
UserDoesntExist,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum JoinResult {
|
pub enum JoinResult {
|
||||||
|
@ -500,6 +519,10 @@ impl Player {
|
||||||
self.send_dialog_message(connection_id, recipient, body).await;
|
self.send_dialog_message(connection_id, recipient, body).await;
|
||||||
let _ = promise.send(());
|
let _ = promise.send(());
|
||||||
}
|
}
|
||||||
|
ClientCommand::GetInfo { recipient, promise } => {
|
||||||
|
let result = self.check_user_existence(recipient).await;
|
||||||
|
let _ = promise.send(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -596,6 +619,15 @@ impl Player {
|
||||||
self.broadcast_update(update, connection_id).await;
|
self.broadcast_update(update, connection_id).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), name = "Player::check_user_existence")]
|
||||||
|
async fn check_user_existence(&self, recipient: PlayerId) -> GetInfoResult {
|
||||||
|
if self.storage.check_user_existence(recipient.as_inner().as_ref()).await.unwrap() {
|
||||||
|
GetInfoResult::UserExists
|
||||||
|
} else {
|
||||||
|
GetInfoResult::UserDoesntExist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Broadcasts an update to all connections except the one with the given id.
|
/// Broadcasts an update to all connections except the one with the given id.
|
||||||
///
|
///
|
||||||
/// This is called after handling a client command.
|
/// This is called after handling a client command.
|
||||||
|
|
|
@ -7,7 +7,7 @@ use anyhow::anyhow;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::sqlite::SqliteConnectOptions;
|
use sqlx::sqlite::SqliteConnectOptions;
|
||||||
use sqlx::{ConnectOptions, Connection, FromRow, Sqlite, SqliteConnection, Transaction};
|
use sqlx::{ConnectOptions, Connection, Execute, FromRow, Sqlite, SqliteConnection, Transaction};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
@ -56,6 +56,17 @@ impl Storage {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), name = "Storage::check_user_existence")]
|
||||||
|
pub async fn check_user_existence(&self, username: &str) -> Result<bool> {
|
||||||
|
let mut executor = self.conn.lock().await;
|
||||||
|
let result: Option<(String,)> = sqlx::query_as("select name from users where name = ?;")
|
||||||
|
.bind(username)
|
||||||
|
.fetch_optional(&mut *executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(result.is_some())
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self), name = "Storage::retrieve_room_by_name")]
|
#[tracing::instrument(skip(self), name = "Storage::retrieve_room_by_name")]
|
||||||
pub async fn retrieve_room_by_name(&self, name: &str) -> Result<Option<StoredRoom>> {
|
pub async fn retrieve_room_by_name(&self, name: &str) -> Result<Option<StoredRoom>> {
|
||||||
let mut executor = self.conn.lock().await;
|
let mut executor = self.conn.lock().await;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
use lavina_core::{player::PlayerConnection, prelude::Str, LavinaCore};
|
||||||
|
use std::future::Future;
|
||||||
|
use tokio::io::AsyncWrite;
|
||||||
|
|
||||||
|
pub struct IrcConnection<'a, T: AsyncWrite + Unpin> {
|
||||||
|
pub server_name: Str,
|
||||||
|
/// client is nick of requester
|
||||||
|
pub client: Str,
|
||||||
|
pub writer: &'a mut T,
|
||||||
|
pub player_connection: &'a mut PlayerConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Handler<T>
|
||||||
|
where
|
||||||
|
T: AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
fn handle(&self, arg: IrcConnection<T>) -> impl Future<Output = anyhow::Result<()>>;
|
||||||
|
}
|
|
@ -28,11 +28,14 @@ use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody};
|
||||||
use proto_irc::user::PrefixedNick;
|
use proto_irc::user::PrefixedNick;
|
||||||
use proto_irc::{Chan, Recipient, Tag};
|
use proto_irc::{Chan, Recipient, Tag};
|
||||||
use sasl::AuthBody;
|
use sasl::AuthBody;
|
||||||
|
|
||||||
mod cap;
|
mod cap;
|
||||||
|
use handler::Handler;
|
||||||
|
mod whois;
|
||||||
|
|
||||||
use crate::cap::Capabilities;
|
use crate::cap::Capabilities;
|
||||||
|
|
||||||
|
mod handler;
|
||||||
|
|
||||||
pub const APP_VERSION: &str = concat!("lavina", "_", env!("CARGO_PKG_VERSION"));
|
pub const APP_VERSION: &str = concat!("lavina", "_", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
@ -806,6 +809,17 @@ async fn handle_incoming_message(
|
||||||
log::warn!("Local chans not supported");
|
log::warn!("Local chans not supported");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ClientMessage::Whois { arg } => {
|
||||||
|
arg.handle(handler::IrcConnection {
|
||||||
|
server_name: config.server_name.clone(),
|
||||||
|
client: user.nickname.clone(),
|
||||||
|
writer,
|
||||||
|
player_connection: user_handle,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
writer.flush().await?;
|
||||||
|
}
|
||||||
ClientMessage::Mode { target } => {
|
ClientMessage::Mode { target } => {
|
||||||
match target {
|
match target {
|
||||||
Recipient::Nick(nickname) => {
|
Recipient::Nick(nickname) => {
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
use lavina_core::{
|
||||||
|
player::{GetInfoResult, PlayerId},
|
||||||
|
prelude::Str,
|
||||||
|
};
|
||||||
|
use proto_irc::{
|
||||||
|
client::command_args::Whois,
|
||||||
|
commands::whois::{
|
||||||
|
error::{ErrNoNicknameGiven431, ErrNoSuchNick401},
|
||||||
|
response::RplEndOfWhois318,
|
||||||
|
},
|
||||||
|
response::{IrcResponseMessage, WriteResponse},
|
||||||
|
};
|
||||||
|
use tokio::io::AsyncWrite;
|
||||||
|
|
||||||
|
use crate::handler::{Handler, IrcConnection};
|
||||||
|
|
||||||
|
impl<T: AsyncWrite + Unpin> Handler<T> for Whois {
|
||||||
|
async fn handle(&self, body: IrcConnection<'_, T>) -> anyhow::Result<()> {
|
||||||
|
match self {
|
||||||
|
Whois::Nick(nick) => handle_nick_target(nick.clone(), body).await?,
|
||||||
|
Whois::TargetNick(_, nick) => handle_nick_target(nick.clone(), body).await?,
|
||||||
|
Whois::EmptyArgs => {
|
||||||
|
let IrcConnection {
|
||||||
|
server_name,
|
||||||
|
mut writer,
|
||||||
|
..
|
||||||
|
} = body;
|
||||||
|
IrcResponseMessage::empty_tags(
|
||||||
|
Some(server_name.clone()),
|
||||||
|
ErrNoNicknameGiven431::new(server_name.clone()),
|
||||||
|
)
|
||||||
|
.write_response(&mut writer)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_nick_target(nick: Str, body: IrcConnection<'_, impl AsyncWrite + Unpin>) -> anyhow::Result<()> {
|
||||||
|
let IrcConnection {
|
||||||
|
server_name,
|
||||||
|
mut writer,
|
||||||
|
client,
|
||||||
|
player_connection,
|
||||||
|
} = body;
|
||||||
|
|
||||||
|
if let GetInfoResult::UserDoesntExist =
|
||||||
|
player_connection.check_user_existence(PlayerId::from(nick.clone())?).await?
|
||||||
|
{
|
||||||
|
IrcResponseMessage::empty_tags(
|
||||||
|
Some(server_name.clone()),
|
||||||
|
ErrNoSuchNick401::new(client.clone(), nick.clone()),
|
||||||
|
)
|
||||||
|
.write_response(&mut writer)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
IrcResponseMessage::empty_tags(
|
||||||
|
Some(server_name.clone()),
|
||||||
|
RplEndOfWhois318::new(client.clone(), nick.clone()),
|
||||||
|
)
|
||||||
|
.write_response(&mut writer)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -383,7 +383,14 @@ async fn scenario_two_users() -> Result<()> {
|
||||||
// The second user should receive the PART message
|
// The second user should receive the PART message
|
||||||
s2.expect(":tester1 PART #test").await?;
|
s2.expect(":tester1 PART #test").await?;
|
||||||
|
|
||||||
|
s1.send("WHOIS tester2").await?;
|
||||||
|
s1.expect(":testserver 318 tester1 tester2 :End of /WHOIS list").await?;
|
||||||
|
|
||||||
stream1.shutdown().await?;
|
stream1.shutdown().await?;
|
||||||
|
s2.send("WHOIS tester3").await?;
|
||||||
|
s2.expect(":testserver 401 tester2 tester3 :No such nick/channel").await?;
|
||||||
|
s2.expect(":testserver 318 tester2 tester3 :End of /WHOIS list").await?;
|
||||||
|
|
||||||
stream2.shutdown().await?;
|
stream2.shutdown().await?;
|
||||||
|
|
||||||
server.shutdown().await?;
|
server.shutdown().await?;
|
||||||
|
|
|
@ -201,7 +201,7 @@ async fn handle_socket(
|
||||||
pin!(termination);
|
pin!(termination);
|
||||||
select! {
|
select! {
|
||||||
biased;
|
biased;
|
||||||
_ = &mut termination =>{
|
_ = &mut termination => {
|
||||||
log::info!("Socket handling was terminated");
|
log::info!("Socket handling was terminated");
|
||||||
return Ok(())
|
return Ok(())
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use nom::combinator::{all_consuming, opt};
|
use nom::combinator::{all_consuming, opt};
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
/// Client-to-server command.
|
/// Client-to-server command.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum ClientMessage {
|
pub enum ClientMessage {
|
||||||
|
@ -42,6 +42,10 @@ pub enum ClientMessage {
|
||||||
Who {
|
Who {
|
||||||
target: Recipient, // aka mask
|
target: Recipient, // aka mask
|
||||||
},
|
},
|
||||||
|
/// WHOIS [<target>] <nick>
|
||||||
|
Whois {
|
||||||
|
arg: command_args::Whois,
|
||||||
|
},
|
||||||
/// `TOPIC <chan> :<topic>`
|
/// `TOPIC <chan> :<topic>`
|
||||||
Topic {
|
Topic {
|
||||||
chan: Chan,
|
chan: Chan,
|
||||||
|
@ -63,6 +67,17 @@ pub enum ClientMessage {
|
||||||
Authenticate(Str),
|
Authenticate(Str),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod command_args {
|
||||||
|
use crate::prelude::Str;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Whois {
|
||||||
|
Nick(Str),
|
||||||
|
TargetNick(Str, Str),
|
||||||
|
EmptyArgs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn client_message(input: &str) -> Result<ClientMessage> {
|
pub fn client_message(input: &str) -> Result<ClientMessage> {
|
||||||
let res = all_consuming(alt((
|
let res = all_consuming(alt((
|
||||||
client_message_capability,
|
client_message_capability,
|
||||||
|
@ -74,6 +89,7 @@ pub fn client_message(input: &str) -> Result<ClientMessage> {
|
||||||
client_message_join,
|
client_message_join,
|
||||||
client_message_mode,
|
client_message_mode,
|
||||||
client_message_who,
|
client_message_who,
|
||||||
|
client_message_whois,
|
||||||
client_message_topic,
|
client_message_topic,
|
||||||
client_message_part,
|
client_message_part,
|
||||||
client_message_privmsg,
|
client_message_privmsg,
|
||||||
|
@ -177,6 +193,31 @@ fn client_message_who(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
Ok((input, ClientMessage::Who { target }))
|
Ok((input, ClientMessage::Who { target }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn client_message_whois(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
|
let (input, _) = tag("WHOIS ")(input)?;
|
||||||
|
let args: Vec<_> = input.split_whitespace().collect();
|
||||||
|
match args.as_slice()[..] {
|
||||||
|
[nick] => Ok((
|
||||||
|
"",
|
||||||
|
ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::Nick(nick.into()),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
[target, nick, ..] => Ok((
|
||||||
|
"",
|
||||||
|
ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::TargetNick(target.into(), nick.into()),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
[] => Ok((
|
||||||
|
"",
|
||||||
|
ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::EmptyArgs,
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn client_message_topic(input: &str) -> IResult<&str, ClientMessage> {
|
fn client_message_topic(input: &str) -> IResult<&str, ClientMessage> {
|
||||||
let (input, _) = tag("TOPIC ")(input)?;
|
let (input, _) = tag("TOPIC ")(input)?;
|
||||||
let (input, chan) = chan(input)?;
|
let (input, chan) = chan(input)?;
|
||||||
|
@ -311,6 +352,7 @@ mod test {
|
||||||
use nonempty::nonempty;
|
use nonempty::nonempty;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_client_message_cap_ls() {
|
fn test_client_message_cap_ls() {
|
||||||
let input = "CAP LS 302";
|
let input = "CAP LS 302";
|
||||||
|
@ -360,6 +402,66 @@ mod test {
|
||||||
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
assert_matches!(result, Ok(result) => assert_eq!(expected, result));
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
|
fn test_client_message_whois() {
|
||||||
|
let test_user = "WHOIS val";
|
||||||
|
let test_user_user = "WHOIS val val";
|
||||||
|
let test_server_user = "WHOIS com.test.server user";
|
||||||
|
let test_user_server = "WHOIS user com.test.server";
|
||||||
|
let test_users_list = "WHOIS user_1,user_2,user_3";
|
||||||
|
let test_server_users_list = "WHOIS com.test.server user_1,user_2,user_3";
|
||||||
|
let test_more_than_two_params = "WHOIS test.server user_1,user_2,user_3 whatever spam";
|
||||||
|
let test_none_none_params = "WHOIS ";
|
||||||
|
|
||||||
|
let res_one_arg = client_message(test_user);
|
||||||
|
let res_user_user = client_message(test_user_user);
|
||||||
|
|
||||||
|
let res_server_user = client_message(test_server_user);
|
||||||
|
let res_user_server = client_message(test_user_server);
|
||||||
|
let res_users_list = client_message(test_users_list);
|
||||||
|
let res_server_users_list = client_message(test_server_users_list);
|
||||||
|
let res_more_than_two_params = client_message(test_more_than_two_params);
|
||||||
|
let res_none_none_params = client_message(test_none_none_params);
|
||||||
|
|
||||||
|
let expected_arg = ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::Nick("val".into()),
|
||||||
|
};
|
||||||
|
let expected_user_user = ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::TargetNick("val".into(), "val".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_server_user = ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::TargetNick("com.test.server".into(), "user".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_user_server = ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::TargetNick("user".into(), "com.test.server".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_user_list = ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::Nick("user_1,user_2,user_3".into()),
|
||||||
|
};
|
||||||
|
let expected_server_user_list = ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::TargetNick("com.test.server".into(), "user_1,user_2,user_3".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_more_than_two_params = ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::TargetNick("test.server".into(), "user_1,user_2,user_3".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_none_none_params = ClientMessage::Whois {
|
||||||
|
arg: command_args::Whois::EmptyArgs,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_matches!(res_one_arg, Ok(result) => assert_eq!(expected_arg, result));
|
||||||
|
assert_matches!(res_user_user, Ok(result) => assert_eq!(expected_user_user, result));
|
||||||
|
assert_matches!(res_server_user, Ok(result) => assert_eq!(expected_server_user, result));
|
||||||
|
assert_matches!(res_user_server, Ok(result) => assert_eq!(expected_user_server, result));
|
||||||
|
assert_matches!(res_users_list, Ok(result) => assert_eq!(expected_user_list, result));
|
||||||
|
assert_matches!(res_server_users_list, Ok(result) => assert_eq!(expected_server_user_list, result));
|
||||||
|
assert_matches!(res_more_than_two_params, Ok(result) => assert_eq!(expected_more_than_two_params, result));
|
||||||
|
assert_matches!(res_none_none_params, Ok(result) => assert_eq!(expected_none_none_params, result))
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
fn test_client_message_user() {
|
fn test_client_message_user() {
|
||||||
let input = "USER SomeNick 8 * :Real Name";
|
let input = "USER SomeNick 8 * :Real Name";
|
||||||
let expected = ClientMessage::User {
|
let expected = ClientMessage::User {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod whois;
|
|
@ -0,0 +1,67 @@
|
||||||
|
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
||||||
|
|
||||||
|
use crate::{prelude::Str, response::WriteResponse};
|
||||||
|
|
||||||
|
/// ErrNoSuchNick401
|
||||||
|
pub struct ErrNoSuchNick401 {
|
||||||
|
client: Str,
|
||||||
|
nick: Str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrNoSuchNick401 {
|
||||||
|
pub fn new(client: Str, nick: Str) -> Self {
|
||||||
|
ErrNoSuchNick401 { client, nick }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ErrNoSuchServer402
|
||||||
|
struct ErrNoSuchServer402 {
|
||||||
|
client: Str,
|
||||||
|
/// target parameter in WHOIS
|
||||||
|
/// example: `/whois <target> <nick>`
|
||||||
|
server_name: Str,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ErrNoNicknameGiven431
|
||||||
|
pub struct ErrNoNicknameGiven431 {
|
||||||
|
client: Str,
|
||||||
|
}
|
||||||
|
impl ErrNoNicknameGiven431 {
|
||||||
|
pub fn new(client: Str) -> Self {
|
||||||
|
ErrNoNicknameGiven431 { client }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteResponse for ErrNoSuchNick401 {
|
||||||
|
async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
||||||
|
writer.write_all(b"401 ").await?;
|
||||||
|
writer.write_all(self.client.as_bytes()).await?;
|
||||||
|
writer.write_all(b" ").await?;
|
||||||
|
writer.write_all(self.nick.as_bytes()).await?;
|
||||||
|
writer.write_all(b" :").await?;
|
||||||
|
writer.write_all("No such nick/channel".as_bytes()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteResponse for ErrNoNicknameGiven431 {
|
||||||
|
async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
||||||
|
writer.write_all(b"431").await?;
|
||||||
|
writer.write_all(self.client.as_bytes()).await?;
|
||||||
|
writer.write_all(b" :").await?;
|
||||||
|
writer.write_all("No nickname given".as_bytes()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteResponse for ErrNoSuchServer402 {
|
||||||
|
async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
||||||
|
writer.write_all(b"402 ").await?;
|
||||||
|
writer.write_all(self.client.as_bytes()).await?;
|
||||||
|
writer.write_all(b" ").await?;
|
||||||
|
writer.write_all(self.server_name.as_bytes()).await?;
|
||||||
|
writer.write_all(b" :").await?;
|
||||||
|
writer.write_all("No such server".as_bytes()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod error;
|
||||||
|
pub mod response;
|
|
@ -0,0 +1,24 @@
|
||||||
|
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
||||||
|
|
||||||
|
use crate::{prelude::Str, response::WriteResponse};
|
||||||
|
|
||||||
|
pub struct RplEndOfWhois318 {
|
||||||
|
client: Str,
|
||||||
|
nick: Str,
|
||||||
|
}
|
||||||
|
impl RplEndOfWhois318 {
|
||||||
|
pub fn new(client: Str, nick: Str) -> Self {
|
||||||
|
RplEndOfWhois318 { client, nick }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl WriteResponse for RplEndOfWhois318 {
|
||||||
|
async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
||||||
|
writer.write_all(b"318 ").await?;
|
||||||
|
writer.write_all(self.client.as_bytes()).await?;
|
||||||
|
writer.write_all(b" ").await?;
|
||||||
|
writer.write_all(self.nick.as_bytes()).await?;
|
||||||
|
writer.write_all(b" :").await?;
|
||||||
|
writer.write_all("End of /WHOIS list".as_bytes()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
//! Client-to-Server IRC protocol.
|
//! Client-to-Server IRC protocol.
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
pub mod commands;
|
||||||
mod prelude;
|
mod prelude;
|
||||||
|
pub mod response;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod testkit;
|
mod testkit;
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
||||||
|
|
||||||
|
use crate::prelude::Str;
|
||||||
|
use crate::Tag;
|
||||||
|
|
||||||
|
pub trait WriteResponse {
|
||||||
|
fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> impl Future<Output = std::io::Result<()>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Server-to-client enum agnostic message
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct IrcResponseMessage<T> {
|
||||||
|
/// Optional tags section, prefixed with `@`
|
||||||
|
pub tags: Vec<Tag>,
|
||||||
|
/// Optional server name, prefixed with `:`.
|
||||||
|
pub sender: Option<Str>,
|
||||||
|
pub body: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IrcResponseMessage<T> {
|
||||||
|
pub fn empty_tags(sender: Option<Str>, body: T) -> Self {
|
||||||
|
IrcResponseMessage {
|
||||||
|
tags: vec![],
|
||||||
|
sender,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(tags: Vec<Tag>, sender: Option<Str>, body: T) -> Self {
|
||||||
|
IrcResponseMessage { tags, sender, body }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: WriteResponse> WriteResponse for IrcResponseMessage<T> {
|
||||||
|
async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
|
||||||
|
if let Some(sender) = &self.sender {
|
||||||
|
writer.write_all(b":").await?;
|
||||||
|
writer.write_all(sender.as_bytes()).await?;
|
||||||
|
writer.write_all(b" ").await?;
|
||||||
|
}
|
||||||
|
self.body.write_response(writer).await?;
|
||||||
|
writer.write_all(b"\r\n").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
use tokio::io::AsyncWrite;
|
use tokio::io::AsyncWrite;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::user::PrefixedNick;
|
use crate::user::PrefixedNick;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
/// Server-to-client message.
|
/// Server-to-client message.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct ServerMessage {
|
pub struct ServerMessage {
|
||||||
|
@ -114,6 +113,12 @@ pub enum ServerMessageBody {
|
||||||
/// Usually `b"End of WHO list"`
|
/// Usually `b"End of WHO list"`
|
||||||
msg: Str,
|
msg: Str,
|
||||||
},
|
},
|
||||||
|
N318EndOfWhois {
|
||||||
|
client: Str,
|
||||||
|
nick: Str,
|
||||||
|
/// Usually `b"End of /WHOIS list"`
|
||||||
|
msg: Str,
|
||||||
|
},
|
||||||
N332Topic {
|
N332Topic {
|
||||||
client: Str,
|
client: Str,
|
||||||
chat: Chan,
|
chat: Chan,
|
||||||
|
@ -143,6 +148,10 @@ pub enum ServerMessageBody {
|
||||||
client: Str,
|
client: Str,
|
||||||
chan: Chan,
|
chan: Chan,
|
||||||
},
|
},
|
||||||
|
N431ErrNoNicknameGiven {
|
||||||
|
client: Str,
|
||||||
|
message: Str,
|
||||||
|
},
|
||||||
N474BannedFromChan {
|
N474BannedFromChan {
|
||||||
client: Str,
|
client: Str,
|
||||||
chan: Chan,
|
chan: Chan,
|
||||||
|
@ -280,6 +289,14 @@ impl ServerMessageBody {
|
||||||
writer.write_all(b" :").await?;
|
writer.write_all(b" :").await?;
|
||||||
writer.write_all(msg.as_bytes()).await?;
|
writer.write_all(msg.as_bytes()).await?;
|
||||||
}
|
}
|
||||||
|
ServerMessageBody::N318EndOfWhois { client, nick, msg } => {
|
||||||
|
writer.write_all(b"318 ").await?;
|
||||||
|
writer.write_all(client.as_bytes()).await?;
|
||||||
|
writer.write_all(b" ").await?;
|
||||||
|
writer.write_all(nick.as_bytes()).await?;
|
||||||
|
writer.write_all(b" :").await?;
|
||||||
|
writer.write_all(msg.as_bytes()).await?;
|
||||||
|
}
|
||||||
ServerMessageBody::N332Topic { client, chat, topic } => {
|
ServerMessageBody::N332Topic { client, chat, topic } => {
|
||||||
writer.write_all(b"332 ").await?;
|
writer.write_all(b"332 ").await?;
|
||||||
writer.write_all(client.as_bytes()).await?;
|
writer.write_all(client.as_bytes()).await?;
|
||||||
|
@ -342,6 +359,12 @@ impl ServerMessageBody {
|
||||||
chan.write_async(writer).await?;
|
chan.write_async(writer).await?;
|
||||||
writer.write_all(b" :End of /NAMES list").await?;
|
writer.write_all(b" :End of /NAMES list").await?;
|
||||||
}
|
}
|
||||||
|
ServerMessageBody::N431ErrNoNicknameGiven { client, message } => {
|
||||||
|
writer.write_all(b"431").await?;
|
||||||
|
writer.write_all(client.as_bytes()).await?;
|
||||||
|
writer.write_all(b" :").await?;
|
||||||
|
writer.write_all(message.as_bytes()).await?;
|
||||||
|
}
|
||||||
ServerMessageBody::N474BannedFromChan { client, chan, message } => {
|
ServerMessageBody::N474BannedFromChan { client, chan, message } => {
|
||||||
writer.write_all(b"474 ").await?;
|
writer.write_all(b"474 ").await?;
|
||||||
writer.write_all(client.as_bytes()).await?;
|
writer.write_all(client.as_bytes()).await?;
|
||||||
|
@ -470,9 +493,10 @@ fn server_message_body_cap(input: &str) -> IResult<&str, ServerMessageBody> {
|
||||||
mod test {
|
mod test {
|
||||||
use assert_matches::*;
|
use assert_matches::*;
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use crate::testkit::*;
|
use crate::testkit::*;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_server_message_notice() {
|
fn test_server_message_notice() {
|
||||||
let input = "NOTICE * :*** Looking up your hostname...\r\n";
|
let input = "NOTICE * :*** Looking up your hostname...\r\n";
|
||||||
|
|
Loading…
Reference in New Issue