An IRC bot that sends an email to users when they are mentioned.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

255 lines
6.2 KiB

/* 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/>.
*/
pub mod connect;
pub mod config;
use std::{
env::args,
path::PathBuf,
collections::VecDeque,
sync::mpsc::{
channel,
Sender,
},
thread,
};
use irc_proto::command::{
CapSubCommand,
Command,
};
use irc_proto::message::Message;
use irc_stream::{
IrcRead,
IrcWrite,
};
use toml::value::Value;
trait IrcReadWrite: IrcRead + IrcWrite {}
impl<T: IrcRead + IrcWrite> IrcReadWrite for T {}
/// 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) -> (Option<Message>, Option<[String; 3]>) {
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) => {
return (None, Some([rec.clone(), sender.clone(), msg.clone()]))
},
_ => println!("{}", message.clone().to_string())
}
return (None, None)
}
fn handle_server(config: Value, tx: Sender<[String; 4]>)
-> 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 mut stream = 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);
match answer {
Some(a) => stream.write(a).unwrap(),
None => ()
}
match data {
Some(d) => {
// There must be a better way to do this…
let d = [
server_name.to_owned(),
d[0].clone(),
d[1].clone(),
d[2].clone()
];
tx.send(d)?
},
None => (),
}
}
}
fn main() {
let mut server_conf_flag = false;
let mut config_path = None;
let mut server_conf_path = None;
for arg in args().skip(1) {
match arg.as_str() {
"-s" => server_conf_flag = true,
"-h" => {
println!("Usage: mention2mail [config file] [-s server config directory]");
},
_ => if server_conf_flag {
server_conf_path = Some(arg);
} else {
config_path = Some(arg);
}
}
}
let config;
match config_path {
Some(p) => config = config::get_main_config(PathBuf::from(p))
.expect("Could not get config"),
None => config = config::get_main_config(PathBuf::from("/etc/Mention2Mail/config.toml"))
.expect("Could not get default config in /etc/Mention2Mail/default.toml"),
}
let server_configs;
match server_conf_path {
Some(p) => server_configs = config::get_server_configs(config, PathBuf::from(p))
.expect("Could not get server config."),
None => server_configs = vec![config],
}
let (tx,rx) = channel();
let mut server_threads = vec![];
for s_conf in server_configs {
let t = tx.clone();
server_threads.push(thread::Builder::new()
.name("server name here".to_string())
.spawn(move || {
handle_server(s_conf, t).unwrap();
})
);
}
loop {
match rx.recv() {
Ok(data) => println!("{},{},{},{}",
data[0],
data[1],
data[2],
data[3]),
Err(_e) => (),
}
}
}