diff --git a/crates/projection-irc/src/commands/mod.rs b/crates/projection-irc/src/commands/mod.rs new file mode 100644 index 0000000..b4d948b --- /dev/null +++ b/crates/projection-irc/src/commands/mod.rs @@ -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 { + fn handle( + &self, + server_name: &Str, + client: &Str, + writer: &mut (impl AsyncWrite + Unpin), + storage: &mut Storage, + ) -> impl Future>; +} diff --git a/crates/projection-irc/src/commands/whois/error.rs b/crates/projection-irc/src/commands/whois/error.rs new file mode 100644 index 0000000..d7fd63b --- /dev/null +++ b/crates/projection-irc/src/commands/whois/error.rs @@ -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 ` + 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(()) + } +} diff --git a/crates/projection-irc/src/commands/whois/mod.rs b/crates/projection-irc/src/commands/whois/mod.rs new file mode 100644 index 0000000..f233328 --- /dev/null +++ b/crates/projection-irc/src/commands/whois/mod.rs @@ -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 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(()) +} diff --git a/crates/projection-irc/src/commands/whois/response.rs b/crates/projection-irc/src/commands/whois/response.rs new file mode 100644 index 0000000..05213f1 --- /dev/null +++ b/crates/projection-irc/src/commands/whois/response.rs @@ -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, + /// server name + host: Str, +} + +impl RplWhoIsUser311 { + pub fn new(client: Str, nick: Str, username: Option, 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("".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(()) + } +} diff --git a/crates/projection-irc/src/lib.rs b/crates/projection-irc/src/lib.rs index e596478..6c58fd7 100644 --- a/crates/projection-irc/src/lib.rs +++ b/crates/projection-irc/src/lib.rs @@ -25,10 +25,12 @@ use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; use proto_irc::user::PrefixedNick; use proto_irc::{Chan, Recipient}; use sasl::AuthBody; - mod cap; +mod commands; use crate::cap::Capabilities; +use commands::Handler; +use proto_irc::response::IrcResponseMessage; pub const APP_VERSION: &str = concat!("lavina", "_", env!("CARGO_PKG_VERSION")); @@ -75,7 +77,7 @@ async fn handle_socket( match registered_user { Ok(user) => { 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) => { log::debug!("Registration failed: {err}"); @@ -387,6 +389,7 @@ async fn handle_registered_socket<'a>( reader: &mut BufReader>, writer: &mut BufWriter>, user: RegisteredUser, + storage: &mut Storage, ) -> Result<()> { let mut buffer = vec![]; log::info!("Handling registered user: {user:?}"); @@ -466,7 +469,7 @@ async fn handle_registered_socket<'a>( len }; 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; } buffer.clear(); @@ -615,6 +618,7 @@ async fn handle_incoming_message( rooms: &RoomRegistry, user_handle: &mut PlayerConnection, writer: &mut (impl AsyncWrite + Unpin), + storage: &mut Storage, ) -> Result { log::debug!("Incoming raw IRC message: '{buffer}'"); let parsed = client_message(buffer); @@ -724,55 +728,16 @@ async fn handle_incoming_message( log::warn!("Local chans not supported"); } }, - ClientMessage::Whois { target, nick } => { - // todo: finish replpies from the server to the command - match (target, nick) { - (Some(target), 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? - } - (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::Whois { arg } => { + arg.handle( + &config.server_name, + &user.nickname, + writer, + storage, + ) + .await? } + ClientMessage::Mode { target } => { match target { Recipient::Nick(nickname) => { diff --git a/crates/projection-irc/tests/lib.rs b/crates/projection-irc/tests/lib.rs index c3efd2a..42cfb8c 100644 --- a/crates/projection-irc/tests/lib.rs +++ b/crates/projection-irc/tests/lib.rs @@ -8,10 +8,11 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::TcpStream; -use lavina_core::repo::{Storage, StorageConfig}; 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::APP_VERSION; + struct TestScope<'a> { reader: BufReader>, writer: WriteHalf<'a>, @@ -193,6 +194,52 @@ async fn scenario_cap_full_negotiation() -> Result<()> { 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] async fn scenario_cap_short_negotiation() -> Result<()> { let mut server = TestServer::start().await?; diff --git a/crates/projection-xmpp/src/lib.rs b/crates/projection-xmpp/src/lib.rs index a2a0a5b..512ecf2 100644 --- a/crates/projection-xmpp/src/lib.rs +++ b/crates/projection-xmpp/src/lib.rs @@ -190,7 +190,7 @@ async fn handle_socket( pin!(termination); select! { biased; - _ = &mut termination =>{ + _ = &mut termination => { log::info!("Socket handling was terminated"); return Ok(()) }, diff --git a/crates/proto-irc/src/client.rs b/crates/proto-irc/src/client.rs index 081ed39..ecced72 100644 --- a/crates/proto-irc/src/client.rs +++ b/crates/proto-irc/src/client.rs @@ -1,6 +1,5 @@ use anyhow::{anyhow, Result}; use nom::combinator::{all_consuming, opt}; -use nom::error::ErrorKind; use nonempty::NonEmpty; use super::*; @@ -45,8 +44,7 @@ pub enum ClientMessage { }, /// WHOIS [] Whois { - target: Option, // server_name or nick_name - nick: Option, + arg: command_args::Whois, }, /// `TOPIC :` Topic { @@ -69,6 +67,17 @@ pub enum ClientMessage { 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 { let res = all_consuming(alt(( client_message_capability, @@ -191,23 +200,19 @@ fn client_message_whois(input: &str) -> IResult<&str, ClientMessage> { [nick] => Ok(( "", ClientMessage::Whois { - target: None, - nick: Some(nick.into()), + arg: command_args::Whois::Nick(nick.into()), }, )), [target, nick, ..] => Ok(( "", ClientMessage::Whois { - target: Some(target.into()), - nick: Some(nick.into()), + arg: command_args::Whois::TargetNick(target.into(), nick.into()), }, )), - // fixme: idk how to deal with this in more elegant way [] => Ok(( "", ClientMessage::Whois { - target: None, - nick: None, + arg: command_args::Whois::EmptyArgs, }, )), } @@ -412,41 +417,33 @@ mod test { let res_none_none_params = client_message(test_none_none_params); let expected_arg = ClientMessage::Whois { - target: None, - nick: Some("val".into()), + arg: command_args::Whois::Nick("val".into()), }; let expected_user_user = ClientMessage::Whois { - target: Some("val".into()), - nick: Some("val".into()), + arg: command_args::Whois::TargetNick("val".into(), "val".into()), }; let expected_server_user = ClientMessage::Whois { - target: Some("com.test.server".into()), - nick: Some("user".into()), + arg: command_args::Whois::TargetNick("com.test.server".into(), "user".into()), }; let expected_user_server = ClientMessage::Whois { - target: Some("user".into()), - nick: Some("com.test.server".into()), + arg: command_args::Whois::TargetNick("user".into(), "com.test.server".into()), }; let expected_user_list = ClientMessage::Whois { - target: None, - nick: Some("user_1,user_2,user_3".into()), + arg: command_args::Whois::Nick("user_1,user_2,user_3".into()), }; let expected_server_user_list = ClientMessage::Whois { - target: Some("com.test.server".into()), - nick: Some("user_1,user_2,user_3".into()), + arg: command_args::Whois::TargetNick("com.test.server".into(), "user_1,user_2,user_3".into()), }; let expected_more_than_two_params = ClientMessage::Whois { - target: Some("test.server".into()), - nick: Some("user_1,user_2,user_3".into()), + arg: command_args::Whois::TargetNick("test.server".into(), "user_1,user_2,user_3".into()), }; let expected_none_none_params = ClientMessage::Whois { - target: None, - nick: None, + arg: command_args::Whois::EmptyArgs, }; assert_matches!(res_one_arg, Ok(result) => assert_eq!(expected_arg, result)); diff --git a/crates/proto-irc/src/lib.rs b/crates/proto-irc/src/lib.rs index 54ff676..ab82664 100644 --- a/crates/proto-irc/src/lib.rs +++ b/crates/proto-irc/src/lib.rs @@ -5,6 +5,7 @@ pub mod server; #[cfg(test)] mod testkit; pub mod user; +pub mod response; use crate::prelude::Str; diff --git a/crates/proto-irc/src/response.rs b/crates/proto-irc/src/response.rs new file mode 100644 index 0000000..1a0051c --- /dev/null +++ b/crates/proto-irc/src/response.rs @@ -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>; +} + +pub trait SendResponseMessage { + fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> impl Future>; +} + +/// Server-to-client enum agnostic message +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IrcResponseMessage { + /// Optional tags section, prefixed with `@` + pub tags: Vec, + /// Optional server name, prefixed with `:`. + pub sender: Option, + pub body: T, +} + +impl IrcResponseMessage { + pub fn empty_tags(sender: Option, body: T) -> Self { + IrcResponseMessage { + tags: vec![], + sender, + body, + } + } + + pub fn new(tags: Vec, sender: Option, body: T) -> Self { + IrcResponseMessage { tags, sender, body } + } +} + +impl SendResponseMessage> for IrcResponseMessage { + 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(()) + } +} diff --git a/crates/proto-irc/src/server.rs b/crates/proto-irc/src/server.rs index 15da778..7a6e82c 100644 --- a/crates/proto-irc/src/server.rs +++ b/crates/proto-irc/src/server.rs @@ -283,7 +283,7 @@ impl ServerMessageBody { writer.write_all(msg.as_bytes()).await?; } 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(b" ").await?; writer.write_all(nick.as_bytes()).await?;