chore: move client-related code into its own directory
All checks were successful
build / test-alpine (push) Successful in 51s
build / test-debian (push) Successful in 1m23s

This commit is contained in:
user0-07161 2026-02-15 17:47:10 +01:00
parent 0489cac646
commit d3c3cd8292
12 changed files with 297 additions and 278 deletions

View file

@ -1,14 +1,12 @@
use async_trait::async_trait;
use crate::{
commands::{IrcAction, IrcHandler},
user::User,
};
use super::{ClientAction, ClientHandler};
use crate::user::User;
pub struct Cap;
#[async_trait]
impl IrcHandler for Cap {
impl ClientHandler for Cap {
async fn handle(
&self,
_arguments: Vec<String>,
@ -17,7 +15,7 @@ impl IrcHandler for Cap {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<super::IrcAction> {
vec![IrcAction::DoNothing]
) -> Vec<super::ClientAction> {
vec![ClientAction::DoNothing]
}
}

View file

@ -1,16 +1,12 @@
use async_trait::async_trait;
use crate::{
JOINED_CHANNELS,
channels::Channel,
commands::{IrcAction, IrcHandler},
user::User,
};
use super::{ClientAction, ClientHandler};
use crate::{JOINED_CHANNELS, channels::Channel, user::User};
pub struct Join;
#[async_trait]
impl IrcHandler for Join {
impl ClientHandler for Join {
async fn handle(
&self,
arguments: Vec<String>,
@ -19,7 +15,7 @@ impl IrcHandler for Join {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<super::IrcAction> {
) -> Vec<super::ClientAction> {
let mut joined_channels = JOINED_CHANNELS.lock().await;
let mut channels = Vec::new();
@ -31,7 +27,7 @@ impl IrcHandler for Join {
}
if !authenticated {
return vec![IrcAction::ErrorAuthenticateFirst];
return vec![ClientAction::ErrorAuthenticateFirst];
}
for existing_channel in joined_channels.clone() {
@ -56,6 +52,6 @@ impl IrcHandler for Join {
}
}
vec![IrcAction::JoinChannels(channels)]
vec![ClientAction::JoinChannels(channels)]
}
}

View file

@ -5,13 +5,13 @@ use async_trait::async_trait;
use tokio::{io::BufWriter, net::TcpStream, sync::broadcast::Sender};
use tracing::debug;
use super::commands::{
cap::Cap, join::Join, nick::Nick, pass::Pass, ping::Ping, privmsg::PrivMsg,
user::User as UserHandler, who::Who,
};
use crate::{
SENDER,
channels::Channel,
commands::{
cap::Cap, join::Join, nick::Nick, pass::Pass, ping::Ping, privmsg::PrivMsg,
user::User as UserHandler, who::Who,
},
config::ServerInfo,
error_structs::CommandExecError,
messages::{ChanJoinMessage, Message},
@ -29,17 +29,17 @@ mod user;
mod who;
#[derive(Debug)]
pub struct IrcCommand {
pub struct ClientCommand {
command: String,
arguments: Vec<String>,
}
pub struct IrcMessage {
pub struct ClientMessage {
pub sender: String, // TODO: replace with hostmask
pub message: String,
}
pub enum IrcAction {
pub enum ClientAction {
SendText(IrcResponse),
SendMessage(Message),
JoinChannels(Vec<Channel>),
@ -55,7 +55,7 @@ pub enum ReturnAction {
}
#[async_trait]
pub trait IrcHandler: Send + Sync {
pub trait ClientHandler: Send + Sync {
async fn handle(
&self,
command: Vec<String>,
@ -64,12 +64,12 @@ pub trait IrcHandler: Send + Sync {
server_outgoing_password: String,
server_incoming_passwords: Vec<String>,
user_passwords: Vec<String>,
) -> Vec<IrcAction>;
) -> Vec<ClientAction>;
}
pub struct SendMessage(Option<String>);
impl IrcCommand {
impl ClientCommand {
pub async fn new(command_with_arguments: String) -> Self {
let mut split_command: Vec<&str> = command_with_arguments
.split_whitespace()
@ -114,7 +114,7 @@ impl IrcCommand {
user_state: &mut User,
config: &ServerInfo,
) -> Result<Vec<ReturnAction>, CommandExecError> {
let mut command_map: HashMap<String, &dyn IrcHandler> = HashMap::new();
let mut command_map: HashMap<String, &dyn ClientHandler> = HashMap::new();
let broadcast_sender = SENDER.lock().await.clone().unwrap();
// Command map is defined here
@ -159,7 +159,7 @@ impl IrcCommand {
}
}
impl IrcAction {
impl ClientAction {
pub async fn execute(
&self,
writer: &mut BufWriter<TcpStream>,
@ -168,11 +168,11 @@ impl IrcAction {
sender: Sender<Message>,
) -> ReturnAction {
match self {
IrcAction::SendText(msg) => {
ClientAction::SendText(msg) => {
msg.send(hostname, writer, false).await.unwrap();
}
IrcAction::JoinChannels(channels) => {
ClientAction::JoinChannels(channels) => {
for channel in channels {
let join_message = ChanJoinMessage {
sender: user_state.clone().unwrap_all(),
@ -182,11 +182,11 @@ impl IrcAction {
}
}
IrcAction::SendMessage(msg) => {
ClientAction::SendMessage(msg) => {
sender.send(msg.clone()).unwrap();
}
IrcAction::UpgradeToServerConn => {
ClientAction::UpgradeToServerConn => {
return ReturnAction::ServerConn;
}

View file

@ -1,14 +1,12 @@
use async_trait::async_trait;
use crate::{
commands::{IrcAction, IrcHandler},
user::User,
};
use super::{ClientAction, ClientHandler};
use crate::user::User;
pub struct Nick;
#[async_trait]
impl IrcHandler for Nick {
impl ClientHandler for Nick {
async fn handle(
&self,
command: Vec<String>,
@ -17,7 +15,7 @@ impl IrcHandler for Nick {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
) -> Vec<ClientAction> {
user_state.nickname = Some({
if command[0].len() > 9 {
String::from_utf8(
@ -34,6 +32,6 @@ impl IrcHandler for Nick {
}
});
vec![IrcAction::DoNothing]
vec![ClientAction::DoNothing]
}
}

View file

@ -1,14 +1,12 @@
use async_trait::async_trait;
use crate::{
commands::{IrcAction, IrcHandler},
user::User,
};
use super::{ClientAction, ClientHandler};
use crate::user::User;
pub struct Pass;
#[async_trait]
impl IrcHandler for Pass {
impl ClientHandler for Pass {
async fn handle(
&self,
command: Vec<String>,
@ -17,22 +15,22 @@ impl IrcHandler for Pass {
server_outgoing_password: String,
server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
) -> Vec<ClientAction> {
// XXX
if server_incoming_passwords.contains(&command[0]) {
vec![
IrcAction::SendText(crate::sender::IrcResponse {
ClientAction::SendText(crate::sender::IrcResponse {
sender: None,
command: "PASS".to_owned(),
receiver: None,
arguments: Vec::new(),
message: server_outgoing_password.clone(),
}),
IrcAction::UpgradeToServerConn,
ClientAction::UpgradeToServerConn,
]
} else {
vec![IrcAction::DoNothing]
vec![ClientAction::DoNothing]
}
}
}

View file

@ -1,15 +1,12 @@
use async_trait::async_trait;
use crate::{
commands::{IrcAction, IrcHandler},
sender::IrcResponse,
user::User,
};
use super::{ClientAction, ClientHandler};
use crate::{sender::IrcResponse, user::User};
pub struct Ping;
#[async_trait]
impl IrcHandler for Ping {
impl ClientHandler for Ping {
async fn handle(
&self,
command: Vec<String>,
@ -18,9 +15,9 @@ impl IrcHandler for Ping {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
) -> Vec<ClientAction> {
if authenticated {
vec![IrcAction::SendText(IrcResponse {
vec![ClientAction::SendText(IrcResponse {
sender: None,
command: "PONG".into(),
arguments: Vec::new(),
@ -28,7 +25,7 @@ impl IrcHandler for Ping {
message: format!(":{}", command[0].clone()),
})]
} else {
vec![IrcAction::DoNothing]
vec![ClientAction::DoNothing]
}
}
}

View file

@ -1,7 +1,7 @@
use async_trait::async_trait;
use super::{ClientAction, ClientHandler};
use crate::{
commands::{IrcAction, IrcHandler},
messages::{Message, PrivMessage, Receiver},
user::User,
};
@ -9,7 +9,7 @@ use crate::{
pub struct PrivMsg;
#[async_trait]
impl IrcHandler for PrivMsg {
impl ClientHandler for PrivMsg {
async fn handle(
&self,
command: Vec<String>,
@ -18,9 +18,9 @@ impl IrcHandler for PrivMsg {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
) -> Vec<ClientAction> {
if !authenticated {
return vec![IrcAction::ErrorAuthenticateFirst];
return vec![ClientAction::ErrorAuthenticateFirst];
}
let receiver = if command[0].clone().starts_with("#") {
@ -35,6 +35,6 @@ impl IrcHandler for PrivMsg {
text: command[1].clone(),
};
vec![IrcAction::SendMessage(Message::PrivMessage(message))]
vec![ClientAction::SendMessage(Message::PrivMessage(message))]
}
}

View file

@ -1,14 +1,12 @@
use async_trait::async_trait;
use crate::{
commands::{IrcAction, IrcHandler},
user::User as UserState,
};
use super::{ClientAction, ClientHandler};
use crate::user::User as UserState;
pub struct User;
#[async_trait]
impl IrcHandler for User {
impl ClientHandler for User {
async fn handle(
&self,
command: Vec<String>,
@ -17,9 +15,9 @@ impl IrcHandler for User {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<IrcAction> {
) -> Vec<ClientAction> {
if command.len() < 4 {
return vec![IrcAction::DoNothing]; // XXX: return an error
return vec![ClientAction::DoNothing]; // XXX: return an error
}
// oh my god this is a mess
@ -40,6 +38,6 @@ impl IrcHandler for User {
});
user_state.realname = Some(command[3].clone());
vec![IrcAction::DoNothing]
vec![ClientAction::DoNothing]
}
}

View file

@ -1,14 +1,12 @@
use async_trait::async_trait;
use crate::{
commands::{IrcAction, IrcHandler},
user::User,
};
use super::{ClientAction, ClientHandler};
use crate::user::User;
pub struct Who;
#[async_trait]
impl IrcHandler for Who {
impl ClientHandler for Who {
async fn handle(
&self,
_arguments: Vec<String>,
@ -17,7 +15,7 @@ impl IrcHandler for Who {
_server_outgoing_password: String,
_server_incoming_passwords: Vec<String>,
_user_passwords: Vec<String>,
) -> Vec<super::IrcAction> {
vec![IrcAction::DoNothing] // TODO
) -> Vec<super::ClientAction> {
vec![ClientAction::DoNothing] // TODO
}
}

229
src/client/mod.rs Normal file
View file

@ -0,0 +1,229 @@
use std::{
net::TcpStream,
time::{Duration, SystemTime},
};
use crate::{
CONNECTED_USERS, JOINED_CHANNELS, SENDER, TcpListenerResult,
config::ServerInfo,
error_structs::{self, ListenerError},
login::send_motd,
messages::{Message, NetJoinMessage, Receiver as MsgReceiver},
sender::{IrcResponse, IrcResponseCodes},
ts6::structs::{ServerId, UserId},
user::User,
userid_gen,
};
use tokio::{
io::{AsyncBufReadExt, BufReader as TokioBufReader, BufWriter as TokioBufWriter},
net::TcpStream as TokioTcpStream,
spawn,
sync::{
Mutex,
broadcast::{self, Receiver, Sender},
},
time::sleep,
};
use tracing::debug;
mod commands;
#[derive(Clone, Debug, Default)]
pub struct Client {}
impl Client {
pub async fn tcp_listener(
&self,
stream: &TcpStream,
mut user_state: User,
info: &ServerInfo,
reader: &mut TokioBufReader<TokioTcpStream>,
our_sid: ServerId,
) -> Result<TcpListenerResult, ListenerError> {
let mut buffer = String::new();
let mut writer = TokioBufWriter::new(TokioTcpStream::from_std(stream.try_clone()?)?);
match reader.read_line(&mut buffer).await {
Ok(0) => return Err(ListenerError::ConnectionError),
Ok(_) => {}
Err(_) => {
let mut conneted_users = CONNECTED_USERS.lock().await;
let _ = conneted_users.remove(&user_state.clone().unwrap_all());
return Err(ListenerError::ConnectionError);
}
}
let command = commands::ClientCommand::new(buffer.clone()).await;
match command
.execute(&mut writer, &info.server_hostname, &mut user_state, info)
.await
{
Ok(return_actions) => {
for return_action in return_actions {
match return_action {
commands::ReturnAction::ServerConn => {
return Ok(TcpListenerResult::ServerConnectionInit);
}
_ => {}
}
}
}
Err(error) => match error {
error_structs::CommandExecError::NonexistantCommand => {
let error_string = format!("error processing your command: {error:#?}\n");
let error = IrcResponseCodes::UnknownCommand;
error
.into_irc_response("*".into(), error_string.into())
.send(&info.server_hostname, &mut writer, true)
.await
.unwrap();
}
},
}
if !user_state.identified && user_state.is_populated_without_uid() {
let id = userid_gen::increase_user_id()
.await
.unwrap()
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join("");
let user_id = format!("{our_sid}{id}");
user_state.identified = true;
user_state.user_id = Some(UserId::try_from(user_id).unwrap()); // XXX: error handling
user_state.timestamp = Some(SystemTime::now());
send_motd(info.clone(), user_state.clone(), &mut writer).await?;
let broadcast_sender = SENDER.lock().await.clone().unwrap();
broadcast_sender
.send(Message::NetJoinMessage(NetJoinMessage {
user: user_state.clone().unwrap_all(),
server_id: our_sid.clone(),
}))
.unwrap();
CONNECTED_USERS
.lock()
.await
.insert(user_state.clone().unwrap_all());
}
Ok(TcpListenerResult::UpdatedUser(user_state))
}
pub async fn message_listener(
&self,
user_wrapped: &User,
receiver: &mut Receiver<Message>,
writer: &mut TokioBufWriter<TokioTcpStream>,
hostname: &str,
) -> Result<(), ListenerError> {
if !user_wrapped.is_populated() {
sleep(Duration::from_millis(250)).await; // avoid immediate returns b'cuz they result in high
// cpu usage
return Err(ListenerError::UserIsUnidentified);
}
let user = user_wrapped.clone().unwrap_all();
let message: Message = receiver.recv().await.unwrap();
let joined_channels = JOINED_CHANNELS.lock().await;
let mut channel_name: Option<String> = None;
debug!("new message in the message stream: {message:?}");
match message {
Message::PrivMessage(message) => {
for channel in joined_channels.clone() {
if let MsgReceiver::ChannelName(channelname) = message.clone().receiver
&& channelname == channel.name
&& channel.joined_users.contains(user_wrapped)
{
channel_name = Some(channel.name.clone());
}
}
dbg!(&message);
if match message.clone().receiver {
MsgReceiver::UserId(userid) => {
debug!("{userid} ?= {}", user.user_id);
if userid == user.user_id { true } else { false }
}
MsgReceiver::Username(username) => {
if username.to_lowercase() == user.username.to_lowercase() {
true
} else {
false
}
}
_ => false,
} {
IrcResponse {
sender: Some(message.sender.hostmask()),
command: "PRIVMSG".into(),
arguments: Vec::new(),
message: message.text,
receiver: Some(user.username.clone()),
}
.send("", writer, true)
.await?;
} else if let Some(channel_name) = channel_name {
if message.sender != user {
IrcResponse {
sender: Some(message.sender.hostmask()),
command: "PRIVMSG".into(),
arguments: Vec::new(),
message: message.text,
receiver: Some(channel_name),
}
.send("", writer, true)
.await?;
}
}
}
Message::ChanJoinMessage(message) => {
if message.channel.joined_users.contains(user_wrapped) || message.sender == user {
let channel = message.channel.clone();
IrcResponse {
sender: Some(message.sender.hostmask().clone()),
command: "JOIN".into(),
arguments: Vec::new(),
message: message.channel.name.clone(),
receiver: None,
}
.send("", writer, true)
.await?;
channel
.send_topic(user_wrapped.clone(), writer, hostname)
.await
.unwrap();
channel
.names_list_send(user_wrapped.clone(), &channel, writer, hostname)
.await
.unwrap();
}
}
Message::NetJoinMessage(_) => {} // we don't care about these here :)
}
Ok(())
}
}

View file

@ -23,6 +23,7 @@ use tracing::{debug, instrument};
use crate::{
channels::Channel,
client::Client,
config::ServerInfo,
error_structs::{HandlerError, ListenerError},
logging::LogLevel,
@ -37,7 +38,7 @@ use crate::{
};
mod channels;
mod commands;
mod client;
mod config;
mod error_structs;
mod logging;
@ -93,9 +94,7 @@ async fn main() -> Result<(), AnyhowError> {
let tx_thread = tx.clone();
let info = info.clone();
spawn(handle_connection(
stream, info, /*&mut rx_thread,*/ tx_thread,
));
spawn(handle_connection(stream, info, tx_thread));
}
Ok(())
@ -117,12 +116,13 @@ async fn handle_connection(
let hostname = info.server_hostname.clone();
// TODO: generate randomally and allow overriding from config
// TODO: generate randomly and allow overriding from config
let my_server_id = ServerId::try_from("000".to_owned()).unwrap();
let client_status = Client::default();
loop {
tokio::select! {
result = tcp_listener(&stream_tcp, state.clone(), &info, &mut tcp_reader, my_server_id.clone()) => {
result = client_status.tcp_listener(&stream_tcp, state.clone(), &info, &mut tcp_reader, my_server_id.clone()) => {
match result {
Ok(tcp_listener_result) => {
match tcp_listener_result {
@ -141,7 +141,7 @@ async fn handle_connection(
}
}
},
result = message_listener(&state, &mut message_receiver, &mut tcp_writer, &hostname) => {
result = client_status.message_listener(&state, &mut message_receiver, &mut tcp_writer, &hostname) => {
match result {
Ok(_) => {},
Err(err) => {
@ -191,198 +191,6 @@ async fn handle_connection(
Ok(())
}
async fn tcp_listener(
stream: &TcpStream,
mut user_state: User,
info: &ServerInfo,
reader: &mut TokioBufReader<TokioTcpStream>,
our_sid: ServerId,
) -> Result<TcpListenerResult, ListenerError> {
let mut buffer = String::new();
let mut writer = TokioBufWriter::new(TokioTcpStream::from_std(stream.try_clone()?)?);
match reader.read_line(&mut buffer).await {
Ok(0) => return Err(ListenerError::ConnectionError),
Ok(_) => {}
Err(_) => {
let mut conneted_users = CONNECTED_USERS.lock().await;
let _ = conneted_users.remove(&user_state.clone().unwrap_all());
return Err(ListenerError::ConnectionError);
}
}
let command = commands::IrcCommand::new(buffer.clone()).await;
match command
.execute(&mut writer, &info.server_hostname, &mut user_state, info)
.await
{
Ok(return_actions) => {
for return_action in return_actions {
match return_action {
commands::ReturnAction::ServerConn => {
return Ok(TcpListenerResult::ServerConnectionInit);
}
_ => {}
}
}
}
Err(error) => match error {
error_structs::CommandExecError::NonexistantCommand => {
let error_string = format!("error processing your command: {error:#?}\n");
let error = IrcResponseCodes::UnknownCommand;
error
.into_irc_response("*".into(), error_string.into())
.send(&info.server_hostname, &mut writer, true)
.await
.unwrap();
}
},
}
if !user_state.identified && user_state.is_populated_without_uid() {
let id = userid_gen::increase_user_id()
.await
.unwrap()
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>()
.join("");
let user_id = format!("{our_sid}{id}");
user_state.identified = true;
user_state.user_id = Some(UserId::try_from(user_id).unwrap()); // XXX: error handling
user_state.timestamp = Some(SystemTime::now());
send_motd(info.clone(), user_state.clone(), &mut writer).await?;
let broadcast_sender = SENDER.lock().await.clone().unwrap();
broadcast_sender
.send(Message::NetJoinMessage(NetJoinMessage {
user: user_state.clone().unwrap_all(),
server_id: our_sid.clone(),
}))
.unwrap();
CONNECTED_USERS
.lock()
.await
.insert(user_state.clone().unwrap_all());
}
Ok(TcpListenerResult::UpdatedUser(user_state))
}
async fn message_listener(
user_wrapped: &User,
receiver: &mut Receiver<Message>,
writer: &mut TokioBufWriter<TokioTcpStream>,
hostname: &str,
) -> Result<(), ListenerError> {
if !user_wrapped.is_populated() {
sleep(Duration::from_millis(250)).await; // avoid immediate returns b'cuz they result in high
// cpu usage
return Err(ListenerError::UserIsUnidentified);
}
let user = user_wrapped.clone().unwrap_all();
let message: Message = receiver.recv().await.unwrap();
let joined_channels = JOINED_CHANNELS.lock().await;
let mut channel_name: Option<String> = None;
debug!("new message in the message stream: {message:?}");
match message {
Message::PrivMessage(message) => {
for channel in joined_channels.clone() {
if let MsgReceiver::ChannelName(channelname) = message.clone().receiver
&& channelname == channel.name
&& channel.joined_users.contains(user_wrapped)
{
channel_name = Some(channel.name.clone());
}
}
dbg!(&message);
if match message.clone().receiver {
MsgReceiver::UserId(userid) => {
debug!("{userid} ?= {}", user.user_id);
if userid == user.user_id { true } else { false }
}
MsgReceiver::Username(username) => {
if username.to_lowercase() == user.username.to_lowercase() {
true
} else {
false
}
}
_ => false,
} {
IrcResponse {
sender: Some(message.sender.hostmask()),
command: "PRIVMSG".into(),
arguments: Vec::new(),
message: message.text,
receiver: Some(user.username.clone()),
}
.send("", writer, true)
.await?;
} else if let Some(channel_name) = channel_name {
if message.sender != user {
IrcResponse {
sender: Some(message.sender.hostmask()),
command: "PRIVMSG".into(),
arguments: Vec::new(),
message: message.text,
receiver: Some(channel_name),
}
.send("", writer, true)
.await?;
}
}
}
Message::ChanJoinMessage(message) => {
if message.channel.joined_users.contains(user_wrapped) || message.sender == user {
let channel = message.channel.clone();
IrcResponse {
sender: Some(message.sender.hostmask().clone()),
command: "JOIN".into(),
arguments: Vec::new(),
message: message.channel.name.clone(),
receiver: None,
}
.send("", writer, true)
.await?;
channel
.send_topic(user_wrapped.clone(), writer, hostname)
.await
.unwrap();
channel
.names_list_send(user_wrapped.clone(), &channel, writer, hostname)
.await
.unwrap();
}
}
Message::NetJoinMessage(_) => {} // we don't care about these here :)
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::userid_gen;

View file

@ -2,7 +2,6 @@ use std::collections::HashMap;
use crate::{
SENDER,
commands::IrcMessage,
messages::Message,
sender::IrcResponse,
ts6::{