aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/factoid
diff options
context:
space:
mode:
authorJokler <jokler.contact@gmail.com>2019-06-22 15:51:21 +0200
committerJokler <jokler.contact@gmail.com>2019-06-22 15:51:21 +0200
commit3592c7b6fb2522ff57c7f312b8927eb680d6dc5c (patch)
treed484a367c205afe43ba7327a888b06844fd24c0c /src/plugins/factoid
parent237f6ebe59c90d4ceddd9af6a8a19e562d304aaa (diff)
parenta92e622a0d42911e8e46239c3bde17169ed60c92 (diff)
downloadfrippy-3592c7b6fb2522ff57c7f312b8927eb680d6dc5c.tar.gz
frippy-3592c7b6fb2522ff57c7f312b8927eb680d6dc5c.zip
Merge branch 'dev'HEADv0.5.0master
Diffstat (limited to 'src/plugins/factoid')
-rw-r--r--src/plugins/factoid/database.rs161
-rw-r--r--src/plugins/factoid/mod.rs376
-rw-r--r--src/plugins/factoid/sandbox.lua126
-rw-r--r--src/plugins/factoid/utils.rs82
4 files changed, 745 insertions, 0 deletions
diff --git a/src/plugins/factoid/database.rs b/src/plugins/factoid/database.rs
new file mode 100644
index 0000000..5e7e24c
--- /dev/null
+++ b/src/plugins/factoid/database.rs
@@ -0,0 +1,161 @@
+use std::collections::HashMap;
+#[cfg(feature = "mysql")]
+use std::sync::Arc;
+
+#[cfg(feature = "mysql")]
+use diesel::mysql::MysqlConnection;
+#[cfg(feature = "mysql")]
+use diesel::prelude::*;
+#[cfg(feature = "mysql")]
+use failure::ResultExt;
+#[cfg(feature = "mysql")]
+use r2d2::Pool;
+#[cfg(feature = "mysql")]
+use r2d2_diesel::ConnectionManager;
+
+use chrono::NaiveDateTime;
+
+use super::error::*;
+
+#[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_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 + Sync {
+ fn insert_factoid(&mut self, factoid: &NewFactoid) -> Result<(), FactoidError>;
+ fn get_factoid(&self, name: &str, idx: i32) -> Result<Factoid, FactoidError>;
+ fn delete_factoid(&mut self, name: &str, idx: i32) -> Result<(), FactoidError>;
+ fn count_factoids(&self, name: &str) -> Result<i32, FactoidError>;
+}
+
+// HashMap
+impl<S: ::std::hash::BuildHasher + Send + Sync> Database for HashMap<(String, i32), Factoid, S> {
+ fn insert_factoid(&mut self, factoid: &NewFactoid) -> Result<(), FactoidError> {
+ let factoid = Factoid {
+ name: factoid.name.to_owned(),
+ idx: factoid.idx,
+ content: factoid.content.to_owned(),
+ author: factoid.author.to_owned(),
+ created: factoid.created,
+ };
+
+ let name = factoid.name.clone();
+ match self.insert((name, factoid.idx), factoid) {
+ None => Ok(()),
+ Some(_) => Err(ErrorKind::Duplicate)?,
+ }
+ }
+
+ fn get_factoid(&self, name: &str, idx: i32) -> Result<Factoid, FactoidError> {
+ Ok(self
+ .get(&(name.to_owned(), idx))
+ .cloned()
+ .ok_or(ErrorKind::NotFound)?)
+ }
+
+ fn delete_factoid(&mut self, name: &str, idx: i32) -> Result<(), FactoidError> {
+ match self.remove(&(name.to_owned(), idx)) {
+ Some(_) => Ok(()),
+ None => Err(ErrorKind::NotFound)?,
+ }
+ }
+
+ fn count_factoids(&self, name: &str) -> Result<i32, FactoidError> {
+ Ok(self.iter().filter(|&(&(ref n, _), _)| n == name).count() as i32)
+ }
+}
+
+// Diesel automatically defines the factoids module as public.
+// We create a schema module to keep it private.
+#[cfg(feature = "mysql")]
+mod schema {
+ table! {
+ factoids (name, idx) {
+ name -> Varchar,
+ idx -> Integer,
+ content -> Text,
+ author -> Varchar,
+ created -> Timestamp,
+ }
+ }
+}
+
+#[cfg(feature = "mysql")]
+use self::schema::factoids;
+
+#[cfg(feature = "mysql")]
+impl Database for Arc<Pool<ConnectionManager<MysqlConnection>>> {
+ fn insert_factoid(&mut self, factoid: &NewFactoid) -> Result<(), FactoidError> {
+ use diesel;
+
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+ diesel::insert_into(factoids::table)
+ .values(factoid)
+ .execute(conn)
+ .context(ErrorKind::MysqlError)?;
+
+ Ok(())
+ }
+
+ fn get_factoid(&self, name: &str, idx: i32) -> Result<Factoid, FactoidError> {
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+ Ok(factoids::table
+ .find((name, idx))
+ .first(conn)
+ .context(ErrorKind::MysqlError)?)
+ }
+
+ fn delete_factoid(&mut self, name: &str, idx: i32) -> Result<(), FactoidError> {
+ use self::factoids::columns;
+ use diesel;
+
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+ match diesel::delete(
+ factoids::table
+ .filter(columns::name.eq(name))
+ .filter(columns::idx.eq(idx)),
+ ).execute(conn)
+ {
+ Ok(v) => {
+ if v > 0 {
+ Ok(())
+ } else {
+ Err(ErrorKind::NotFound)?
+ }
+ }
+ Err(e) => Err(e).context(ErrorKind::MysqlError)?,
+ }
+ }
+
+ fn count_factoids(&self, name: &str) -> Result<i32, FactoidError> {
+ use diesel;
+
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+ let count: Result<i64, _> = factoids::table
+ .filter(factoids::columns::name.eq(name))
+ .count()
+ .get_result(conn);
+
+ match count {
+ Ok(c) => Ok(c as i32),
+ Err(diesel::NotFound) => Ok(0),
+ Err(e) => Err(e).context(ErrorKind::MysqlError)?,
+ }
+ }
+}
diff --git a/src/plugins/factoid/mod.rs b/src/plugins/factoid/mod.rs
new file mode 100644
index 0000000..4fcc7a0
--- /dev/null
+++ b/src/plugins/factoid/mod.rs
@@ -0,0 +1,376 @@
+extern crate rlua;
+
+use self::rlua::prelude::*;
+use antidote::RwLock;
+use irc::client::prelude::*;
+use std::fmt;
+use std::marker::PhantomData;
+use std::str::FromStr;
+
+use chrono::NaiveDateTime;
+use time;
+
+use plugin::*;
+use FrippyClient;
+pub mod database;
+use self::database::Database;
+
+mod utils;
+use self::utils::*;
+use utils::Url;
+
+use self::error::*;
+use error::ErrorKind as FrippyErrorKind;
+use error::FrippyError;
+use failure::ResultExt;
+
+static LUA_SANDBOX: &'static str = include_str!("sandbox.lua");
+
+enum FactoidResponse {
+ Public(String),
+ Private(String),
+}
+
+#[derive(PluginName)]
+pub struct Factoid<T: Database, C: Client> {
+ factoids: RwLock<T>,
+ phantom: PhantomData<C>,
+}
+
+impl<T: Database, C: Client> Factoid<T, C> {
+ pub fn new(db: T) -> Self {
+ Factoid {
+ factoids: RwLock::new(db),
+ phantom: PhantomData,
+ }
+ }
+
+ fn create_factoid(
+ &self,
+ name: &str,
+ content: &str,
+ author: &str,
+ ) -> Result<&str, FactoidError> {
+ let count = self.factoids.read().count_factoids(name)?;
+ let tm = time::now().to_timespec();
+
+ let factoid = database::NewFactoid {
+ name,
+ idx: count,
+ content,
+ author,
+ created: NaiveDateTime::from_timestamp(tm.sec, 0u32),
+ };
+
+ Ok(self
+ .factoids
+ .write()
+ .insert_factoid(&factoid)
+ .map(|()| "Successfully added!")?)
+ }
+
+ fn add(&self, command: &mut PluginCommand) -> Result<&str, FactoidError> {
+ if command.tokens.len() < 2 {
+ Err(ErrorKind::InvalidCommand)?;
+ }
+
+ let name = command.tokens.remove(0);
+ let content = command.tokens.join(" ");
+
+ Ok(self.create_factoid(&name, &content, &command.source)?)
+ }
+
+ fn add_from_url(&self, command: &mut PluginCommand) -> Result<&str, FactoidError> {
+ if command.tokens.len() < 2 {
+ Err(ErrorKind::InvalidCommand)?;
+ }
+
+ let name = command.tokens.remove(0);
+ let url = &command.tokens[0];
+ let content = Url::from(url.as_ref())
+ .max_kib(1024)
+ .request()
+ .context(ErrorKind::Download)?;
+
+ Ok(self.create_factoid(&name, &content, &command.source)?)
+ }
+
+ fn remove(&self, command: &mut PluginCommand) -> Result<&str, FactoidError> {
+ if command.tokens.is_empty() {
+ Err(ErrorKind::InvalidCommand)?;
+ }
+
+ let name = command.tokens.remove(0);
+ let count = self.factoids.read().count_factoids(&name)?;
+
+ match self.factoids.write().delete_factoid(&name, count - 1) {
+ Ok(()) => Ok("Successfully removed"),
+ Err(e) => Err(e)?,
+ }
+ }
+
+ fn get(&self, command: &PluginCommand) -> Result<String, FactoidError> {
+ let (name, idx) = match command.tokens.len() {
+ 0 => Err(ErrorKind::InvalidCommand)?,
+ 1 => {
+ let name = &command.tokens[0];
+ let count = self.factoids.read().count_factoids(name)?;
+
+ if count < 1 {
+ Err(ErrorKind::NotFound)?;
+ }
+
+ (name, count - 1)
+ }
+ _ => {
+ let name = &command.tokens[0];
+ let idx = match i32::from_str(&command.tokens[1]) {
+ Ok(i) => i,
+ Err(_) => Err(ErrorKind::InvalidCommand)?,
+ };
+
+ (name, idx)
+ }
+ };
+
+ let factoid = self
+ .factoids
+ .read()
+ .get_factoid(name, idx)
+ .context(ErrorKind::NotFound)?;
+
+ let message = factoid.content.replace("\n", "|").replace("\r", "");
+
+ Ok(format!("{}: {}", factoid.name, message))
+ }
+
+ fn info(&self, command: &PluginCommand) -> Result<String, FactoidError> {
+ match command.tokens.len() {
+ 0 => Err(ErrorKind::InvalidCommand)?,
+ 1 => {
+ let name = &command.tokens[0];
+ let count = self.factoids.read().count_factoids(name)?;
+
+ Ok(match count {
+ 0 => Err(ErrorKind::NotFound)?,
+ 1 => format!("There is 1 version of {}", name),
+ _ => format!("There are {} versions of {}", count, name),
+ })
+ }
+ _ => {
+ let name = &command.tokens[0];
+ let idx = i32::from_str(&command.tokens[1]).context(ErrorKind::InvalidIndex)?;
+ let factoid = self.factoids.read().get_factoid(name, idx)?;
+
+ Ok(format!(
+ "{}: Added by {} at {} UTC",
+ name, factoid.author, factoid.created
+ ))
+ }
+ }
+ }
+
+ fn exec(&self, mut command: PluginCommand) -> Result<String, FactoidError> {
+ if command.tokens.is_empty() {
+ Err(ErrorKind::InvalidIndex)?
+ } else {
+ let name = command.tokens.remove(0);
+ let count = self.factoids.read().count_factoids(&name)?;
+ let factoid = self.factoids.read().get_factoid(&name, count - 1)?;
+
+ let content = factoid.content;
+ let value = if content.starts_with('>') {
+ let content = String::from(&content[1..]);
+
+ if content.starts_with('>') {
+ content
+ } else {
+ match self.run_lua(&name, &content, &command) {
+ Ok(v) => v,
+ Err(e) => match e {
+ LuaError::CallbackError { cause, .. } => cause.to_string(),
+ _ => e.to_string(),
+ },
+ }
+ }
+ } else {
+ content
+ };
+
+ Ok(value.replace("\n", "|").replace("\r", ""))
+ }
+ }
+
+ fn run_lua(&self, name: &str, code: &str, command: &PluginCommand) -> Result<String, LuaError> {
+ 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("json_decode", lua.create_function(json_decode)?)?;
+ 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 help(&self) -> &str {
+ "usage: factoids <subcommand>\r\n\
+ subcommands: add, fromurl, remove, get, info, exec, help"
+ }
+}
+
+impl<T: Database, C: FrippyClient> Plugin for Factoid<T, C> {
+ type Client = C;
+ fn execute(&self, _: &Self::Client, 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: &Self::Client,
+ message: &Message,
+ ) -> Result<(), FrippyError> {
+ 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_owned(),
+ target: message.response_target().unwrap().to_owned(),
+ tokens: t,
+ };
+
+ if let Ok(f) = self.exec(c) {
+ client
+ .send_privmsg(&message.response_target().unwrap(), &f)
+ .context(FrippyErrorKind::Connection)?;
+ }
+ }
+
+ Ok(())
+ }
+
+ fn command(
+ &self,
+ client: &Self::Client,
+ mut command: PluginCommand,
+ ) -> Result<(), FrippyError> {
+ use self::FactoidResponse::{Private, Public};
+
+ if command.tokens.is_empty() {
+ client
+ .send_notice(&command.source, "Invalid command")
+ .context(FrippyErrorKind::Connection)?;
+
+ return Ok(());
+ }
+
+ let target = command.target.clone();
+ let source = command.source.clone();
+
+ let sub_command = command.tokens.remove(0);
+ let result = match sub_command.as_ref() {
+ "add" => self.add(&mut command).map(|s| Private(s.to_owned())),
+ "fromurl" => self
+ .add_from_url(&mut command)
+ .map(|s| Private(s.to_owned())),
+ "remove" => self.remove(&mut command).map(|s| Private(s.to_owned())),
+ "get" => self.get(&command).map(Public),
+ "info" => self.info(&command).map(Public),
+ "exec" => self.exec(command).map(Public),
+ "help" => Ok(Private(self.help().to_owned())),
+ _ => Err(ErrorKind::InvalidCommand.into()),
+ };
+
+ match result {
+ Ok(v) => match v {
+ Public(m) => client
+ .send_privmsg(&target, &m)
+ .context(FrippyErrorKind::Connection)?,
+ Private(m) => client
+ .send_notice(&source, &m)
+ .context(FrippyErrorKind::Connection)?,
+ },
+ Err(e) => {
+ let message = e.to_string();
+ client
+ .send_notice(&source, &message)
+ .context(FrippyErrorKind::Connection)?;
+ Err(e).context(FrippyErrorKind::Factoid)?
+ }
+ }
+
+ Ok(())
+ }
+
+ fn evaluate(&self, _: &Self::Client, _: PluginCommand) -> Result<String, String> {
+ Err(String::from(
+ "Evaluation of commands is not implemented for Factoid at this time",
+ ))
+ }
+}
+
+impl<T: Database, C: FrippyClient> fmt::Debug for Factoid<T, C> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "Factoid {{ ... }}")
+ }
+}
+
+pub mod error {
+ #[derive(Copy, Clone, Eq, PartialEq, Debug, Fail, Error)]
+ #[error = "FactoidError"]
+ pub enum ErrorKind {
+ /// Invalid command error
+ #[fail(display = "Invalid Command")]
+ InvalidCommand,
+
+ /// Invalid index error
+ #[fail(display = "Invalid index")]
+ InvalidIndex,
+
+ /// Download error
+ #[fail(display = "Download failed")]
+ Download,
+
+ /// Duplicate error
+ #[fail(display = "Entry already exists")]
+ Duplicate,
+
+ /// Not found error
+ #[fail(display = "Factoid was not found")]
+ NotFound,
+
+ /// MySQL error
+ #[cfg(feature = "mysql")]
+ #[fail(display = "Failed to execute MySQL Query")]
+ MysqlError,
+
+ /// No connection error
+ #[cfg(feature = "mysql")]
+ #[fail(display = "No connection to the database")]
+ NoConnection,
+ }
+}
diff --git a/src/plugins/factoid/sandbox.lua b/src/plugins/factoid/sandbox.lua
new file mode 100644
index 0000000..a927535
--- /dev/null
+++ b/src/plugins/factoid/sandbox.lua
@@ -0,0 +1,126 @@
+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
+
+function trim(s)
+ local from = s:match"^%s*()"
+ return from > #s and "" or s:match(".*%S", from)
+end
+
+trimmedInput = trim(input)
+
+if trimmedInput == "" then
+ ioru = user
+else
+ ioru = trimmedInput
+end
+
+local sandbox_env = {
+ print = send,
+ println = sendln,
+ trim = trim,
+ eval = nil,
+ sleep = nil,
+ json = {decode = json_decode},
+ args = args,
+ input = input,
+ user = user,
+ ioru = ioru,
+ channel = channel,
+ request = download,
+ string = string,
+ math = math,
+ table = table,
+ pairs = pairs,
+ ipairs = ipairs,
+ next = next,
+ select = select,
+ unpack = unpack,
+ tostring = tostring,
+ tonumber = tonumber,
+ type = type,
+ assert = assert,
+ error = error,
+ pcall = pcall,
+ xpcall = xpcall,
+ _VERSION = _VERSION
+}
+
+sandbox_env.os = {
+ clock = os.clock,
+ time = os.time,
+ difftime = os.difftime
+}
+
+sandbox_env.string.rep = nil
+sandbox_env.string.dump = nil
+sandbox_env.math.randomseed = nil
+
+-- Temporary evaluation function
+function eval(code)
+ local c, e = load(code, nil, nil, sandbox_env)
+ if c then
+ return c()
+ else
+ error(e)
+ end
+end
+
+-- Only sleeps for 1 second at a time
+-- This ensures that the timeout check can still run
+function safesleep(dur)
+ while dur > 1000 do
+ dur = dur - 1000
+ sleep(1000)
+ end
+ sleep(dur)
+end
+
+sandbox_env.eval = eval
+sandbox_env.sleep = safesleep
+
+-- Check if the factoid timed out
+function checktime()
+ if os.time() - time >= timeout then
+ error("Timed out after " .. timeout .. " seconds", 0)
+ else
+ -- Limit the cpu usage of factoids
+ sleep(1)
+ end
+end
+
+-- Check if the factoid uses too much memory
+function checkmem()
+ if collectgarbage("count") > maxmem then
+ error("Factoid used over " .. maxmem .. " kbyte of ram")
+ end
+end
+
+local f, e = load(factoid, nil, nil, sandbox_env)
+
+-- Add timeout hook
+time = os.time()
+-- The timeout is defined in seconds
+timeout = 30
+debug.sethook(checktime, "l")
+-- Add memory check hook
+-- The max memory is defined in kilobytes
+maxmem = 1000
+debug.sethook(checkmem, "l")
+
+if f then
+ f()
+else
+ error(e)
+end
diff --git a/src/plugins/factoid/utils.rs b/src/plugins/factoid/utils.rs
new file mode 100644
index 0000000..7bd9b20
--- /dev/null
+++ b/src/plugins/factoid/utils.rs
@@ -0,0 +1,82 @@
+use std::thread;
+use std::time::Duration;
+
+use serde_json::{self, Value as SerdeValue};
+
+use super::rlua::Error as LuaError;
+use super::rlua::Error::RuntimeError;
+use super::rlua::{Lua, Value as LuaValue};
+
+use utils::error::ErrorKind::Connection;
+use utils::Url;
+
+use failure::Fail;
+
+pub fn sleep(_: &Lua, dur: u64) -> Result<(), LuaError> {
+ thread::sleep(Duration::from_millis(dur));
+ Ok(())
+}
+
+pub fn download(_: &Lua, url: String) -> Result<String, LuaError> {
+ let url = Url::from(url).max_kib(1024);
+ match url.request() {
+ Ok(v) => Ok(v),
+ Err(e) => {
+ let error = match e.kind() {
+ Connection => e.cause().unwrap().to_string(),
+ _ => e.to_string(),
+ };
+
+ Err(RuntimeError(format!(
+ "Failed to download {} - {}",
+ url.as_str(),
+ error
+ )))
+ }
+ }
+}
+
+fn convert_value(lua: &Lua, sval: SerdeValue, max_recurs: usize) -> Result<LuaValue, LuaError> {
+ if max_recurs == 0 {
+ return Err(RuntimeError(String::from(
+ "Reached max recursion level - json is nested too deep",
+ )));
+ }
+
+ let lval = match sval {
+ SerdeValue::Null => LuaValue::Nil,
+ SerdeValue::Bool(b) => LuaValue::Boolean(b),
+ SerdeValue::String(s) => LuaValue::String(lua.create_string(&s)?),
+ SerdeValue::Number(n) => {
+ let f = n.as_f64().ok_or_else(|| {
+ RuntimeError(String::from("Failed to convert number into double"))
+ })?;
+ LuaValue::Number(f)
+ }
+ SerdeValue::Array(arr) => {
+ let table = lua.create_table()?;
+ for (i, val) in arr.into_iter().enumerate() {
+ table.set(i + 1, convert_value(lua, val, max_recurs - 1)?)?;
+ }
+
+ LuaValue::Table(table)
+ }
+ SerdeValue::Object(obj) => {
+ let table = lua.create_table()?;
+ for (key, val) in obj {
+ table.set(key, convert_value(lua, val, max_recurs - 1)?)?;
+ }
+
+ LuaValue::Table(table)
+ }
+ };
+
+ Ok(lval)
+}
+
+pub fn json_decode(lua: &Lua, json: String) -> Result<LuaValue, LuaError> {
+ let ser_val: SerdeValue =
+ serde_json::from_str(&json).map_err(|e| RuntimeError(e.to_string()))?;
+
+ convert_value(lua, ser_val, 25)
+}