feat: basic channel-based messaging

This commit is contained in:
user0-07161 2025-10-03 21:06:38 +02:00
parent 463c9c06da
commit 0d6447ae01
13 changed files with 295 additions and 42 deletions

72
src/channels.rs Normal file
View file

@ -0,0 +1,72 @@
use std::collections::BTreeSet;
use anyhow::Result;
use tokio::{io::BufWriter, net::TcpStream};
use crate::{sender::IrcResponseCodes, user::User};
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct Channel {
pub name: String,
pub joined_users: BTreeSet<User>,
}
impl Channel {
pub fn add_user(&mut self, user: User) {
self.joined_users.insert(user);
}
pub fn new_channel(name: String, user: User) -> Self {
Channel {
name,
joined_users: BTreeSet::from([user]),
}
}
pub async fn names_list_send(
&self,
user: User,
writer: &mut BufWriter<TcpStream>,
hostname: &str,
) -> Result<()> {
let mut members = Vec::new();
for member in self.clone().joined_users {
members.push(member.nickname.unwrap());
}
IrcResponseCodes::NameReply
.into_irc_response(
user.nickname.clone().unwrap(),
format!("= {} :{}", self.name.clone(), members.join(" ")),
)
.send(hostname, writer, false)
.await?;
IrcResponseCodes::EndOfNames
.into_irc_response(
user.nickname.clone().unwrap(),
format!("{} :End of /NAMES list", self.name.clone()),
)
.send(hostname, writer, false)
.await?;
Ok(())
}
pub async fn send_topic(
&self,
user: User,
writer: &mut BufWriter<TcpStream>,
hostname: &str,
) -> Result<()> {
IrcResponseCodes::NoTopic
.into_irc_response(
user.nickname.clone().unwrap(),
format!("{} :No topic is set", self.name.clone()),
)
.send(hostname, writer, false)
.await?;
Ok(())
}
}

View file

@ -1,7 +1,9 @@
use async_trait::async_trait;
use tokio::sync::broadcast::Sender;
use crate::{
commands::{IrcAction, IrcHandler},
messages::Message,
user::User,
};
@ -14,6 +16,7 @@ impl IrcHandler for Cap {
_arguments: Vec<String>,
_authenticated: bool,
_user_state: &mut User,
_sender: Sender<Message>,
) -> super::IrcAction {
IrcAction::DoNothing
}

69
src/commands/join.rs Normal file
View file

@ -0,0 +1,69 @@
use async_trait::async_trait;
use tokio::sync::broadcast::Sender;
use crate::{
JOINED_CHANNELS,
channels::Channel,
commands::{IrcAction, IrcHandler},
messages::{JoinMessage, Message},
user::User,
};
pub struct Join;
#[async_trait]
impl IrcHandler for Join {
async fn handle(
&self,
arguments: Vec<String>,
authenticated: bool,
user_state: &mut User,
sender: Sender<Message>,
) -> super::IrcAction {
let mut joined_channels = JOINED_CHANNELS.lock().await;
let mut channels = Vec::new();
for channel in arguments[0].clone().split(',') {
let mut maybe_existing_channel: Option<Channel> = None;
if !channel.starts_with("#") {
continue;
}
if !authenticated {
return IrcAction::ErrorAuthenticateFirst;
}
for existing_channel in joined_channels.clone() {
if existing_channel.name == channel {
maybe_existing_channel = Some(existing_channel);
}
}
if let Some(mut new_channel) = maybe_existing_channel.clone() {
new_channel.joined_users.insert(user_state.clone());
joined_channels.remove(&maybe_existing_channel.clone().unwrap());
joined_channels.insert(new_channel.clone());
channels.push(new_channel.clone());
} else {
let new_channel = Channel::new_channel(channel.into(), user_state.clone());
joined_channels.insert(new_channel.clone());
channels.push(new_channel.clone());
}
}
for channel in channels.clone() {
let join_message = JoinMessage {
sender: user_state.clone().unwrap_all(),
channel: channel.clone(),
};
sender.send(Message::JoinMessage(join_message)).unwrap();
}
IrcAction::JoinChannels(channels)
}
}

