forked from lavina/lavina
1
0
Fork 0

- added trait to handle incoming commands from client

- implemented handle trait for whois command
This commit is contained in:
homycdev 2024-04-21 21:50:45 +03:00
parent 5b30312238
commit 07f2730c8f
11 changed files with 380 additions and 81 deletions

View File

@ -0,0 +1,17 @@
use lavina_core::prelude::Str;
use lavina_core::repo::Storage;
use std::future::Future;
use tokio::io::AsyncWrite;
use proto_irc::response::SendResponseBody;
pub mod whois;
pub trait Handler<Command> {
fn handle(
&self,
server_name: &Str,
client: &Str,
writer: &mut (impl AsyncWrite + Unpin),
storage: &mut Storage,
) -> impl Future<Output = anyhow::Result<()>>;
}

View File

@ -0,0 +1,64 @@
use tokio::io::{AsyncWrite, AsyncWriteExt};
use lavina_core::prelude::Str;
use proto_irc::response::SendResponseBody;
pub struct ERR_NOSUCHNICK_401 {
client: Str,
nick: Str,
}
impl ERR_NOSUCHNICK_401 {
pub fn new(client: Str, nick: Str) -> Self {
ERR_NOSUCHNICK_401 { client, nick }
}
}
struct ERR_NOSUCHSERVER_402 {
client: Str,
/// target parameter in WHOIS
/// example: `/whois <target> <nick>`
server_name: Str,
}
pub struct ERR_NONICKNAMEGIVEN_431 {
client: Str,
}
impl ERR_NONICKNAMEGIVEN_431 {
pub fn new(client: Str) -> Self {
ERR_NONICKNAMEGIVEN_431 { client }
}
}
impl SendResponseBody for ERR_NOSUCHNICK_401 {
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 SendResponseBody for ERR_NONICKNAMEGIVEN_431 {
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 SendResponseBody for ERR_NOSUCHSERVER_402 {
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(())
}
}

View File

@ -0,0 +1,74 @@
use tokio::io::{AsyncWrite, AsyncWriteExt};
use tracing::instrument::WithSubscriber;
use lavina_core::prelude::Str;
use lavina_core::repo::Storage;
use proto_irc::client::command_args::Whois;
use proto_irc::response::{IrcResponseMessage, SendResponseBody, SendResponseMessage};
use crate::commands::whois::error::{ERR_NONICKNAMEGIVEN_431, ERR_NOSUCHNICK_401};
use crate::commands::whois::response::{RplWhoIsUser311, RPL_ENDOFWHOIS_318};
use crate::commands::Handler;
pub mod error;
pub mod response;
impl Handler<Whois> for Whois {
async fn handle(
&self,
server_name: &Str,
client: &Str,
writer: &mut (impl AsyncWrite + Unpin),
storage: &mut Storage,
) -> anyhow::Result<()> {
match self {
Whois::Nick(nick) => handle_nick_target(nick, None, server_name, client, writer, storage).await?,
Whois::TargetNick(target, nick) => {
handle_nick_target(nick, Some(target), server_name, client, writer, storage).await?
}
Whois::EmptyArgs => {
IrcResponseMessage::empty_tags(
Some(server_name.clone()),
ERR_NONICKNAMEGIVEN_431::new(server_name.clone()),
)
.write_response(writer)
.await?
}
}
Ok(())
}
}
async fn handle_nick_target(
nick: &Str,
// todo: implement logic with target
_target: Option<&Str>,
server_name: &Str,
client: &Str,
writer: &mut (impl AsyncWrite + Unpin),
storage: &mut Storage,
) -> anyhow::Result<()> {
if let Some(user) = storage.retrieve_user_by_name(nick).await? {
IrcResponseMessage::empty_tags(
Some(server_name.clone()),
RplWhoIsUser311::new(
client.clone(),
nick.clone(),
Some(Str::from(user.name.clone())),
server_name.clone(),
),
)
.write_response(writer)
.await?;
IrcResponseMessage::empty_tags(
Some(server_name.clone()),
RPL_ENDOFWHOIS_318::new(client.clone(), user.name.clone().into()),
)
.write_response(writer)
.await?
} else {
ERR_NOSUCHNICK_401::new(client.clone(), nick.clone()).write_response(writer).await?
}
Ok(())
}

View File

@ -0,0 +1,83 @@
use tokio::io::{AsyncWrite, AsyncWriteExt};
use lavina_core::prelude::Str;
use proto_irc::response::SendResponseBody;
struct RplWhoiscertfp276;
struct RplWhoIsRegNick307;
pub struct RplWhoIsUser311 {
client: Str,
/// unique name
nick: Str,
/// username not unique
username: Option<Str>,
/// server name
host: Str,
}
impl RplWhoIsUser311 {
pub fn new(client: Str, nick: Str, username: Option<Str>, host: Str) -> Self {
RplWhoIsUser311 {
client,
nick,
username,
host,
}
}
}
struct _RplWhoisserver312;
struct _RplWhoisoperator313;
struct _RplWhoisidle317;
struct _RplWhoischannels319;
struct _RplWhoisspecial320;
struct _RplWhoisaccount330;
struct _RplWhoisactually338;
struct _RplWhoishost378;
struct _RplWhoismodes379;
struct _RplWhoissecure671;
struct _RplAway301;
pub struct RPL_ENDOFWHOIS_318 {
client: Str,
nick: Str,
}
impl RPL_ENDOFWHOIS_318 {
pub fn new(client: Str, nick: Str) -> Self {
RPL_ENDOFWHOIS_318 { client, nick }
}
}
impl SendResponseBody for RplWhoIsUser311 {
async fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
writer.write_all(b"311 ").await?;
writer.write_all(self.client.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(self.nick.as_bytes()).await?;
if let Some(username) = self.username {
writer.write_all(b" ").await?;
writer.write_all(username.as_bytes()).await?;
}
writer.write_all(b" ").await?;
writer.write_all(self.host.as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all("*".as_bytes()).await?;
writer.write_all(b" ").await?;
writer.write_all(b" :").await?;
//todo no entity in db that represents whole irc user entity
writer.write_all("<realname>".as_bytes()).await?;
Ok(())
}
}
impl SendResponseBody for RPL_ENDOFWHOIS_318 {
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(())
}
}

View File

@ -25,10 +25,12 @@ use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody};
use proto_irc::user::PrefixedNick; use proto_irc::user::PrefixedNick;
use proto_irc::{Chan, Recipient}; use proto_irc::{Chan, Recipient};
use sasl::AuthBody; use sasl::AuthBody;
mod cap; mod cap;
mod commands;
use crate::cap::Capabilities; use crate::cap::Capabilities;
use commands::Handler;
use proto_irc::response::IrcResponseMessage;
pub const APP_VERSION: &str = concat!("lavina", "_", env!("CARGO_PKG_VERSION")); pub const APP_VERSION: &str = concat!("lavina", "_", env!("CARGO_PKG_VERSION"));
@ -75,7 +77,7 @@ async fn handle_socket(
match registered_user { match registered_user {
Ok(user) => { Ok(user) => {
log::debug!("User registered"); log::debug!("User registered");
handle_registered_socket(config, players, rooms, &mut reader, &mut writer, user).await?; handle_registered_socket(config, players, rooms, &mut reader, &mut writer, user, &mut storage).await?;
} }
Err(err) => { Err(err) => {
log::debug!("Registration failed: {err}"); log::debug!("Registration failed: {err}");
@ -387,6 +389,7 @@ async fn handle_registered_socket<'a>(
reader: &mut BufReader<ReadHalf<'a>>, reader: &mut BufReader<ReadHalf<'a>>,
writer: &mut BufWriter<WriteHalf<'a>>, writer: &mut BufWriter<WriteHalf<'a>>,
user: RegisteredUser, user: RegisteredUser,
storage: &mut Storage,
) -> Result<()> { ) -> Result<()> {
let mut buffer = vec![]; let mut buffer = vec![];
log::info!("Handling registered user: {user:?}"); log::info!("Handling registered user: {user:?}");
@ -466,7 +469,7 @@ async fn handle_registered_socket<'a>(
len len
}; };
let incoming = std::str::from_utf8(&buffer[0..len-2])?; let incoming = std::str::from_utf8(&buffer[0..len-2])?;
if let HandleResult::Leave = handle_incoming_message(incoming, &config, &user, &rooms, &mut connection, writer).await? { if let HandleResult::Leave = handle_incoming_message(incoming, &config, &user, &rooms, &mut connection, writer, storage).await? {
break; break;
} }
buffer.clear(); buffer.clear();
@ -615,6 +618,7 @@ async fn handle_incoming_message(
rooms: &RoomRegistry, rooms: &RoomRegistry,
user_handle: &mut PlayerConnection, user_handle: &mut PlayerConnection,
writer: &mut (impl AsyncWrite + Unpin), writer: &mut (impl AsyncWrite + Unpin),
storage: &mut Storage,
) -> Result<HandleResult> { ) -> Result<HandleResult> {
log::debug!("Incoming raw IRC message: '{buffer}'"); log::debug!("Incoming raw IRC message: '{buffer}'");
let parsed = client_message(buffer); let parsed = client_message(buffer);
@ -724,55 +728,16 @@ async fn handle_incoming_message(
log::warn!("Local chans not supported"); log::warn!("Local chans not supported");
} }
}, },
ClientMessage::Whois { target, nick } => { ClientMessage::Whois { arg } => {
// todo: finish replpies from the server to the command arg.handle(
match (target, nick) { &config.server_name,
(Some(target), Some(nick)) => { &user.nickname,
ServerMessage { writer,
tags: vec![], storage,
sender: Some(config.server_name.clone()), )
body: ServerMessageBody::N318EndOfWhois { .await?
client: user.nickname.clone(),
nick: nick,
msg: "End of /WHOIS list".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?
}
(Some(target), None) => {
todo!()
}
(None, Some(nick)) => {
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N318EndOfWhois {
client: user.nickname.clone(),
nick: nick,
msg: "End of /WHOIS list".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?
}
(None, None) => {
ServerMessage {
tags: vec![],
sender: Some(config.server_name.clone()),
body: ServerMessageBody::N431ErrNoNicknameGiven {
client: user.nickname.clone(),
message: "No nickname given".into(),
},
}
.write_async(writer)
.await?;
writer.flush().await?
}
}
} }
ClientMessage::Mode { target } => { ClientMessage::Mode { target } => {
match target { match target {
Recipient::Nick(nickname) => { Recipient::Nick(nickname) => {

View File

@ -8,10 +8,11 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::tcp::{ReadHalf, WriteHalf};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use lavina_core::repo::{Storage, StorageConfig};
use lavina_core::{player::PlayerRegistry, room::RoomRegistry}; use lavina_core::{player::PlayerRegistry, room::RoomRegistry};
use projection_irc::APP_VERSION; use lavina_core::repo::{Storage, StorageConfig};
use projection_irc::{launch, read_irc_message, RunningServer, ServerConfig}; use projection_irc::{launch, read_irc_message, RunningServer, ServerConfig};
use projection_irc::APP_VERSION;
struct TestScope<'a> { struct TestScope<'a> {
reader: BufReader<ReadHalf<'a>>, reader: BufReader<ReadHalf<'a>>,
writer: WriteHalf<'a>, writer: WriteHalf<'a>,
@ -193,6 +194,52 @@ async fn scenario_cap_full_negotiation() -> Result<()> {
Ok(()) Ok(())
} }
// #[tokio::test]
// async fn scenario_whois_command() -> Result<()> {
// let mut server = TestServer::start().await?;
//
// server.storage.create_user("tester").await?;
// server.storage.set_password("tester", "password").await?;
// let mut stream = TcpStream::connect(server.server.addr).await?;
// let mut s = TestScope::new(&mut stream);
//
//
//
// s.send("PASS password").await?;
// s.send("NICK tester").await?;
// s.send("USER UserName 0 * :Real Name").await?;
//
// s.expect(":testserver 001 tester :Welcome to testserver Server").await?;
// s.expect(":testserver 002 tester :Welcome to testserver Server").await?;
// s.expect(":testserver 003 tester :Welcome to testserver Server").await?;
// s.expect(
// format!(
// ":testserver 004 tester testserver {} r CFILPQbcefgijklmnopqrstvz",
// &APP_VERSION
// )
// .as_str(),
// )
// .await?;
// s.expect(":testserver 005 tester CHANTYPES=# :are supported by this server").await?;
// s.expect_nothing().await?;
//
// s.send("WHOIS tester").await?;
// s.expect(" ").await?;
//
// s.send("QUIT :Leaving").await?;
// s.expect(":testserver ERROR :Leaving the server").await?;
// s.expect_eof().await?;
//
// stream.shutdown().await?;
//
// // wrap up
//
// server.server.terminate().await?;
//
// Ok(())
// }
#[tokio::test] #[tokio::test]
async fn scenario_cap_short_negotiation() -> Result<()> { async fn scenario_cap_short_negotiation() -> Result<()> {
let mut server = TestServer::start().await?; let mut server = TestServer::start().await?;

View File

@ -190,7 +190,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(())
}, },

View File

@ -1,6 +1,5 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use nom::combinator::{all_consuming, opt}; use nom::combinator::{all_consuming, opt};
use nom::error::ErrorKind;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use super::*; use super::*;
@ -45,8 +44,7 @@ pub enum ClientMessage {
}, },
/// WHOIS [<target>] <nick> /// WHOIS [<target>] <nick>
Whois { Whois {
target: Option<Str>, // server_name or nick_name arg: command_args::Whois,
nick: Option<Str>,
}, },
/// `TOPIC <chan> :<topic>` /// `TOPIC <chan> :<topic>`
Topic { Topic {
@ -69,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,
@ -191,23 +200,19 @@ fn client_message_whois(input: &str) -> IResult<&str, ClientMessage> {
[nick] => Ok(( [nick] => Ok((
"", "",
ClientMessage::Whois { ClientMessage::Whois {
target: None, arg: command_args::Whois::Nick(nick.into()),
nick: Some(nick.into()),
}, },
)), )),
[target, nick, ..] => Ok(( [target, nick, ..] => Ok((
"", "",
ClientMessage::Whois { ClientMessage::Whois {
target: Some(target.into()), arg: command_args::Whois::TargetNick(target.into(), nick.into()),
nick: Some(nick.into()),
}, },
)), )),
// fixme: idk how to deal with this in more elegant way
[] => Ok(( [] => Ok((
"", "",
ClientMessage::Whois { ClientMessage::Whois {
target: None, arg: command_args::Whois::EmptyArgs,
nick: None,
}, },
)), )),
} }
@ -412,41 +417,33 @@ mod test {
let res_none_none_params = client_message(test_none_none_params); let res_none_none_params = client_message(test_none_none_params);
let expected_arg = ClientMessage::Whois { let expected_arg = ClientMessage::Whois {
target: None, arg: command_args::Whois::Nick("val".into()),
nick: Some("val".into()),
}; };
let expected_user_user = ClientMessage::Whois { let expected_user_user = ClientMessage::Whois {
target: Some("val".into()), arg: command_args::Whois::TargetNick("val".into(), "val".into()),
nick: Some("val".into()),
}; };
let expected_server_user = ClientMessage::Whois { let expected_server_user = ClientMessage::Whois {
target: Some("com.test.server".into()), arg: command_args::Whois::TargetNick("com.test.server".into(), "user".into()),
nick: Some("user".into()),
}; };
let expected_user_server = ClientMessage::Whois { let expected_user_server = ClientMessage::Whois {
target: Some("user".into()), arg: command_args::Whois::TargetNick("user".into(), "com.test.server".into()),
nick: Some("com.test.server".into()),
}; };
let expected_user_list = ClientMessage::Whois { let expected_user_list = ClientMessage::Whois {
target: None, arg: command_args::Whois::Nick("user_1,user_2,user_3".into()),
nick: Some("user_1,user_2,user_3".into()),
}; };
let expected_server_user_list = ClientMessage::Whois { let expected_server_user_list = ClientMessage::Whois {
target: Some("com.test.server".into()), arg: command_args::Whois::TargetNick("com.test.server".into(), "user_1,user_2,user_3".into()),
nick: Some("user_1,user_2,user_3".into()),
}; };
let expected_more_than_two_params = ClientMessage::Whois { let expected_more_than_two_params = ClientMessage::Whois {
target: Some("test.server".into()), arg: command_args::Whois::TargetNick("test.server".into(), "user_1,user_2,user_3".into()),
nick: Some("user_1,user_2,user_3".into()),
}; };
let expected_none_none_params = ClientMessage::Whois { let expected_none_none_params = ClientMessage::Whois {
target: None, arg: command_args::Whois::EmptyArgs,
nick: None,
}; };
assert_matches!(res_one_arg, Ok(result) => assert_eq!(expected_arg, result)); assert_matches!(res_one_arg, Ok(result) => assert_eq!(expected_arg, result));

View File

@ -5,6 +5,7 @@ pub mod server;
#[cfg(test)] #[cfg(test)]
mod testkit; mod testkit;
pub mod user; pub mod user;
pub mod response;
use crate::prelude::Str; use crate::prelude::Str;

View File

@ -0,0 +1,51 @@
use std::future::Future;
use tokio::io::{AsyncWrite, AsyncWriteExt};
use crate::prelude::Str;
use crate::Tag;
pub trait SendResponseBody {
fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> impl Future<Output = std::io::Result<()>>;
}
pub trait SendResponseMessage<Message> {
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: SendResponseBody> {
/// Optional tags section, prefixed with `@`
pub tags: Vec<Tag>,
/// Optional server name, prefixed with `:`.
pub sender: Option<Str>,
pub body: T,
}
impl<T: SendResponseBody> 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: SendResponseBody> SendResponseMessage<IrcResponseMessage<T>> 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(())
}
}

View File

@ -283,7 +283,7 @@ impl ServerMessageBody {
writer.write_all(msg.as_bytes()).await?; writer.write_all(msg.as_bytes()).await?;
} }
ServerMessageBody::N318EndOfWhois { client, nick, msg } => { ServerMessageBody::N318EndOfWhois { client, nick, msg } => {
writer.write_all(b"b318 ").await?; writer.write_all(b"318 ").await?;
writer.write_all(client.as_bytes()).await?; writer.write_all(client.as_bytes()).await?;
writer.write_all(b" ").await?; writer.write_all(b" ").await?;
writer.write_all(nick.as_bytes()).await?; writer.write_all(nick.as_bytes()).await?;