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::{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<ReadHalf<'a>>,
writer: &mut BufWriter<WriteHalf<'a>>,
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<HandleResult> {
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) => {

View File

@ -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<ReadHalf<'a>>,
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?;

View File

@ -190,7 +190,7 @@ async fn handle_socket(
pin!(termination);
select! {
biased;
_ = &mut termination =>{
_ = &mut termination => {
log::info!("Socket handling was terminated");
return Ok(())
},

View File

@ -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 [<target>] <nick>
Whois {
target: Option<Str>, // server_name or nick_name
nick: Option<Str>,
arg: command_args::Whois,
},
/// `TOPIC <chan> :<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<ClientMessage> {
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));

View File

@ -5,6 +5,7 @@ pub mod server;
#[cfg(test)]
mod testkit;
pub mod user;
pub mod response;
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?;
}
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?;