diff --git a/src/channels.rs b/src/channels.rs new file mode 100644 index 0000000..05a8b5e --- /dev/null +++ b/src/channels.rs @@ -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, +} + +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, + 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, + 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(()) + } +} diff --git a/src/commands/cap.rs b/src/commands/cap.rs index b13135a..ec23c4f 100644 --- a/src/commands/cap.rs +++ b/src/commands/cap.rs @@ -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, _authenticated: bool, _user_state: &mut User, + _sender: Sender, ) -> super::IrcAction { IrcAction::DoNothing } diff --git a/src/commands/join.rs b/src/commands/join.rs new file mode 100644 index 0000000..67c6321 --- /dev/null +++ b/src/commands/join.rs @@ -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, + authenticated: bool, + user_state: &mut User, + sender: Sender, + ) -> 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 = 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) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 9ae9ab9..6b2ec0a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -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), SendText(IrcResponse), + JoinChannels(Vec), ErrorAuthenticateFirst, DoNothing, } @@ -44,6 +50,7 @@ pub trait IrcHandler: Send + Sync { command: Vec, authenticated: bool, user_state: &mut User, + sender: Sender, ) -> IrcAction; } @@ -89,6 +96,7 @@ impl IrcCommand { user_state: &mut User, ) -> Result<()> { let mut command_map: HashMap = 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, hostname: &str) { + pub async fn execute( + &self, + writer: &mut BufWriter, + 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(); + } + } + _ => {} } } diff --git a/src/commands/nick.rs b/src/commands/nick.rs index aea53f2..abf0774 100644 --- a/src/commands/nick.rs +++ b/src/commands/nick.rs @@ -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, _authenticated: bool, user_state: &mut User, + _sender: Sender, ) -> IrcAction { user_state.nickname = Some(command[0].clone()); diff --git a/src/commands/ping.rs b/src/commands/ping.rs index 4bcaa4a..2eea4fb 100644 --- a/src/commands/ping.rs +++ b/src/commands/ping.rs @@ -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, authenticated: bool, user_state: &mut User, + _sender: Sender, ) -> 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 diff --git a/src/commands/privmsg.rs b/src/commands/privmsg.rs index 488a9ab..88eff31 100644 --- a/src/commands/privmsg.rs +++ b/src/commands/privmsg.rs @@ -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, authenticated: bool, user_state: &mut User, + sender: Sender, ) -> 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 } diff --git a/src/commands/user.rs b/src/commands/user.rs index db291f1..807e5ba 100644 --- a/src/commands/user.rs +++ b/src/commands/user.rs @@ -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, _authenticated: bool, user_state: &mut UserState, + _sender: Sender, ) -> 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()); diff --git a/src/login.rs b/src/login.rs index f6120aa..6d912de 100644 --- a/src/login.rs +++ b/src/login.rs @@ -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 diff --git a/src/main.rs b/src/main.rs index 4de7aab..cae8780 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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>> = Lazy::new(|| Mutex::new(HashSet::new())); +pub static JOINED_CHANNELS: Lazy>> = + Lazy::new(|| Mutex::new(HashSet::new())); pub static SENDER: Lazy>>> = 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 = 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(()) diff --git a/src/messages.rs b/src/messages.rs index e85b8ac..6cae5e7 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -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, diff --git a/src/sender.rs b/src/sender.rs index 2623d3b..67bec6e 100644 --- a/src/sender.rs +++ b/src/sender.rs @@ -8,7 +8,8 @@ use tokio::{ pub struct IrcResponse { pub sender: Option, pub command: String, - pub receiver: String, + pub receiver: Option, + pub arguments: Vec, 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, 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 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, } } diff --git a/src/user.rs b/src/user.rs index 184c464..2e24e7b 100644 --- a/src/user.rs +++ b/src/user.rs @@ -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, pub username: Option, @@ -47,7 +47,7 @@ impl UserUnwrapped { format!( "{}!~{}@{}", self.nickname.clone(), - self.realname.clone(), + self.username.clone(), "unimplement.ed" ) }