diff --git a/Cargo.lock b/Cargo.lock index d135573..3699c22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "ascii_utils" version = "0.9.3" @@ -348,7 +350,7 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "mention_2_mail" -version = "0.1.0" +version = "0.2.0" dependencies = [ "irc-proto", "irc_stream", diff --git a/src/main.rs b/src/main.rs index 729b0d8..22e1871 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,8 +65,9 @@ fn main() { .expect("Could not get server config."), None => server_configs = vec![config], } - - let (tx,rx) = channel(); + + // TODO: This line has many problems… + let (tx,rx): (std::sync::mpsc::Sender<(String, String, String, [Option<[String; 2]>; 5])>, std::sync::mpsc::Receiver<(String, String, String, [Option<[String; 2]>; 5])>)= channel(); let mut server_threads = vec![]; for s_conf in server_configs { @@ -83,11 +84,22 @@ fn main() { loop { match rx.recv() { - Ok(data) => { - println!("Sending mail to: {}", data[0]); + Ok((email_address, server, channel, context)) => { + println!("Sending mail to: {}", email_address); let id = "notarealiid@mention2mail".to_owned(); - let body = format!("Subject: You were mentioned in {} on {}\n{} wrote: {}", - data[2], data[1], data[3], data[4]).into_bytes(); + + let mut body = format!("Subject: You were mentioned in {} on {}", + channel, server); + for opt in context.iter() { + match opt{ + Some([sender, msg]) => body.push_str( + &format!("{} wrote: {}", sender, msg) + ), + None => (), + } + } + let body = body.into_bytes(); + let mail = lettre::SendableEmail::new( lettre::Envelope::new( Some( @@ -95,7 +107,7 @@ fn main() { .unwrap() ), vec![ - lettre::EmailAddress::new(data[0].clone()).unwrap()] + lettre::EmailAddress::new(email_address.clone()).unwrap()] ).unwrap(), id, body diff --git a/src/servers.rs b/src/servers.rs index 47fe91d..1dd1393 100644 --- a/src/servers.rs +++ b/src/servers.rs @@ -17,6 +17,7 @@ use std::{ collections::VecDeque, + collections::HashMap, sync::mpsc::Sender, }; use irc_proto::command::{ @@ -30,6 +31,52 @@ use toml::map::{ use irc_proto::message::Message; use toml::value::Value; +/// A static implementation of a ringbuffer. +/// It holds arrays of three strings each, +/// which are meant to represent the author and message respectively. +struct MessageRingBuf { + buf: [Option<[String; 2]>; SIZE], + ptr: usize, +} + +impl MessageRingBuf { + pub fn new() -> Self { + Self{ + buf: [None; SIZE], + ptr: 0 + } + } + + /// Adds a message to the end of the buffer, + /// overwriting the first value if the buffer is full. + pub fn push(&mut self, val: [String; 2]) { + self.ptr = (self.ptr + 1) % SIZE; + self.buf[self.ptr] = Some(val); + } + + /// Returns the last element of the buffer, + /// replacing it with none. + pub fn pop(&mut self) -> Option<[String; 2]> { + self.ptr = (self.ptr + SIZE - 1) % SIZE; + self.buf[self.ptr].take() + } + + /// Returns the contained buffer in the correct order. + pub fn get_queue(& self) -> [Option<[String; 2]>; SIZE] { + let mut queue = self.buf.clone(); + + for i in self.ptr .. SIZE { + queue[i - self.ptr] = self.buf[i].clone(); + } + + for i in 0 .. self.ptr { + queue[SIZE - self.ptr + i] = self.buf[i].clone(); + } + + return queue + } +} + /// Constructs a VecDeque with the messages /// required to identify with an IRC server. /// Uses the credentials set in -`config.toml`. @@ -82,30 +129,32 @@ fn get_irc_identify_messages (config: &Value) /// Appends a given VecDeque to include the messages /// used to join the channels defined in `config.toml`. -fn get_irc_join_messages(config: &Value, queue: VecDeque) +fn get_irc_join_messages(channels: &Vec, queue: VecDeque) -> Result, Box> { let mut queue = queue; - match config.get("channels") { - Some(c) => { - for channel in c.as_array().ok_or("Could not parse channels.")? { - queue.push_back(Message::from( - Command::JOIN( - String::from(channel.as_str().ok_or("Could not parse one of the channels")?), - None, - None - ) - )) - } - }, - None => () + for channel in channels { + queue.push_back(Message::from( + Command::JOIN( + String::from(channel.as_str().ok_or("Could not parse one of the channels")?), + None, + None + ) + )) } Ok(queue) } -fn handle_message(message: Message, nick: &String, client_names: Keys) - -> (Option, Option<[String; 4]>) { +/// Checks messages for mentions of users or commands in private messages. +/// Returns either one of: +/// - None and None in the case no mention or command is recognized +/// - The appropriate answer as Message and None for a command +/// - None and the mentioned user and channel as String and the context as a MessageRingBuf for mentions. +fn handle_message (message: Message, nick: &String, + client_names: Keys, contexts: &mut HashMap>) + -> (Option, Option<(String, String, [Option<[String; 2]>; SIZE])>) { + let sender = match message.clone().source_nickname() { Some(s) => String::from(s), None => String::from("anonymous") @@ -132,14 +181,23 @@ fn handle_message(message: Message, nick: &String, client_names: Keys) ), None ) - } - for c_name in client_names { - if msg.contains(c_name) { - return (None, Some([c_name.clone(), - rec.clone(), - sender.clone(), - msg.clone() - ])) + } else { + match contexts.get_mut(&rec.clone()) { + Some(ref buf) => { + buf.push([sender.clone(), msg.clone()]); + for c_name in client_names { + if msg.contains(c_name) { + return (None, Some(( + c_name.clone(), + rec.clone(), + buf.get_queue(), + ))) + } + } + } + None => { + println!("Channel not recognized: {}", rec.clone()); + } } } }, @@ -148,18 +206,36 @@ fn handle_message(message: Message, nick: &String, client_names: Keys) return (None, None) } -pub fn handle_server(config: Value, tx: Sender<[String; 5]>) +/// Connects to a server and indefinitely iterates the messages received. +/// Calls handle_message on each one and pushes +/// email, server, channel as Strings +/// and the context as MessageRingBuf to the given stream. +pub fn handle_server + (config: Value, tx: Sender<(String, String, String, [Option<[String; 2]>; SIZE])>) -> Result<(), Box> { + let server_name = config.get("server").unwrap().as_str() .ok_or("Could not get server adress from config")?; let nick = config.get("nickname").unwrap().as_str() .ok_or("Could not get nickname from config")?.to_owned(); - + let channels = config.get("channels").unwrap().as_array() + .ok_or("Could not get channels from config")?; + let clients; match config.get("clients").unwrap().as_table() { Some(t) => clients = t.clone(), None => clients = Map::new(), } + + let mut chat_contexts = HashMap::new(); + for channel in channels { + chat_contexts.insert( + channel.as_str() + .ok_or("Could not parse one of the channels")? + .to_owned(), + MessageRingBuf::new(), + ); + } let mut stream = super::connect::connect_irc(&config).unwrap(); let mut message_queue = get_irc_identify_messages(&config).unwrap(); @@ -168,7 +244,7 @@ pub fn handle_server(config: Value, tx: Sender<[String; 5]>) stream.write(message).unwrap(); } - message_queue = get_irc_join_messages(&config, message_queue).unwrap(); + message_queue = get_irc_join_messages(&channels, message_queue).unwrap(); // Wait for first ping and join channels after sending pong. loop { @@ -190,24 +266,28 @@ pub fn handle_server(config: Value, tx: Sender<[String; 5]>) // Handle all incoming messages after joining the channels. loop { let message = stream.read()?; - let (answer, data) = handle_message(message, &nick, clients.keys()); + let (answer, data) = handle_message( + message, + &nick, + clients.keys(), + &mut chat_contexts + ); + match answer { Some(a) => stream.write(a).unwrap(), None => () } match data { - Some(d) => match clients.get(&d[0]) { + Some((client, channel, context)) => match clients.get(&client) { Some(addr) => { - // There must be a better way to do this… - let out = [ + let out = ( addr.as_str() .ok_or("Could not parse email address.")? .to_owned(), server_name.to_owned(), - d[1].clone(), - d[2].clone(), - d[3].clone(), - ]; + channel.clone(), + context.clone(), + ); tx.send(out)? }, None => (), diff --git a/ßqa b/ßqa new file mode 100644 index 0000000..56be040 --- /dev/null +++ b/ßqa @@ -0,0 +1,263 @@ +/* Copyright 2021 Daniel Mowitz + * This file is part of Mention2Mail. + * + * Mention2Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Mention2Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Mention2Mail. If not, see . + */ + +use std::{ + collections::VecDeque, + collections::HashMap, + sync::mpsc::Sender, +}; +use irc_proto::command::{ + CapSubCommand, + Command, +}; +use toml::map::{ + Map, + Keys +}; +use irc_proto::message::Message; +use toml::value::Value; + +struct MessageRingBuf { + buf: [Option<[String; 3]>; SIZE], + ptr: usize, +} + +impl MessageRingBuf { + pub fn new() -> Self { + Self{ + buf: [None; SIZE], + ptr: 0 + } + } + + pub fn push(&mut self, val: [String; 3]) { + self.ptr = (self.ptr + 1) % SIZE; + self.buf[self.ptr] = Some(val); + } + + pub fn pop(&mut self) -> Option<[String; 3]> { + self.buf[self.ptr].take() + } + + pub fn get_queue(& self) -> [Option; SIZE] { + let mut queue = self.buf.clone(); + + for i in self.ptr .. SIZE { + queue[i - self.ptr] = self.buf[i]; + } + + for i in 0 .. self.ptr { + queue[SIZE - self.ptr + i] = self.buf[i]; + } + + return queue + } +} + +/// Constructs a VecDeque with the messages +/// required to identify with an IRC server. +/// Uses the credentials set in -`config.toml`. +fn get_irc_identify_messages (config: &Value) + -> Result, Box> { + let mut queue = VecDeque::new(); + + queue.push_back(Message::from( + Command::CAP( + None, + CapSubCommand::END, + None, + None + ))); + + match config.get("password") { + Some(p) => queue.push_back(Message::from( + Command::PASS( + String::from(p.as_str().ok_or("Could not parse password.")?) + ) + )), + None => () + } + + let nick: String; + match config.get("nickname") { + Some(n) => { + nick = String::from(n.as_str().ok_or("Could not parse nickname.")?); + queue.push_back(Message::from(Command::NICK(nick.clone()))); + }, + None => return Err("No nickname supplied!".into()), + } + + match config.get("username") { + Some(u) => queue.push_back(Message::from( + Command::USER( + String::from(u.as_str().ok_or("Could not parse username.")?), + "0".to_owned(), + String::from(u.as_str().ok_or("Could not parse username.")?) + ) + )), + None => queue.push_back(Message::from( + // nick.clone() is only used once because the value + // can be moved the second time + Command::USER(nick.clone(), "0".to_owned(), nick) + )) + } + Ok(queue) +} + +/// Appends a given VecDeque to include the messages +/// used to join the channels defined in `config.toml`. +fn get_irc_join_messages(config: &Value, queue: VecDeque) + -> Result, Box> { + let mut queue = queue; + + match config.get("channels") { + Some(c) => { + for channel in c.as_array().ok_or("Could not parse channels.")? { + queue.push_back(Message::from( + Command::JOIN( + String::from(channel.as_str().ok_or("Could not parse one of the channels")?), + None, + None + ) + )) + } + }, + None => () + } + + Ok(queue) +} + +fn handle_message(message: Message, nick: &String, + client_names: Keys, contexts: HashMap>) + -> (Option, Option<[String; 4]>) { + let sender = match message.clone().source_nickname() { + Some(s) => String::from(s), + None => String::from("anonymous") + }; + match message.clone().command { + Command::PING(ref data, _) => { + return ( + Some(Message::from( + Command::PONG(data.to_owned(), None) + )), + None + ); + }, + Command::PRIVMSG(ref rec, ref msg) => { + // Send bot info on receiving a private message. + if rec.as_str() == nick.as_str() { + return ( + Some(Message::from( + Command::PRIVMSG( + sender.clone(), + super::texts::get_bot_info(), + ) + ) + ), + None + ) + } else { + match contexts.get_mut(sender.clone()) { + Some(ref buf) => buf.push( + for c_name in client_names { + if msg.contains(c_name) { + return (None, Some([c_name.clone(), + rec.clone(), + sender.clone(), + msg.clone() + ])) + } + } + } + }, + _ => println!("{}", message.clone().to_string()) + } + return (None, None) +} + +pub fn handle_server(config: Value, tx: Sender<[String; 5]>) + -> Result<(), Box> { + let server_name = config.get("server").unwrap().as_str() + .ok_or("Could not get server adress from config")?; + let nick = config.get("nickname").unwrap().as_str() + .ok_or("Could not get nickname from config")?.to_owned(); + + let clients; + match config.get("clients").unwrap().as_table() { + Some(t) => clients = t.clone(), + None => clients = Map::new(), + } + + let mut chat_context = HashMap::new(); + + let mut stream = super::connect::connect_irc(&config).unwrap(); + let mut message_queue = get_irc_identify_messages(&config).unwrap(); + + while let Some(message) = message_queue.pop_front() { + stream.write(message).unwrap(); + } + + message_queue = get_irc_join_messages(&config, message_queue).unwrap(); + + // Wait for first ping and join channels after sending pong. + loop { + let message = stream.read()?; + match message.command { + Command::PING(ref data, _) => { + stream.write(Message::from( + Command::PONG(data.to_owned(), None) + )).unwrap(); + while let Some(message) = message_queue.pop_front() { + stream.write(message).unwrap(); + } + break; + } + _ => () + } + } + + // Handle all incoming messages after joining the channels. + loop { + let message = stream.read()?; + let (answer, data) = handle_message(message, &nick, clients.keys()); + match answer { + Some(a) => stream.write(a).unwrap(), + None => () + } + match data { + Some(d) => match clients.get(&d[0]) { + Some(addr) => { + // There must be a better way to do this… + let out = [ + addr.as_str() + .ok_or("Could not parse email address.")? + .to_owned(), + server_name.to_owned(), + d[1].clone(), + d[2].clone(), + d[3].clone(), + ]; + tx.send(out)? + }, + None => (), + }, + None => (), + } + } +} +