IRC bot for GamingOnLinux.com, written in Rust.
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.
 
 

379 lines
9.9 KiB

extern crate regex;
extern crate toml;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate lazy_static;
extern crate xdg;
use regex::Regex;
use toml::Value;
use std::env;
use std::io::prelude::*;
use std::io::Error;
use std::net::TcpStream;
use std::sync::Arc;
use std::sync::mpsc::*;
use std::thread;
use std::boxed::Box;
use std::time::Duration;
use std::fs::File;
use std::path::Path;
use std::path::PathBuf;
mod plugin;
use self::plugin::*;
use self::plugin::pingpong::PingPong;
use self::plugin::rss_plugin::RssReader;
// use plugin::hello::Hello;
use self::plugin::seen::LastSeen;
use self::plugin::tell::Tell;
use self::plugin::url_reader::UrlReader;
use self::plugin::notice::Notice;
#[derive(Deserialize)]
pub struct BotConfig {
nick: String,
serv: String,
channel: String,
realname: String,
ident: String,
password: Option<String>,
}
impl BotConfig {
fn new(
nick: &str,
serv: &str,
channel: &str,
realname: &str,
ident: &str,
password: &str,
) -> BotConfig {
return BotConfig {
nick: String::from(nick),
serv: String::from(serv),
channel: String::from(channel),
realname: String::from(realname),
ident: String::from(ident),
password: Some(String::from(password)),
};
}
fn debug_defaults() -> BotConfig {
return BotConfig::new("GOLBOTv3", "chat.freenode.net", "#golbottest", "", "", "");
}
}
fn send_sock(sock: &mut TcpStream, msg: String) -> Result<usize, Error> {
sock.write(msg.as_bytes())
}
fn recv_sock(sock: &mut TcpStream) -> Result<String, String> {
let mut buffer: [u8; 1024] = [0 as u8; 1024];
let res = sock.read(&mut buffer);
let mut vec_buffer: Vec<u8> = Vec::new();
for byte in buffer.iter() {
vec_buffer.push(byte.clone());
}
let msg_result = String::from_utf8(vec_buffer);
if msg_result.is_err() {
return Ok(String::from("incorrect_utf8"));
}
let mut msg = msg_result.unwrap();
match res {
Ok(size) => {
if size == 0 {
println!("We should be cutting the connection here.");
return Err(String::from("Empty string, cutting loose"));
}
msg.truncate(size);
Ok(msg)
}
Err(_) => Err(String::from("Failed to read!")),
}
}
fn form_chan_msg(conf: &BotConfig, msg: String) -> String {
return format!("PRIVMSG {} :{}\r\n", conf.channel, msg);
}
fn parse_chan_msg(msg: &String) -> Option<(String, String, String)> {
if msg.contains("PRIVMSG") {
lazy_static! {
static ref CHAN_MSG_REGEX: Regex = Regex::new(r":(.+)!.+(#.*) :(.+)").unwrap();
}
println!("{}", &msg);
let chan_msg = CHAN_MSG_REGEX.captures(msg);
if chan_msg.is_some() {
println!("This is a chan message");
let chan_msg = chan_msg.unwrap();
let nick = chan_msg.get(1).unwrap();
let chan = chan_msg.get(2).unwrap();
let msg = chan_msg.get(3).unwrap();
return Some((
String::from(nick.as_str()),
String::from(chan.as_str()),
String::from(msg.as_str()),
));
}
}
return None;
}
fn read_bot_config(file: &Path) -> Option<BotConfig> {
if file.is_file() {
let mut fob = File::open(file).unwrap();
let mut text = String::new();
fob.read_to_string(&mut text).unwrap();
let result: Result<BotConfig, toml::de::Error> = toml::from_str(text.as_str());
match result {
Ok(config) => return Some(config),
Err(e) => {
println!("Couldn't load config file: {}", e);
return None;
}
}
}
println!("Couldn't load config file: File not found.");
return None;
}
fn read_plugin_config(file: &Path) -> Option<Value> {
if file.is_file() {
let mut fob = File::open(file).unwrap();
let mut text = String::new();
fob.read_to_string(&mut text).unwrap();
return Some(text.as_str().parse::<Value>().unwrap());
} else {
return None;
}
}
struct IRCBot {
config: Arc<BotConfig>,
pl_config: Arc<Value>,
plugins: Vec<Box<dyn Plugin>>,
}
impl IRCBot {
fn new(config: Arc<BotConfig>, pl_config: Arc<Value>) -> IRCBot {
return IRCBot {
config,
pl_config,
plugins: Vec::new(),
};
}
fn sender(
receiver: Receiver<String>,
socket: TcpStream,
) {
let mut sock = socket;
for line in receiver.iter() {
println!("-> {}", line);
let _result = send_sock(&mut sock, line);
thread::sleep(Duration::from_millis(50));
}
}
fn receiver(
sender: Sender<String>,
socket: TcpStream,
) {
let mut sock = socket;
loop {
let line = recv_sock(&mut sock);
match line {
Ok(s) => {
println!("<- {}", s);
sender.send(s).unwrap();
}
Err(_e) => {
panic!("Socket received empty string, connection broken");
}
}
thread::sleep(Duration::from_millis(50));
}
}
fn add_plugin(&mut self, plugin: Box<dyn Plugin>) {
self.plugins.push(plugin);
}
fn start(&mut self) -> Result<i32, String> {
println!("Connecting to the server...");
let socket = TcpStream::connect(format!("{}:6667", &self.config.serv))
.expect("Couldn't connect to server!");
let sock2 = socket.try_clone().unwrap();
let (out_sender, out_receiver) = channel::<String>();
thread::spawn(move || IRCBot::sender(out_receiver, socket));
let (in_sender, in_receiver) = channel::<String>();
thread::spawn(move || IRCBot::receiver(in_sender, sock2));
out_sender.send(format!("NICK {}\r\n", self.config.nick)).unwrap();
out_sender.send(format!(
"USER {} {} bla :{}\r\n",
self.config.ident, self.config.serv, self.config.realname
)).unwrap();
match &self.config.password {
Some(password) => {
out_sender.send(format!(
"PRIVMSG NickServ :identify {} {}\r\n",
self.config.nick, password
)).unwrap();
// Wait until identified
loop {
let msg = in_receiver.recv().unwrap();
if msg.contains("You are now identified for ") {
break;
}
thread::sleep(Duration::from_millis(20));
}
}
None => (),
}
out_sender.send(format!("JOIN {}\r\n", self.config.channel)).unwrap();
for plugin in &mut self.plugins {
plugin.init(out_sender.clone());
}
'mainloop: loop {
match in_receiver.recv() {
Ok(line) => {
if line.starts_with("ERROR:") {
return Err(String::from("An error occured, line severed."));
} else if line.len() == 0 {
return Err(String::from(
"Received an empty message, likely broken connection.",
));
}
if line.len() > 0 {
for pl in &mut self.plugins {
pl.process_line(&line);
}
}
},
Err(_e) => {
return Err(String::from("Receiver cut out."));
}
}
thread::sleep(Duration::from_millis(50));
}
}
}
fn main() -> Result<(), &'static str> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("golbot").unwrap();
let config_path = if let Ok(val) = std::env::var("GOLBOT_PATH") {
PathBuf::from(val).join("botconfig.toml")
} else {
xdg_dirs.place_config_file("botconfig.toml").expect("Couldn't create configuration.")
};
let plugin_config_path = if let Ok(val) = std::env::var("GOLBOT_PATH") {
PathBuf::from(val).join("plugins.toml")
} else {
xdg_dirs.place_config_file("plugins.toml").expect("Couldn't create configuration.")
};
let config = Arc::new(read_bot_config(&config_path).unwrap());
let pl_config = Arc::new(read_plugin_config(&plugin_config_path).unwrap());
println!("{}", pl_config.to_string());
let mut bot = IRCBot::new(config.clone(), pl_config.clone());
let conf = bot.config.clone();
let pl_conf = bot.pl_config.clone();
bot.add_plugin(Box::new(PingPong::new()));
bot.add_plugin(Box::new(LastSeen::new(conf.clone())));
bot.add_plugin(Box::new(UrlReader::new(conf.clone(), pl_conf.clone())));
bot.add_plugin(Box::new(Tell::new(conf.clone())));
bot.add_plugin(Box::new(Notice::new(conf.clone(), pl_conf.clone())));
bot.add_plugin(Box::new(RssReader::new(conf.clone(), pl_conf.clone())));
let resp = bot.start();
return match &resp {
Err(e) => {
println!("{}", e);
Err("Bot crashed.")
},
Ok(_a) => {
println!("Bot exited gracefully.");
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recognizes_channel_message() {
let _config = BotConfig::debug_defaults();
let message = String::from(":testing!test@testing PRIVMSG #golbottest :hello");
assert!(parse_chan_msg(&message).is_some());
}
#[test]
fn form_chan_msg_is_valid() {
let config = BotConfig::debug_defaults();
let msg = form_chan_msg(&config, String::from("Hello, world!"));
assert_eq!(msg, String::from("PRIVMSG #golbottest :Hello, world!\r\n"));
}
}