forked from lavina/lavina
1
0
Fork 0

Compare commits

...

2 Commits

Author SHA1 Message Date
homycdev ad00fe759a cargo fmt 2024-05-05 15:57:32 +03:00
homycdev 66a3bb7a68 added tests, refactoring made 2024-05-05 15:57:14 +03:00
14 changed files with 163 additions and 126 deletions

View File

@ -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_existance].
#[tracing::instrument(skip(self), name = "PlayerConnection::send_dialog_message")]
pub async fn check_user_existance(&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_existance(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_existance")]
async fn check_user_existance(&self, recipient: PlayerId) -> GetInfoResult {
if self.storage.check_user_existance(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.

View File

@ -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_existance")]
pub async fn check_user_existance(&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;

View File

@ -1,33 +0,0 @@
use lavina_core::{prelude::Str, repo::Storage};
use std::future::Future;
use tokio::io::AsyncWrite;
pub mod whois;
pub struct HandlerArgs<T: AsyncWrite + Unpin> {
server_name: Str,
client: Str,
writer: T,
storage: Storage,
}
impl<T> HandlerArgs<T>
where
T: AsyncWrite + Unpin,
{
pub fn new(server_name: Str, client: Str, writer: T, storage: Storage) -> HandlerArgs<T> {
HandlerArgs {
server_name,
client,
writer,
storage,
}
}
}
pub trait Handler<T>
where
T: AsyncWrite + Unpin,
{
fn handle(&self, arg: HandlerArgs<T>) -> impl Future<Output = anyhow::Result<()>>;
}

View File

@ -1,70 +0,0 @@
use tokio::io::AsyncWrite;
use lavina_core::prelude::Str;
use proto_irc::client::command_args::Whois;
use proto_irc::response::{IrcResponseMessage, WriteResponse};
use crate::commands::whois::error::{ErrNoNicknameGiven431, ErrNoSuchNick401};
use crate::commands::whois::response::{RplEndOfWhois318, RplWhoIsUser311};
use crate::commands::Handler;
use super::HandlerArgs;
pub mod error;
pub mod response;
impl<T: AsyncWrite + Unpin> Handler<T> for Whois {
async fn handle(&self, body: HandlerArgs<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 HandlerArgs {
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: HandlerArgs<impl AsyncWrite + Unpin>) -> anyhow::Result<()> {
let HandlerArgs {
server_name,
mut writer,
client,
mut storage,
} = body;
if let Some(user) = storage.retrieve_user_by_name(nick.clone().as_ref()).await? {
IrcResponseMessage::empty_tags(
Some(server_name.clone()),
RplWhoIsUser311::new(
client.clone(),
nick.clone(),
Some(Str::from(user.name.clone())),
server_name.clone(),
nick.clone(),
),
)
.write_response(&mut writer)
.await?;
IrcResponseMessage::empty_tags(
Some(server_name.clone()),
RplEndOfWhois318::new(client.clone(), user.name.clone().into()),
)
.write_response(&mut writer)
.await?
} else {
ErrNoSuchNick401::new(client.clone(), nick.clone()).write_response(&mut writer).await?
}
Ok(())
}

View File

@ -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<()>>;
}

View File

@ -29,11 +29,12 @@ 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;
mod commands; use handler::Handler;
mod whois;
use crate::cap::Capabilities; use crate::cap::Capabilities;
use commands::Handler;
use proto_irc::response::IrcResponseMessage; mod handler;
pub const APP_VERSION: &str = concat!("lavina", "_", env!("CARGO_PKG_VERSION")); pub const APP_VERSION: &str = concat!("lavina", "_", env!("CARGO_PKG_VERSION"));
@ -80,7 +81,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, core.players, core.rooms, &mut reader, &mut writer, user).await?; handle_registered_socket(config, core.players, core.rooms, &mut reader, &mut writer, user, &mut storage).await?;
} }
Err(err) => { Err(err) => {
log::debug!("Registration failed: {err}"); log::debug!("Registration failed: {err}");
@ -811,13 +812,15 @@ async fn handle_incoming_message(
} }
}, },
ClientMessage::Whois { arg } => { ClientMessage::Whois { arg } => {
arg.handle(commands::HandlerArgs::new( arg.handle(handler::IrcConnection {
config.server_name.clone(), server_name: config.server_name.clone(),
user.nickname.clone(), client: user.nickname.clone(),
writer, writer,
storage.clone(), player_connection: user_handle,
)) })
.await? .await?;
writer.flush().await?;
} }
ClientMessage::Mode { target } => { ClientMessage::Mode { target } => {
match target { match target {

View File

@ -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_existance(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(())
}

View File

@ -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?;

View File

@ -0,0 +1 @@
pub mod whois;

View File

@ -1,7 +1,6 @@
use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncWrite, AsyncWriteExt};
use lavina_core::prelude::Str; use crate::{prelude::Str, response::WriteResponse};
use proto_irc::response::WriteResponse;
/// ErrNoSuchNick401 /// ErrNoSuchNick401
pub struct ErrNoSuchNick401 { pub struct ErrNoSuchNick401 {
@ -34,7 +33,7 @@ impl ErrNoNicknameGiven431 {
} }
impl WriteResponse for ErrNoSuchNick401 { impl WriteResponse for ErrNoSuchNick401 {
async fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
writer.write_all(b"401 ").await?; writer.write_all(b"401 ").await?;
writer.write_all(self.client.as_bytes()).await?; writer.write_all(self.client.as_bytes()).await?;
writer.write_all(b" ").await?; writer.write_all(b" ").await?;
@ -46,7 +45,7 @@ impl WriteResponse for ErrNoSuchNick401 {
} }
impl WriteResponse for ErrNoNicknameGiven431 { impl WriteResponse for ErrNoNicknameGiven431 {
async fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
writer.write_all(b"431").await?; writer.write_all(b"431").await?;
writer.write_all(self.client.as_bytes()).await?; writer.write_all(self.client.as_bytes()).await?;
writer.write_all(b" :").await?; writer.write_all(b" :").await?;
@ -56,7 +55,7 @@ impl WriteResponse for ErrNoNicknameGiven431 {
} }
impl WriteResponse for ErrNoSuchServer402 { impl WriteResponse for ErrNoSuchServer402 {
async fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
writer.write_all(b"402 ").await?; writer.write_all(b"402 ").await?;
writer.write_all(self.client.as_bytes()).await?; writer.write_all(self.client.as_bytes()).await?;
writer.write_all(b" ").await?; writer.write_all(b" ").await?;

View File

@ -0,0 +1,2 @@
pub mod error;
pub mod response;

View File

@ -2,8 +2,7 @@ use std::{net::IpAddr, time::SystemTime};
use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::io::{AsyncWrite, AsyncWriteExt};
use lavina_core::prelude::Str; use crate::{prelude::Str, response::WriteResponse, Chan};
use proto_irc::{response::WriteResponse, Chan};
/// "<client> <nick> :has client certificate fingerprint <fingerprint>" /// "<client> <nick> :has client certificate fingerprint <fingerprint>"
/// Clients MUST only be sent this numeric if they are either using the WHOIS command on themselves /// Clients MUST only be sent this numeric if they are either using the WHOIS command on themselves
@ -150,12 +149,12 @@ impl RplEndOfWhois318 {
} }
impl WriteResponse for RplWhoIsUser311 { impl WriteResponse for RplWhoIsUser311 {
async fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
writer.write_all(b"311 ").await?; writer.write_all(b"311 ").await?;
writer.write_all(self.client.as_bytes()).await?; writer.write_all(self.client.as_bytes()).await?;
writer.write_all(b" ").await?; writer.write_all(b" ").await?;
writer.write_all(self.nick.as_bytes()).await?; writer.write_all(self.nick.as_bytes()).await?;
if let Some(username) = self.username { if let Some(username) = &self.username {
writer.write_all(b" ").await?; writer.write_all(b" ").await?;
writer.write_all(username.as_bytes()).await?; writer.write_all(username.as_bytes()).await?;
} }
@ -173,7 +172,7 @@ impl WriteResponse for RplWhoIsUser311 {
} }
impl WriteResponse for RplEndOfWhois318 { impl WriteResponse for RplEndOfWhois318 {
async fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
writer.write_all(b"318 ").await?; writer.write_all(b"318 ").await?;
writer.write_all(self.client.as_bytes()).await?; writer.write_all(self.client.as_bytes()).await?;
writer.write_all(b" ").await?; writer.write_all(b" ").await?;

View File

@ -1,5 +1,6 @@
//! 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 response;
pub mod server; pub mod server;

View File

@ -6,7 +6,7 @@ use crate::prelude::Str;
use crate::Tag; use crate::Tag;
pub trait WriteResponse { pub trait WriteResponse {
fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> impl Future<Output = std::io::Result<()>>; fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> impl Future<Output = std::io::Result<()>>;
} }
/// Server-to-client enum agnostic message /// Server-to-client enum agnostic message
@ -34,7 +34,7 @@ impl<T> IrcResponseMessage<T> {
} }
impl<T: WriteResponse> WriteResponse for IrcResponseMessage<T> { impl<T: WriteResponse> WriteResponse for IrcResponseMessage<T> {
async fn write_response(self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> { async fn write_response(&self, writer: &mut (impl AsyncWrite + Unpin)) -> std::io::Result<()> {
if let Some(sender) = &self.sender { if let Some(sender) = &self.sender {
writer.write_all(b":").await?; writer.write_all(b":").await?;
writer.write_all(sender.as_bytes()).await?; writer.write_all(sender.as_bytes()).await?;