Got closer to getting chat context to work and added doc comments.

This commit is contained in:
Thelie 2021-05-31 01:04:09 +02:00
parent 9fcbb5ad67
commit 4980c9e24c
4 changed files with 400 additions and 43 deletions

4
Cargo.lock generated
View file

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "ascii_utils" name = "ascii_utils"
version = "0.9.3" version = "0.9.3"
@ -348,7 +350,7 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]] [[package]]
name = "mention_2_mail" name = "mention_2_mail"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"irc-proto", "irc-proto",
"irc_stream", "irc_stream",

View file

@ -66,7 +66,8 @@ fn main() {
None => server_configs = vec![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![]; let mut server_threads = vec![];
for s_conf in server_configs { for s_conf in server_configs {
@ -83,11 +84,22 @@ fn main() {
loop { loop {
match rx.recv() { match rx.recv() {
Ok(data) => { Ok((email_address, server, channel, context)) => {
println!("Sending mail to: {}", data[0]); println!("Sending mail to: {}", email_address);
let id = "notarealiid@mention2mail".to_owned(); 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( let mail = lettre::SendableEmail::new(
lettre::Envelope::new( lettre::Envelope::new(
Some( Some(
@ -95,7 +107,7 @@ fn main() {
.unwrap() .unwrap()
), ),
vec![ vec![
lettre::EmailAddress::new(data[0].clone()).unwrap()] lettre::EmailAddress::new(email_address.clone()).unwrap()]
).unwrap(), ).unwrap(),
id, id,
body body

View file

@ -17,6 +17,7 @@
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
collections::HashMap,
sync::mpsc::Sender, sync::mpsc::Sender,
}; };
use irc_proto::command::{ use irc_proto::command::{
@ -30,6 +31,52 @@ use toml::map::{
use irc_proto::message::Message; use irc_proto::message::Message;
use toml::value::Value; 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<const SIZE: usize> {
buf: [Option<[String; 2]>; SIZE],
ptr: usize,
}
impl <const SIZE: usize> MessageRingBuf<SIZE> {
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 /// Constructs a VecDeque with the messages
/// required to identify with an IRC server. /// required to identify with an IRC server.
/// Uses the credentials set in -`config.toml`. /// Uses the credentials set in -`config.toml`.
@ -82,13 +129,11 @@ fn get_irc_identify_messages (config: &Value)
/// Appends a given VecDeque to include the messages /// Appends a given VecDeque to include the messages
/// used to join the channels defined in `config.toml`. /// used to join the channels defined in `config.toml`.
fn get_irc_join_messages(config: &Value, queue: VecDeque<Message>) fn get_irc_join_messages(channels: &Vec<Value>, queue: VecDeque<Message>)
-> Result<VecDeque<Message>, Box<dyn std::error::Error>> { -> Result<VecDeque<Message>, Box<dyn std::error::Error>> {
let mut queue = queue; let mut queue = queue;
match config.get("channels") { for channel in channels {
Some(c) => {
for channel in c.as_array().ok_or("Could not parse channels.")? {
queue.push_back(Message::from( queue.push_back(Message::from(
Command::JOIN( Command::JOIN(
String::from(channel.as_str().ok_or("Could not parse one of the channels")?), String::from(channel.as_str().ok_or("Could not parse one of the channels")?),
@ -97,15 +142,19 @@ fn get_irc_join_messages(config: &Value, queue: VecDeque<Message>)
) )
)) ))
} }
},
None => ()
}
Ok(queue) Ok(queue)
} }
fn handle_message(message: Message, nick: &String, client_names: Keys) /// Checks messages for mentions of users or commands in private messages.
-> (Option<Message>, Option<[String; 4]>) { /// 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 <const SIZE: usize> (message: Message, nick: &String,
client_names: Keys, contexts: &mut HashMap<String, MessageRingBuf<SIZE>>)
-> (Option<Message>, Option<(String, String, [Option<[String; 2]>; SIZE])>) {
let sender = match message.clone().source_nickname() { let sender = match message.clone().source_nickname() {
Some(s) => String::from(s), Some(s) => String::from(s),
None => String::from("anonymous") None => String::from("anonymous")
@ -132,14 +181,23 @@ fn handle_message(message: Message, nick: &String, client_names: Keys)
), ),
None None
) )
} } else {
match contexts.get_mut(&rec.clone()) {
Some(ref buf) => {
buf.push([sender.clone(), msg.clone()]);
for c_name in client_names { for c_name in client_names {
if msg.contains(c_name) { if msg.contains(c_name) {
return (None, Some([c_name.clone(), return (None, Some((
c_name.clone(),
rec.clone(), rec.clone(),
sender.clone(), buf.get_queue(),
msg.clone() )))
])) }
}
}
None => {
println!("Channel not recognized: {}", rec.clone());
}
} }
} }
}, },
@ -148,12 +206,20 @@ fn handle_message(message: Message, nick: &String, client_names: Keys)
return (None, None) 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 <const SIZE: usize>
(config: Value, tx: Sender<(String, String, String, [Option<[String; 2]>; SIZE])>)
-> Result<(), Box<dyn std::error::Error>> { -> Result<(), Box<dyn std::error::Error>> {
let server_name = config.get("server").unwrap().as_str() let server_name = config.get("server").unwrap().as_str()
.ok_or("Could not get server adress from config")?; .ok_or("Could not get server adress from config")?;
let nick = config.get("nickname").unwrap().as_str() let nick = config.get("nickname").unwrap().as_str()
.ok_or("Could not get nickname from config")?.to_owned(); .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; let clients;
match config.get("clients").unwrap().as_table() { match config.get("clients").unwrap().as_table() {
@ -161,6 +227,16 @@ pub fn handle_server(config: Value, tx: Sender<[String; 5]>)
None => clients = Map::new(), 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 stream = super::connect::connect_irc(&config).unwrap();
let mut message_queue = get_irc_identify_messages(&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(); 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. // Wait for first ping and join channels after sending pong.
loop { loop {
@ -190,24 +266,28 @@ pub fn handle_server(config: Value, tx: Sender<[String; 5]>)
// Handle all incoming messages after joining the channels. // Handle all incoming messages after joining the channels.
loop { loop {
let message = stream.read()?; 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 { match answer {
Some(a) => stream.write(a).unwrap(), Some(a) => stream.write(a).unwrap(),
None => () None => ()
} }
match data { match data {
Some(d) => match clients.get(&d[0]) { Some((client, channel, context)) => match clients.get(&client) {
Some(addr) => { Some(addr) => {
// There must be a better way to do this… let out = (
let out = [
addr.as_str() addr.as_str()
.ok_or("Could not parse email address.")? .ok_or("Could not parse email address.")?
.to_owned(), .to_owned(),
server_name.to_owned(), server_name.to_owned(),
d[1].clone(), channel.clone(),
d[2].clone(), context.clone(),
d[3].clone(), );
];
tx.send(out)? tx.send(out)?
}, },
None => (), None => (),

263
ßqa Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<const SIZE: usize> {
buf: [Option<[String; 3]>; SIZE],
ptr: usize,
}
impl <const SIZE: usize> MessageRingBuf<SIZE> {
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<String>; 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<VecDeque<Message>, Box<dyn std::error::Error>> {
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<Message>)
-> Result<VecDeque<Message>, Box<dyn std::error::Error>> {
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<String, RingBuffer<String>>)
-> (Option<Message>, 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<dyn std::error::Error>> {
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 => (),
}
}
}