diff options
Diffstat (limited to 'src/plugins/factoids')
| -rw-r--r-- | src/plugins/factoids/database.rs | 151 | ||||
| -rw-r--r-- | src/plugins/factoids/mod.rs | 330 | ||||
| -rw-r--r-- | src/plugins/factoids/sandbox.lua | 51 | ||||
| -rw-r--r-- | src/plugins/factoids/utils.rs | 21 |
4 files changed, 553 insertions, 0 deletions
diff --git a/src/plugins/factoids/database.rs b/src/plugins/factoids/database.rs new file mode 100644 index 0000000..b612d6f --- /dev/null +++ b/src/plugins/factoids/database.rs @@ -0,0 +1,151 @@ +#[cfg(feature = "mysql")] +extern crate dotenv; + +use std::collections::HashMap; + +#[cfg(feature = "mysql")] +use diesel::prelude::*; + +#[cfg(feature = "mysql")] +use diesel::mysql::MysqlConnection; + +use chrono::NaiveDateTime; + +pub enum DbResponse { + Success, + Failed(&'static str), +} + +#[cfg_attr(feature = "mysql", derive(Queryable))] +#[derive(Clone, Debug)] +pub struct Factoid { + pub name: String, + pub idx: i32, + pub content: String, + pub author: String, + pub created: NaiveDateTime, +} + +#[cfg(feature = "mysql")] +use self::mysql::factoids; +#[cfg_attr(feature = "mysql", derive(Insertable))] +#[cfg_attr(feature = "mysql", table_name="factoids")] +pub struct NewFactoid<'a> { + pub name: &'a str, + pub idx: i32, + pub content: &'a str, + pub author: &'a str, + pub created: NaiveDateTime, +} + + +pub trait Database: Send { + fn insert(&mut self, factoid: &NewFactoid) -> DbResponse; + fn get(&self, name: &str, idx: i32) -> Option<Factoid>; + fn delete(&mut self, name: &str, idx: i32) -> DbResponse; + fn count(&self, name: &str) -> Result<i32, &'static str>; +} + +// HashMap +impl Database for HashMap<(String, i32), Factoid> { + fn insert(&mut self, factoid: &NewFactoid) -> DbResponse { + let factoid = Factoid { + name: String::from(factoid.name), + idx: factoid.idx, + content: factoid.content.to_string(), + author: factoid.author.to_string(), + created: factoid.created, + }; + + let name = String::from(factoid.name.clone()); + match self.insert((name, factoid.idx), factoid) { + None => DbResponse::Success, + Some(_) => DbResponse::Failed("Factoid was overwritten"), + } + } + + fn get(&self, name: &str, idx: i32) -> Option<Factoid> { + self.get(&(String::from(name), idx)).map(|f| f.clone()) + } + + fn delete(&mut self, name: &str, idx: i32) -> DbResponse { + match self.remove(&(String::from(name), idx)) { + Some(_) => DbResponse::Success, + None => DbResponse::Failed("Factoid not found"), + } + } + + fn count(&self, name: &str) -> Result<i32, &'static str> { + Ok(self.iter() + .filter(|&(&(ref n, _), _)| n == name) + .count() as i32) + } +} + +// Diesel automatically define the factoids module as public. +// For now this is how we keep it private. +#[cfg(feature = "mysql")] +mod mysql { + table! { + factoids (name, idx) { + name -> Varchar, + idx -> Integer, + content -> Text, + author -> Varchar, + created -> Timestamp, + } + } +} + +#[cfg(feature = "mysql")] +impl Database for MysqlConnection { + fn insert(&mut self, factoid: &NewFactoid) -> DbResponse { + use diesel; + match diesel::insert_into(factoids::table) + .values(factoid) + .execute(self) { + Ok(_) => DbResponse::Success, + Err(e) => { + error!("DB Insertion Error: \"{}\"", e); + DbResponse::Failed("Failed to add factoid") + } + } + } + + fn get(&self, name: &str, idx: i32) -> Option<Factoid> { + factoids::table.find((name, idx)).first(self).ok() + } + + fn delete(&mut self, name: &str, idx: i32) -> DbResponse { + use diesel; + use self::factoids::columns; + match diesel::delete(factoids::table + .filter(columns::name.eq(name)) + .filter(columns::idx.eq(idx))) + .execute(self) { + Ok(v) => { + if v > 0 { + DbResponse::Success + } else { + DbResponse::Failed("Could not find any factoid with that name") + } + } + Err(e) => { + error!("DB Deletion Error: \"{}\"", e); + DbResponse::Failed("Failed to delete factoid") + } + } + } + + fn count(&self, name: &str) -> Result<i32, &'static str> { + let count: Result<i64, _> = factoids::table + .filter(factoids::columns::name.eq(name)) + .count() + .first(self); + + match count { + Ok(c) => Ok(c as i32), + Err(_) => Err("Database Error"), + } + } +} diff --git a/src/plugins/factoids/mod.rs b/src/plugins/factoids/mod.rs new file mode 100644 index 0000000..49ace10 --- /dev/null +++ b/src/plugins/factoids/mod.rs @@ -0,0 +1,330 @@ +extern crate rlua; + +use std::fmt; +use std::str::FromStr; +use std::sync::Mutex; +use self::rlua::prelude::*; +use irc::client::prelude::*; +use irc::error::IrcError; + +use time; +use chrono::NaiveDateTime; + +use plugin::*; +pub mod database; +use self::database::{Database, DbResponse}; + +mod utils; +use self::utils::*; + +static LUA_SANDBOX: &'static str = include_str!("sandbox.lua"); + +#[derive(PluginName)] +pub struct Factoids<T: Database> { + factoids: Mutex<T>, +} + +macro_rules! try_lock { + ( $m:expr ) => { + match $m.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + } +} + +impl<T: Database> Factoids<T> { + pub fn new(db: T) -> Factoids<T> { + Factoids { factoids: Mutex::new(db) } + } + + fn create_factoid(&self, name: &str, content: &str, author: &str) -> Result<&str, &str> { + let count = try_lock!(self.factoids).count(&name)?; + let tm = time::now().to_timespec(); + + let factoid = database::NewFactoid { + name: name, + idx: count, + content: content, + author: author, + created: NaiveDateTime::from_timestamp(tm.sec, tm.nsec as u32), + }; + + match try_lock!(self.factoids).insert(&factoid) { + DbResponse::Success => Ok("Successfully added"), + DbResponse::Failed(e) => Err(e), + } + } + + fn add(&self, client: &IrcClient, command: &mut PluginCommand) -> Result<(), IrcError> { + if command.tokens.len() < 2 { + return self.invalid_command(client, command); + } + + let name = command.tokens.remove(0); + let content = command.tokens.join(" "); + + match self.create_factoid(&name, &content, &command.source) { + Ok(v) => client.send_notice(&command.source, v), + Err(e) => client.send_notice(&command.source, e), + } + } + + fn from_url(&self, client: &IrcClient, command: &mut PluginCommand) -> Result<(), IrcError> { + if command.tokens.len() < 2 { + return self.invalid_command(client, command); + } + + let name = command.tokens.remove(0); + let url = &command.tokens[0]; + if let Some(content) = ::utils::download(1024, url) { + match self.create_factoid(&name, &content, &command.source) { + Ok(v) => client.send_notice(&command.source, v), + Err(e) => client.send_notice(&command.source, e), + } + } else { + client.send_notice(&command.source, "Failed to download.") + } + } + + fn remove(&self, client: &IrcClient, command: &mut PluginCommand) -> Result<(), IrcError> { + if command.tokens.len() < 1 { + return self.invalid_command(client, command); + } + + let name = command.tokens.remove(0); + let count = match try_lock!(self.factoids).count(&name) { + Ok(c) => c, + Err(e) => return client.send_notice(&command.source, e), + }; + + match try_lock!(self.factoids).delete(&name, count - 1) { + DbResponse::Success => client.send_notice(&command.source, "Successfully removed"), + DbResponse::Failed(e) => client.send_notice(&command.source, &e), + } + } + + fn get(&self, client: &IrcClient, command: &PluginCommand) -> Result<(), IrcError> { + + let (name, idx) = match command.tokens.len() { + 0 => return self.invalid_command(client, command), + 1 => { + let name = &command.tokens[0]; + let count = match try_lock!(self.factoids).count(name) { + Ok(c) => c, + Err(e) => return client.send_notice(&command.source, e), + }; + + if count < 1 { + return client.send_notice(&command.source, &format!("{} does not exist", name)); + } + + (name, count - 1) + } + _ => { + let name = &command.tokens[0]; + let idx = match i32::from_str(&command.tokens[1]) { + Ok(i) => i, + Err(_) => return client.send_notice(&command.source, "Invalid index"), + }; + + (name, idx) + } + }; + + let factoid = match try_lock!(self.factoids).get(name, idx) { + Some(v) => v, + None => { + return client.send_notice(&command.source, + &format!("{}~{} does not exist", name, idx)) + } + }; + + let message = factoid.content.replace("\n", "|").replace("\r", ""); + + client.send_privmsg(&command.target, + &format!("{}: {}", factoid.name, message)) + } + + fn info(&self, client: &IrcClient, command: &PluginCommand) -> Result<(), IrcError> { + + match command.tokens.len() { + 0 => self.invalid_command(client, command), + 1 => { + let name = &command.tokens[0]; + let count = match try_lock!(self.factoids).count(name) { + Ok(c) => c, + Err(e) => return client.send_notice(&command.source, e), + }; + + match count { + 0 => client.send_notice(&command.source, &format!("{} does not exist", name)), + 1 => { + client.send_privmsg(&command.target, + &format!("There is 1 version of {}", name)) + } + _ => { + client.send_privmsg(&command.target, + &format!("There are {} versions of {}", count, name)) + } + } + } + _ => { + let name = &command.tokens[0]; + let idx = match i32::from_str(&command.tokens[1]) { + Ok(i) => i, + Err(_) => return client.send_notice(&command.source, "Invalid index"), + }; + + let factoid = match try_lock!(self.factoids).get(name, idx) { + Some(v) => v, + None => { + return client.send_notice(&command.source, + &format!("{}~{} does not exist", name, idx)) + } + }; + + client.send_privmsg(&command.target, + &format!("{}: Added by {} at {} UTC", + name, + factoid.author, + factoid.created)) + } + + } + } + + fn exec(&self, + client: &IrcClient, + mut command: PluginCommand, + error: bool) + -> Result<(), IrcError> { + if command.tokens.len() < 1 { + self.invalid_command(client, &command) + + } else { + let name = command.tokens.remove(0); + let count = match try_lock!(self.factoids).count(&name) { + Ok(c) => c, + Err(e) => return client.send_notice(&command.source, e), + }; + + let factoid = match try_lock!(self.factoids).get(&name, count - 1) { + Some(v) => v.content, + None if error => return self.invalid_command(client, &command), + None => return Ok(()), + }; + + let value = &if factoid.starts_with(">") { + let factoid = String::from(&factoid[1..]); + + if factoid.starts_with(">") { + factoid + } else { + match self.run_lua(&name, &factoid, &command) { + Ok(v) => v, + Err(e) => format!("\"{}\"", e), + } + } + } else { + factoid + }; + + client.send_privmsg(&command.target, &value.replace("\n", "|").replace("\r", "")) + } + } + + fn run_lua(&self, + name: &str, + code: &str, + command: &PluginCommand) + -> Result<String, rlua::Error> { + + let args = command + .tokens + .iter() + .filter(|x| !x.is_empty()) + .map(ToOwned::to_owned) + .collect::<Vec<String>>(); + + let lua = unsafe { Lua::new_with_debug() }; + let globals = lua.globals(); + + globals.set("factoid", code)?; + globals.set("download", lua.create_function(download)?)?; + globals.set("sleep", lua.create_function(sleep)?)?; + globals.set("args", args)?; + globals.set("input", command.tokens.join(" "))?; + globals.set("user", command.source.clone())?; + globals.set("channel", command.target.clone())?; + globals.set("output", lua.create_table()?)?; + + lua.exec::<()>(LUA_SANDBOX, Some(name))?; + let output: Vec<String> = globals.get::<_, Vec<String>>("output")?; + + Ok(output.join("|")) + } + + fn invalid_command(&self, client: &IrcClient, command: &PluginCommand) -> Result<(), IrcError> { + client.send_notice(&command.source, "Invalid Command") + } +} + +impl<T: Database> Plugin for Factoids<T> { + fn execute(&self, _: &IrcClient, message: &Message) -> ExecutionStatus { + match message.command { + Command::PRIVMSG(_, ref content) => if content.starts_with('!') { + ExecutionStatus::RequiresThread + } else { + ExecutionStatus::Done + }, + _ => ExecutionStatus::Done, + } + } + + fn execute_threaded(&self, client: &IrcClient, message: &Message) -> Result<(), IrcError> { + if let Command::PRIVMSG(_, mut content) = message.command.clone() { + content.remove(0); + + let t: Vec<String> = content.split(' ').map(ToOwned::to_owned).collect(); + + let c = PluginCommand { + source: message.source_nickname().unwrap().to_string(), + target: message.response_target().unwrap().to_string(), + tokens: t, + }; + + self.exec(client, c, false) + + } else { + Ok(()) + } + } + + fn command(&self, client: &IrcClient, mut command: PluginCommand) -> Result<(), IrcError> { + if command.tokens.is_empty() { + return self.invalid_command(client, &command); + } + + let sub_command = command.tokens.remove(0); + match sub_command.as_ref() { + "add" => self.add(client, &mut command), + "fromurl" => self.from_url(client, &mut command), + "remove" => self.remove(client, &mut command), + "get" => self.get(client, &command), + "info" => self.info(client, &command), + "exec" => self.exec(client, command, true), + _ => self.invalid_command(client, &command), + } + } + + fn evaluate(&self, _: &IrcClient, _: PluginCommand) -> Result<String, String> { + Err(String::from("Evaluation of commands is not implemented for Factoids at this time")) + } +} + +impl<T: Database> fmt::Debug for Factoids<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Factoids {{ ... }}") + } +} diff --git a/src/plugins/factoids/sandbox.lua b/src/plugins/factoids/sandbox.lua new file mode 100644 index 0000000..3fde65e --- /dev/null +++ b/src/plugins/factoids/sandbox.lua @@ -0,0 +1,51 @@ +function send(text) + local text = tostring(text) + local len = #output + if len < 1 then + output = { text } + else + output[len] = output[len] .. text + end +end + +function sendln(text) + send(text) + table.insert(output, "") +end + +local env = { print = send, + println = sendln, + args = args, + input = input, + user = user, + channel = channel, + request = download, + pairs = pairs, + table = table, + string = string, + tostring = tostring, + tonumber = tonumber, + math = math } + +local f, e = load(factoid, nil, nil, env) + +-- Check if the factoid timed out +function checktime(event, line) + if os.time() - time >= timeout then + error("Timed out after " .. timeout .. " seconds", 0) + else + -- Limit the cpu usage of factoids + sleep(1) + end +end + +-- Add timeout hook +time = os.time() +timeout = 30 +debug.sethook(checktime, "l") + +if f then + f() +else + error(e) +end diff --git a/src/plugins/factoids/utils.rs b/src/plugins/factoids/utils.rs new file mode 100644 index 0000000..fc86fb3 --- /dev/null +++ b/src/plugins/factoids/utils.rs @@ -0,0 +1,21 @@ +extern crate reqwest; + +use std::thread; +use std::time::Duration; + +use utils; +use super::rlua::prelude::*; + +use self::LuaError::RuntimeError; + +pub fn download(_: &Lua, url: String) -> Result<String, LuaError> { + match utils::download(1024, &url) { + Some(v) => Ok(v), + None => Err(RuntimeError(format!("Failed to download {}", url))), + } +} + +pub fn sleep(_: &Lua, dur: u64) -> Result<(), LuaError> { + thread::sleep(Duration::from_millis(dur)); + Ok(()) +} |