View file

@ -3,15 +3,21 @@ use std::collections::HashMap;
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use tokio::{io::BufWriter, net::TcpStream};
use tokio::{io::BufWriter, net::TcpStream, sync::broadcast::Sender};
use crate::{
commands::{cap::Cap, nick::Nick, ping::Ping, privmsg::PrivMsg, user::User as UserHandler},
SENDER,
channels::Channel,
commands::{
cap::Cap, join::Join, nick::Nick, ping::Ping, privmsg::PrivMsg, user::User as UserHandler,
},
messages::Message,
sender::IrcResponse,
user::User,
};
mod cap;
mod join;
mod nick;
mod ping;
mod privmsg;
@ -29,8 +35,8 @@ pub struct IrcMessage {
}
pub enum IrcAction {
MultipleActions(Vec<Self>),
SendText(IrcResponse),
JoinChannels(Vec<Channel>),
ErrorAuthenticateFirst,
DoNothing,
}
@ -44,6 +50,7 @@ pub trait IrcHandler: Send + Sync {
command: Vec<String>,
authenticated: bool,
user_state: &mut User,
sender: Sender<Message>,
) -> IrcAction;
}
@ -89,6 +96,7 @@ impl IrcCommand {
user_state: &mut User,
) -> Result<()> {
let mut command_map: HashMap<String, &dyn IrcHandler> = HashMap::new();
let broadcast_sender = SENDER.lock().await.clone().unwrap();
// Command map is defined here
command_map.insert("CAP".to_owned(), &Cap);
@ -96,6 +104,7 @@ impl IrcCommand {
command_map.insert("USER".to_owned(), &UserHandler);
command_map.insert("PRIVMSG".to_owned(), &PrivMsg);
command_map.insert("PING".to_owned(), &Ping);
command_map.insert("JOIN".to_owned(), &Join);
println!("{self:#?}");
@ -109,21 +118,41 @@ impl IrcCommand {
self.arguments.clone(),
user_state.is_populated(),
user_state,
broadcast_sender,
)
.await;
action.execute(writer, hostname).await;
action.execute(writer, hostname, &user_state).await;
Ok(())
}
}
impl IrcAction {
pub async fn execute(&self, writer: &mut BufWriter<TcpStream>, hostname: &str) {
pub async fn execute(
&self,
writer: &mut BufWriter<TcpStream>,
hostname: &str,
user_state: &User,
) {
match self {
IrcAction::SendText(msg) => {
msg.send(hostname, writer, false).await.unwrap();
}
IrcAction::JoinChannels(channels) => {
for channel in channels {
channel
.send_topic(user_state.clone(), writer, hostname)
.await
.unwrap();
channel
.names_list_send(user_state.clone(), writer, hostname)
.await
.unwrap();
}
}
_ => {}
}
}

View file

@ -1,7 +1,9 @@
use async_trait::async_trait;
use tokio::sync::broadcast::Sender;
use crate::{
commands::{IrcAction, IrcHandler},
messages::Message,
user::User,
};
@ -14,6 +16,7 @@ impl IrcHandler for Nick {
command: Vec<String>,
_authenticated: bool,
user_state: &mut User,
_sender: Sender<Message>,
) -> IrcAction {
user_state.nickname = Some(command[0].clone());

View file

@ -1,7 +1,9 @@
use async_trait::async_trait;
use tokio::sync::broadcast::Sender;
use crate::{
commands::{IrcAction, IrcHandler},
messages::Message,
sender::IrcResponse,
user::User,
};
@ -15,13 +17,15 @@ impl IrcHandler for Ping {
command: Vec<String>,
authenticated: bool,
user_state: &mut User,
_sender: Sender<Message>,
) -> IrcAction {
if authenticated {
IrcAction::SendText(IrcResponse {
sender: None,
command: "PONG".into(),
receiver: user_state.nickname.clone().unwrap(),
message: command[0].clone(),
arguments: Vec::new(),
receiver: Some(user_state.username.clone().unwrap()),
message: format!(":{}", command[0].clone()),
})
} else {
IrcAction::DoNothing

View file

@ -1,9 +1,10 @@
use async_trait::async_trait;
use tokio::sync::broadcast::Sender;
use crate::{
CONNECTED_USERS, SENDER,
CONNECTED_USERS,
commands::{IrcAction, IrcHandler},
messages::Message,
messages::{Message, PrivMessage},
user::User,
};
@ -16,23 +17,23 @@ impl IrcHandler for PrivMsg {
command: Vec<String>,
authenticated: bool,
user_state: &mut User,
sender: Sender<Message>,
) -> IrcAction {
if !authenticated {
return IrcAction::ErrorAuthenticateFirst;
}
let connected_users = CONNECTED_USERS.lock().await;
let sender = SENDER.lock().await.clone().unwrap();
println!("{connected_users:#?}");
drop(connected_users);
let message = Message {
let message = PrivMessage {
sender: user_state.clone().unwrap_all(),
receiver: command[0].clone(),
text: command[1].clone(),
};
println!("SENDING: {message:#?}");
sender.send(message).unwrap();
sender.send(Message::PrivMessage(message)).unwrap();
IrcAction::DoNothing
}

View file

@ -1,7 +1,9 @@
use async_trait::async_trait;
use tokio::sync::broadcast::Sender;
use crate::{
commands::{IrcAction, IrcHandler},
messages::Message,
user::User as UserState,
};
@ -14,7 +16,11 @@ impl IrcHandler for User {
command: Vec<String>,
_authenticated: bool,
user_state: &mut UserState,
_sender: Sender<Message>,
) -> IrcAction {
if command.len() < 4 {
return IrcAction::DoNothing; // XXX: return an error
}
user_state.username = Some(command[0].clone());
user_state.realname = Some(command[3].clone());

View file

@ -26,19 +26,19 @@ pub async fn send_motd(
);
IrcResponseCodes::Welcome
.into_irc_response(user_info.username.clone(), welcome_text)
.into_irc_response(user_info.nickname.clone(), welcome_text)
.send(&server_info.server_hostname, writer, true)
.await?;
IrcResponseCodes::YourHost
.into_irc_response(user_info.username.clone(), yourhost_text)
.into_irc_response(user_info.nickname.clone(), yourhost_text)
.send(&server_info.server_hostname, writer, true)
.await?;
IrcResponseCodes::MyInfo
.into_irc_response(user_info.username.clone(), myinfo_text)
.into_irc_response(user_info.nickname.clone(), myinfo_text)
.send(&server_info.server_hostname, writer, false)
.await?;
IrcResponseCodes::ISupport
.into_irc_response(user_info.username.clone(), isupport_text)
.into_irc_response(user_info.nickname.clone(), isupport_text)
.send(&server_info.server_hostname, writer, false)
.await?;
IrcResponseCodes::NoMotd

View file

@ -19,12 +19,14 @@ use tokio::{
};
use crate::{
channels::Channel,
login::send_motd,
messages::Message,
sender::{IrcResponse, IrcResponseCodes},
user::{User, UserUnwrapped},
};
mod channels;
mod commands;
mod login;
mod messages;
@ -33,6 +35,8 @@ mod user;
pub static CONNECTED_USERS: Lazy<Mutex<HashSet<UserUnwrapped>>> =
Lazy::new(|| Mutex::new(HashSet::new()));
pub static JOINED_CHANNELS: Lazy<Mutex<HashSet<Channel>>> =
Lazy::new(|| Mutex::new(HashSet::new()));
pub static SENDER: Lazy<Mutex<Option<Sender<Message>>>> = Lazy::new(|| Mutex::new(None));
#[allow(dead_code)]
@ -178,21 +182,60 @@ async fn message_listener(
bail!("user has not registered yet, returning...");
}
let user = user_wrapped.unwrap_all();
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;
println!("{message:#?}");
if user.nickname.clone().to_ascii_lowercase() == message.receiver.to_ascii_lowercase() {
IrcResponse {
sender: Some(message.sender.hostmask()),
command: "PRIVMSG".into(),
message: message.text,
receiver: user.username.clone(),
match message {
Message::PrivMessage(message) => {
for channel in joined_channels.clone() {
if channel.joined_users.contains(user_wrapped) && channel.name == message.receiver {
channel_name = Some(channel.name.clone());
}
}
if user.nickname.clone().to_ascii_lowercase() == message.receiver.to_ascii_lowercase() {
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::JoinMessage(message) => {
if message.channel.joined_users.contains(user_wrapped) || message.sender == user {
IrcResponse {
sender: Some(message.sender.hostmask().clone()),
command: "JOIN".into(),
arguments: Vec::new(),
message: message.channel.name.clone(),
receiver: None,
}
.send("", writer, true)
.await?;
}
}
.send("", writer, true)
.await
.unwrap();
}
Ok(())

View file

@ -1,8 +1,21 @@
use crate::user::UserUnwrapped;
use crate::{channels::Channel, user::UserUnwrapped};
#[derive(Debug, Clone)]
pub enum Message {
PrivMessage(PrivMessage),
JoinMessage(JoinMessage),
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct Message {
pub struct JoinMessage {
pub sender: UserUnwrapped,
pub channel: Channel,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct PrivMessage {
pub sender: UserUnwrapped,
pub receiver: String,
pub text: String,

View file

@ -8,7 +8,8 @@ use tokio::{
pub struct IrcResponse {
pub sender: Option<String>,
pub command: String,
pub receiver: String,
pub receiver: Option<String>,
pub arguments: Vec<String>,
pub message: String,
}
@ -20,6 +21,9 @@ pub enum IrcResponseCodes {
MyInfo,
ISupport,
NoMotd,
NoTopic,
NameReply,
EndOfNames,
}
impl IrcResponse {
@ -29,20 +33,22 @@ impl IrcResponse {
writer: &mut BufWriter<TcpStream>,
prepend_column: bool,
) -> Result<()> {
let mut response = format!(
":{} {} {} ",
self.sender.clone().unwrap_or(hostname.to_string()),
self.command,
self.receiver
);
let sender = format!(":{}", self.sender.clone().unwrap_or(hostname.to_string()));
let mut full_response = Vec::new();
full_response.push(sender);
full_response.extend_from_slice(&self.arguments);
full_response.push(self.command.clone());
if let Some(receiver) = self.receiver.clone() {
full_response.push(receiver);
}
if prepend_column {
response.push_str(&format!(":{}\r\n", self.message.trim_end()));
full_response.push(format!(":{}\r\n", self.message.trim_end()));
} else {
response.push_str(&format!("{}\r\n", self.message.trim_end()));
full_response.push(format!("{}\r\n", self.message.trim_end()));
}
writer.write_all(response.as_bytes()).await?;
writer.write_all(full_response.join(" ").as_bytes()).await?;
writer.flush().await?;
Ok(())
@ -58,6 +64,9 @@ impl From<IrcResponseCodes> for &str {
IrcResponseCodes::MyInfo => "004",
IrcResponseCodes::ISupport => "005",
IrcResponseCodes::NoMotd => "422",
IrcResponseCodes::NoTopic => "331",
IrcResponseCodes::NameReply => "353",
IrcResponseCodes::EndOfNames => "366",
}
}
}
@ -73,7 +82,8 @@ impl IrcResponseCodes {
IrcResponse {
sender: None,
command: (*self).into(),
receiver,
arguments: Vec::new(),
receiver: Some(receiver),
message,
}
}

View file

@ -2,7 +2,7 @@
use std::borrow::Borrow;
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
#[derive(Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub struct User {
pub nickname: Option<String>,
pub username: Option<String>,
@ -47,7 +47,7 @@ impl UserUnwrapped {
format!(
"{}!~{}@{}",
self.nickname.clone(),
self.realname.clone(),
self.username.clone(),
"unimplement.ed"
)
}